diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/DISCUSSION_TEMPLATE/questions.yml similarity index 82% rename from .github/ISSUE_TEMPLATE/question.yml rename to .github/DISCUSSION_TEMPLATE/questions.yml index 3b16b4ad0c..98424a341d 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -1,5 +1,3 @@ -name: Question or Problem -description: Ask a question or ask about a problem labels: [question] body: - type: markdown @@ -9,9 +7,9 @@ body: Please follow these instructions, fill every question, and do every step. 🙏 - I'm asking this because answering questions and solving problems in GitHub issues is what consumes most of the time. + I'm asking this because answering questions and solving problems in GitHub is what consumes most of the time. - I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling issues. + I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling questions. All that, on top of all the incredible help provided by a bunch of community members, the [FastAPI Experts](https://fastapi.tiangolo.com/fastapi-people/#experts), that give a lot of their time to come here and help others. @@ -21,16 +19,16 @@ body: And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 - As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 + As there are too many questions, I'll have to discard and close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 - type: checkboxes id: checks attributes: label: First Check description: Please confirm and check all the following options. options: - - label: I added a very descriptive title to this issue. + - label: I added a very descriptive title here. required: true - - label: I used the GitHub search to find a similar issue and didn't find it. + - label: I used the GitHub search to find a similar question and didn't find it. required: true - label: I searched the FastAPI documentation, with the integrated search. required: true @@ -38,7 +36,7 @@ body: required: true - label: I already read and followed all the tutorial in the docs and didn't find an answer. required: true - - label: I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/samuelcolvin/pydantic). + - label: I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/pydantic/pydantic). required: true - label: I already checked if it is not related to FastAPI but to [Swagger UI](https://github.com/swagger-api/swagger-ui). required: true @@ -51,9 +49,9 @@ body: description: | 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. + * Read open questions until I find 2 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. + * Review one Pull Request by downloading the code and following [all the review process](https://fastapi.tiangolo.com/help-fastapi/#review-pull-requests). options: - label: I commit to help with one of those options 👆 @@ -125,6 +123,20 @@ body: ``` validations: required: true + - type: input + id: pydantic-version + attributes: + label: Pydantic Version + description: | + What Pydantic version are you using? + + You can find the Pydantic version with: + + ```bash + python -c "import pydantic; print(pydantic.version.VERSION)" + ``` + validations: + required: true - type: input id: python-version attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 55749398fd..a8f4c4de2d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,3 +2,15 @@ blank_issues_enabled: false contact_links: - name: Security Contact about: Please report security vulnerabilities to security@tiangolo.com + - name: Question or Problem + about: Ask a question or ask about a problem in GitHub Discussions. + url: https://github.com/tiangolo/fastapi/discussions/categories/questions + - name: Feature Request + about: To suggest an idea or ask about a feature, please start with a question saying what you would like to achieve. There might be a way to do it already. + url: https://github.com/tiangolo/fastapi/discussions/categories/questions + - name: Show and tell + about: Show what you built with FastAPI or to be used with FastAPI. + url: https://github.com/tiangolo/fastapi/discussions/categories/show-and-tell + - name: Translations + about: Coordinate translations in GitHub Discussions. + url: https://github.com/tiangolo/fastapi/discussions/categories/translations diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml deleted file mode 100644 index 322b6536a7..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ /dev/null @@ -1,181 +0,0 @@ -name: Feature Request -description: Suggest an idea or ask for a feature that you would like to have in FastAPI -labels: [enhancement] -body: - - type: markdown - attributes: - value: | - Thanks for your interest in FastAPI! 🚀 - - Please follow these instructions, fill every question, and do every step. 🙏 - - I'm asking this because answering questions and solving problems in GitHub issues is what consumes most of the time. - - I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling issues. - - All that, on top of all the incredible help provided by a bunch of community members, the [FastAPI Experts](https://fastapi.tiangolo.com/fastapi-people/#experts), that give a lot of their time to come here and help others. - - That's a lot of work they are doing, but if more FastAPI users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅). - - By asking questions in a structured way (following this) it will be much easier to help you. - - And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 - - As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 - - type: checkboxes - id: checks - attributes: - label: First Check - description: Please confirm and check all the following options. - options: - - label: I added a very descriptive title to this issue. - required: true - - label: I used the GitHub search to find a similar issue and didn't find it. - required: true - - label: I searched the FastAPI documentation, with the integrated search. - required: true - - label: I already searched in Google "How to X in FastAPI" and didn't find any information. - required: true - - label: I already read and followed all the tutorial in the docs and didn't find an answer. - required: true - - label: I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/samuelcolvin/pydantic). - required: true - - label: I already checked if it is not related to FastAPI but to [Swagger UI](https://github.com/swagger-api/swagger-ui). - required: true - - label: I already checked if it is not related to FastAPI but to [ReDoc](https://github.com/Redocly/redoc). - required: true - - type: checkboxes - id: help - attributes: - label: Commit to Help - description: | - 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. - - options: - - label: I commit to help with one of those options 👆 - required: true - - type: textarea - id: example - attributes: - label: Example Code - description: | - Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. - - If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. - - placeholder: | - from fastapi import FastAPI - - app = FastAPI() - - - @app.get("/") - def read_root(): - return {"Hello": "World"} - render: python - validations: - required: true - - type: textarea - id: description - attributes: - label: Description - description: | - What is your feature request? - - Write a short description telling me what you are trying to solve and what you are currently doing. - placeholder: | - * 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. - validations: - required: true - - type: textarea - id: wanted-solution - attributes: - label: Wanted Solution - description: | - Tell me what's the solution you would like. - placeholder: | - I would like it to have a `teleport_to_moon` parameter that defaults to `False`, and can be set to `True` to teleport me. - validations: - required: true - - type: textarea - id: wanted-code - attributes: - label: Wanted Code - description: Show me an example of how you would want the code to look like. - placeholder: | - from fastapi import FastAPI - - app = FastAPI() - - - @app.get("/", teleport_to_moon=True) - def read_root(): - return {"Hello": "World"} - render: python - validations: - required: true - - type: textarea - id: alternatives - attributes: - label: Alternatives - description: | - Tell me about alternatives you've considered. - placeholder: | - To wait for Space X moon travel plans to drop down long after they release them. But I would rather teleport. - - type: dropdown - id: os - attributes: - label: Operating System - description: What operating system are you on? - multiple: true - options: - - Linux - - Windows - - macOS - - Other - validations: - required: true - - type: textarea - id: os-details - attributes: - label: Operating System Details - description: You can add more details about your operating system here, in particular if you chose "Other". - - type: input - id: fastapi-version - attributes: - label: FastAPI Version - description: | - What FastAPI version are you using? - - You can find the FastAPI version with: - - ```bash - python -c "import fastapi; print(fastapi.__version__)" - ``` - validations: - required: true - - type: input - id: python-version - attributes: - label: Python Version - description: | - What Python version are you using? - - You can find the Python version with: - - ```bash - python --version - ``` - validations: - required: true - - type: textarea - id: context - attributes: - label: Additional Context - description: Add any additional context information or screenshots you think are useful. diff --git a/.github/ISSUE_TEMPLATE/privileged.yml b/.github/ISSUE_TEMPLATE/privileged.yml new file mode 100644 index 0000000000..c01e34b6dd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/privileged.yml @@ -0,0 +1,22 @@ +name: Privileged +description: You are @tiangolo or he asked you directly to create an issue here. If not, check the other options. 👇 +body: + - type: markdown + attributes: + value: | + Thanks for your interest in FastAPI! 🚀 + + If you are not @tiangolo or he didn't ask you directly to create an issue here, please start the conversation in a [Question in GitHub Discussions](https://github.com/tiangolo/fastapi/discussions/categories/questions) instead. + - type: checkboxes + id: privileged + attributes: + label: Privileged issue + description: Confirm that you are allowed to create an issue here. + options: + - label: I'm @tiangolo or he asked me directly to create an issue here. + required: true + - type: textarea + id: content + attributes: + label: Issue Content + description: Add the content of the issue here. diff --git a/.github/actions/comment-docs-preview-in-pr/Dockerfile b/.github/actions/comment-docs-preview-in-pr/Dockerfile index 4f20c5f10b..42627fe190 100644 --- a/.github/actions/comment-docs-preview-in-pr/Dockerfile +++ b/.github/actions/comment-docs-preview-in-pr/Dockerfile @@ -1,6 +1,8 @@ -FROM python:3.7 +FROM python:3.10 -RUN pip install httpx "pydantic==1.5.1" pygithub +COPY ./requirements.txt /app/requirements.txt + +RUN pip install -r /app/requirements.txt COPY ./app /app diff --git a/.github/actions/comment-docs-preview-in-pr/app/main.py b/.github/actions/comment-docs-preview-in-pr/app/main.py index 68914fdb9a..8cc119fe0a 100644 --- a/.github/actions/comment-docs-preview-in-pr/app/main.py +++ b/.github/actions/comment-docs-preview-in-pr/app/main.py @@ -6,7 +6,8 @@ from typing import Union import httpx from github import Github from github.PullRequest import PullRequest -from pydantic import BaseModel, BaseSettings, SecretStr, ValidationError +from pydantic import BaseModel, SecretStr, ValidationError +from pydantic_settings import BaseSettings github_api = "https://api.github.com" diff --git a/.github/actions/comment-docs-preview-in-pr/requirements.txt b/.github/actions/comment-docs-preview-in-pr/requirements.txt new file mode 100644 index 0000000000..74a3631f48 --- /dev/null +++ b/.github/actions/comment-docs-preview-in-pr/requirements.txt @@ -0,0 +1,4 @@ +PyGithub +pydantic>=2.5.3,<3.0.0 +pydantic-settings>=2.1.0,<3.0.0 +httpx diff --git a/.github/actions/notify-translations/Dockerfile b/.github/actions/notify-translations/Dockerfile index fa4197e6a8..b68b4bb1a2 100644 --- a/.github/actions/notify-translations/Dockerfile +++ b/.github/actions/notify-translations/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7 +FROM python:3.9 RUN pip install httpx PyGithub "pydantic==1.5.1" "pyyaml>=5.3.1,<6.0.0" diff --git a/.github/actions/notify-translations/app/main.py b/.github/actions/notify-translations/app/main.py index d4ba0ecfce..8ac1f233d6 100644 --- a/.github/actions/notify-translations/app/main.py +++ b/.github/actions/notify-translations/app/main.py @@ -1,24 +1,185 @@ import logging import random +import sys import time from pathlib import Path -from typing import Dict, Union +from typing import Any, Dict, List, Union, cast -import yaml +import httpx from github import Github from pydantic import BaseModel, BaseSettings, SecretStr -awaiting_label = "awaiting review" +awaiting_label = "awaiting-review" lang_all_label = "lang-all" approved_label = "approved-2" translations_path = Path(__file__).parent / "translations.yml" +github_graphql_url = "https://api.github.com/graphql" +questions_translations_category_id = "DIC_kwDOCZduT84CT5P9" + +all_discussions_query = """ +query Q($category_id: ID) { + repository(name: "fastapi", owner: "tiangolo") { + discussions(categoryId: $category_id, first: 100) { + nodes { + title + id + number + labels(first: 10) { + edges { + node { + id + name + } + } + } + } + } + } +} +""" + +translation_discussion_query = """ +query Q($after: String, $discussion_number: Int!) { + repository(name: "fastapi", owner: "tiangolo") { + discussion(number: $discussion_number) { + comments(first: 100, after: $after) { + edges { + cursor + node { + id + url + body + } + } + } + } + } +} +""" + +add_comment_mutation = """ +mutation Q($discussion_id: ID!, $body: String!) { + addDiscussionComment(input: {discussionId: $discussion_id, body: $body}) { + comment { + id + url + body + } + } +} +""" + +update_comment_mutation = """ +mutation Q($comment_id: ID!, $body: String!) { + updateDiscussionComment(input: {commentId: $comment_id, body: $body}) { + comment { + id + url + body + } + } +} +""" + + +class Comment(BaseModel): + id: str + url: str + body: str + + +class UpdateDiscussionComment(BaseModel): + comment: Comment + + +class UpdateCommentData(BaseModel): + updateDiscussionComment: UpdateDiscussionComment + + +class UpdateCommentResponse(BaseModel): + data: UpdateCommentData + + +class AddDiscussionComment(BaseModel): + comment: Comment + + +class AddCommentData(BaseModel): + addDiscussionComment: AddDiscussionComment + + +class AddCommentResponse(BaseModel): + data: AddCommentData + + +class CommentsEdge(BaseModel): + node: Comment + cursor: str + + +class Comments(BaseModel): + edges: List[CommentsEdge] + + +class CommentsDiscussion(BaseModel): + comments: Comments + + +class CommentsRepository(BaseModel): + discussion: CommentsDiscussion + + +class CommentsData(BaseModel): + repository: CommentsRepository + + +class CommentsResponse(BaseModel): + data: CommentsData + + +class AllDiscussionsLabelNode(BaseModel): + id: str + name: str + + +class AllDiscussionsLabelsEdge(BaseModel): + node: AllDiscussionsLabelNode + + +class AllDiscussionsDiscussionLabels(BaseModel): + edges: List[AllDiscussionsLabelsEdge] + + +class AllDiscussionsDiscussionNode(BaseModel): + title: str + id: str + number: int + labels: AllDiscussionsDiscussionLabels + + +class AllDiscussionsDiscussions(BaseModel): + nodes: List[AllDiscussionsDiscussionNode] + + +class AllDiscussionsRepository(BaseModel): + discussions: AllDiscussionsDiscussions + + +class AllDiscussionsData(BaseModel): + repository: AllDiscussionsRepository + + +class AllDiscussionsResponse(BaseModel): + data: AllDiscussionsData + class Settings(BaseSettings): github_repository: str input_token: SecretStr github_event_path: Path github_event_name: Union[str, None] = None + httpx_timeout: int = 30 input_debug: Union[bool, None] = False @@ -30,6 +191,113 @@ class PartialGitHubEvent(BaseModel): pull_request: PartialGitHubEventIssue +def get_graphql_response( + *, + settings: Settings, + query: str, + after: Union[str, None] = None, + category_id: Union[str, None] = None, + discussion_number: Union[int, None] = None, + discussion_id: Union[str, None] = None, + comment_id: Union[str, None] = None, + body: Union[str, None] = None, +) -> Dict[str, Any]: + headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} + # some fields are only used by one query, but GraphQL allows unused variables, so + # keep them here for simplicity + variables = { + "after": after, + "category_id": category_id, + "discussion_number": discussion_number, + "discussion_id": discussion_id, + "comment_id": comment_id, + "body": body, + } + response = httpx.post( + github_graphql_url, + headers=headers, + timeout=settings.httpx_timeout, + json={"query": query, "variables": variables, "operationName": "Q"}, + ) + if response.status_code != 200: + logging.error( + f"Response was not 200, after: {after}, category_id: {category_id}" + ) + logging.error(response.text) + raise RuntimeError(response.text) + data = response.json() + if "errors" in data: + logging.error(f"Errors in response, after: {after}, category_id: {category_id}") + logging.error(response.text) + raise RuntimeError(response.text) + return cast(Dict[str, Any], data) + + +def get_graphql_translation_discussions(*, settings: Settings): + data = get_graphql_response( + settings=settings, + query=all_discussions_query, + category_id=questions_translations_category_id, + ) + graphql_response = AllDiscussionsResponse.parse_obj(data) + return graphql_response.data.repository.discussions.nodes + + +def get_graphql_translation_discussion_comments_edges( + *, settings: Settings, discussion_number: int, after: Union[str, None] = None +): + data = get_graphql_response( + settings=settings, + query=translation_discussion_query, + discussion_number=discussion_number, + after=after, + ) + graphql_response = CommentsResponse.parse_obj(data) + return graphql_response.data.repository.discussion.comments.edges + + +def get_graphql_translation_discussion_comments( + *, settings: Settings, discussion_number: int +): + comment_nodes: List[Comment] = [] + discussion_edges = get_graphql_translation_discussion_comments_edges( + settings=settings, discussion_number=discussion_number + ) + + while discussion_edges: + for discussion_edge in discussion_edges: + comment_nodes.append(discussion_edge.node) + last_edge = discussion_edges[-1] + discussion_edges = get_graphql_translation_discussion_comments_edges( + settings=settings, + discussion_number=discussion_number, + after=last_edge.cursor, + ) + return comment_nodes + + +def create_comment(*, settings: Settings, discussion_id: str, body: str): + data = get_graphql_response( + settings=settings, + query=add_comment_mutation, + discussion_id=discussion_id, + body=body, + ) + response = AddCommentResponse.parse_obj(data) + return response.data.addDiscussionComment.comment + + +def update_comment(*, settings: Settings, comment_id: str, body: str): + data = get_graphql_response( + settings=settings, + query=update_comment_mutation, + comment_id=comment_id, + body=body, + ) + response = UpdateCommentResponse.parse_obj(data) + return response.data.updateDiscussionComment.comment + + if __name__ == "__main__": settings = Settings() if settings.input_debug: @@ -45,60 +313,105 @@ if __name__ == "__main__": ) contents = settings.github_event_path.read_text() github_event = PartialGitHubEvent.parse_raw(contents) - translations_map: Dict[str, int] = yaml.safe_load(translations_path.read_text()) - logging.debug(f"Using translations map: {translations_map}") + + # Avoid race conditions with multiple labels sleep_time = random.random() * 10 # random number between 0 and 10 seconds - pr = repo.get_pull(github_event.pull_request.number) - logging.debug( - f"Processing PR: {pr.number}, with anti-race condition sleep time: {sleep_time}" + logging.info( + f"Sleeping for {sleep_time} seconds to avoid " + "race conditions and multiple comments" ) - if pr.state == "open": - logging.debug(f"PR is open: {pr.number}") - label_strs = {label.name for label in pr.get_labels()} - if lang_all_label in label_strs and awaiting_label in label_strs: - logging.info( - f"This PR seems to be a language translation and awaiting reviews: {pr.number}" - ) - if approved_label in label_strs: - message = ( - f"It seems this PR already has the approved label: {pr.number}" - ) - logging.error(message) - raise RuntimeError(message) - langs = [] - for label in label_strs: - if label.startswith("lang-") and not label == lang_all_label: - langs.append(label[5:]) - for lang in langs: - if lang in translations_map: - num = translations_map[lang] - logging.info( - f"Found a translation issue for language: {lang} in issue: {num}" - ) - issue = repo.get_issue(num) - message = f"Good news everyone! 😉 There's a new translation PR to be reviewed: #{pr.number} 🎉" - already_notified = False - time.sleep(sleep_time) - logging.info( - f"Sleeping for {sleep_time} seconds to avoid race conditions and multiple comments" - ) - logging.info( - f"Checking current comments in issue: {num} to see if already notified about this PR: {pr.number}" - ) - for comment in issue.get_comments(): - if message in comment.body: - already_notified = True - if not already_notified: - logging.info( - f"Writing comment in issue: {num} about PR: {pr.number}" - ) - issue.create_comment(message) - else: - logging.info( - f"Issue: {num} was already notified of PR: {pr.number}" - ) - else: + time.sleep(sleep_time) + + # Get PR + logging.debug(f"Processing PR: #{github_event.pull_request.number}") + pr = repo.get_pull(github_event.pull_request.number) + label_strs = {label.name for label in pr.get_labels()} + langs = [] + for label in label_strs: + if label.startswith("lang-") and not label == lang_all_label: + langs.append(label[5:]) + logging.info(f"PR #{pr.number} has labels: {label_strs}") + if not langs or lang_all_label not in label_strs: + logging.info(f"PR #{pr.number} doesn't seem to be a translation PR, skipping") + sys.exit(0) + + # Generate translation map, lang ID to discussion + discussions = get_graphql_translation_discussions(settings=settings) + lang_to_discussion_map: Dict[str, AllDiscussionsDiscussionNode] = {} + for discussion in discussions: + for edge in discussion.labels.edges: + label = edge.node.name + if label.startswith("lang-") and not label == lang_all_label: + lang = label[5:] + lang_to_discussion_map[lang] = discussion + logging.debug(f"Using translations map: {lang_to_discussion_map}") + + # Messages to create or check + new_translation_message = f"Good news everyone! 😉 There's a new translation PR to be reviewed: #{pr.number} by @{pr.user.login}. 🎉 This requires 2 approvals from native speakers to be merged. 🤓" + done_translation_message = f"~There's a new translation PR to be reviewed: #{pr.number} by @{pr.user.login}~ Good job! This is done. 🍰☕" + + # Normally only one language, but still + for lang in langs: + if lang not in lang_to_discussion_map: + log_message = f"Could not find discussion for language: {lang}" + logging.error(log_message) + raise RuntimeError(log_message) + discussion = lang_to_discussion_map[lang] logging.info( - f"Changing labels in a closed PR doesn't trigger comments, PR: {pr.number}" + f"Found a translation discussion for language: {lang} in discussion: #{discussion.number}" ) + + already_notified_comment: Union[Comment, None] = None + already_done_comment: Union[Comment, None] = None + + logging.info( + f"Checking current comments in discussion: #{discussion.number} to see if already notified about this PR: #{pr.number}" + ) + comments = get_graphql_translation_discussion_comments( + settings=settings, discussion_number=discussion.number + ) + for comment in comments: + if new_translation_message in comment.body: + already_notified_comment = comment + elif done_translation_message in comment.body: + already_done_comment = comment + logging.info( + f"Already notified comment: {already_notified_comment}, already done comment: {already_done_comment}" + ) + + if pr.state == "open" and awaiting_label in label_strs: + logging.info( + f"This PR seems to be a language translation and awaiting reviews: #{pr.number}" + ) + if already_notified_comment: + logging.info( + f"This PR #{pr.number} was already notified in comment: {already_notified_comment.url}" + ) + else: + logging.info( + f"Writing notification comment about PR #{pr.number} in Discussion: #{discussion.number}" + ) + comment = create_comment( + settings=settings, + discussion_id=discussion.id, + body=new_translation_message, + ) + logging.info(f"Notified in comment: {comment.url}") + elif pr.state == "closed" or approved_label in label_strs: + logging.info(f"Already approved or closed PR #{pr.number}") + if already_done_comment: + logging.info( + f"This PR #{pr.number} was already marked as done in comment: {already_done_comment.url}" + ) + elif already_notified_comment: + updated_comment = update_comment( + settings=settings, + comment_id=already_notified_comment.id, + body=done_translation_message, + ) + logging.info(f"Marked as done in comment: {updated_comment.url}") + else: + logging.info( + f"There doesn't seem to be anything to be done about PR #{pr.number}" + ) logging.info("Finished") diff --git a/.github/actions/notify-translations/app/translations.yml b/.github/actions/notify-translations/app/translations.yml deleted file mode 100644 index 4338e1326e..0000000000 --- a/.github/actions/notify-translations/app/translations.yml +++ /dev/null @@ -1,21 +0,0 @@ -pt: 1211 -es: 1218 -zh: 1228 -ru: 1362 -it: 1556 -ja: 1572 -uk: 1748 -tr: 1892 -fr: 1972 -ko: 2017 -fa: 2041 -pl: 3169 -de: 3716 -id: 3717 -az: 3994 -nl: 4701 -uz: 4883 -sv: 5146 -he: 5157 -ta: 5434 -ar: 3349 diff --git a/.github/actions/people/Dockerfile b/.github/actions/people/Dockerfile index fa4197e6a8..1455106bde 100644 --- a/.github/actions/people/Dockerfile +++ b/.github/actions/people/Dockerfile @@ -1,6 +1,6 @@ -FROM python:3.7 +FROM python:3.9 -RUN pip install httpx PyGithub "pydantic==1.5.1" "pyyaml>=5.3.1,<6.0.0" +RUN pip install httpx PyGithub "pydantic==2.0.2" pydantic-settings "pyyaml>=5.3.1,<6.0.0" COPY ./app /app diff --git a/.github/actions/people/action.yml b/.github/actions/people/action.yml index 16bc8cdcb8..71745b8747 100644 --- a/.github/actions/people/action.yml +++ b/.github/actions/people/action.yml @@ -3,10 +3,7 @@ description: "Generate the data for the FastAPI People page" author: "Sebastián Ramírez " inputs: token: - description: 'User token, to read the GitHub API. Can be passed in using {{ secrets.ACTION_TOKEN }}' - required: true - standard_token: - description: 'Default GitHub Action token, used for the PR. Can be passed in using {{ secrets.GITHUB_TOKEN }}' + description: 'User token, to read the GitHub API. Can be passed in using {{ secrets.FASTAPI_PEOPLE }}' required: true runs: using: 'docker' diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py index 31756a5fcb..cb6b229e8e 100644 --- a/.github/actions/people/app/main.py +++ b/.github/actions/people/app/main.py @@ -4,14 +4,59 @@ import sys from collections import Counter, defaultdict from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Container, DefaultDict, Dict, List, Set, Union +from typing import Any, Container, DefaultDict, Dict, List, Set, Union import httpx import yaml from github import Github -from pydantic import BaseModel, BaseSettings, SecretStr +from pydantic import BaseModel, SecretStr +from pydantic_settings import BaseSettings github_graphql_url = "https://api.github.com/graphql" +questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0" + +discussions_query = """ +query Q($after: String, $category_id: ID) { + repository(name: "fastapi", owner: "tiangolo") { + discussions(first: 100, after: $after, categoryId: $category_id) { + edges { + cursor + node { + number + author { + login + avatarUrl + url + } + title + createdAt + comments(first: 100) { + nodes { + createdAt + author { + login + avatarUrl + url + } + isAnswer + replies(first: 10) { + nodes { + createdAt + author { + login + avatarUrl + url + } + } + } + } + } + } + } + } + } +} +""" issues_query = """ query Q($after: String) { @@ -131,15 +176,30 @@ class Author(BaseModel): url: str +# Issues and Discussions + + class CommentsNode(BaseModel): createdAt: datetime author: Union[Author, None] = None +class Replies(BaseModel): + nodes: List[CommentsNode] + + +class DiscussionsCommentsNode(CommentsNode): + replies: Replies + + class Comments(BaseModel): nodes: List[CommentsNode] +class DiscussionsComments(BaseModel): + nodes: List[DiscussionsCommentsNode] + + class IssuesNode(BaseModel): number: int author: Union[Author, None] = None @@ -149,27 +209,59 @@ class IssuesNode(BaseModel): comments: Comments +class DiscussionsNode(BaseModel): + number: int + author: Union[Author, None] = None + title: str + createdAt: datetime + comments: DiscussionsComments + + class IssuesEdge(BaseModel): cursor: str node: IssuesNode +class DiscussionsEdge(BaseModel): + cursor: str + node: DiscussionsNode + + class Issues(BaseModel): edges: List[IssuesEdge] +class Discussions(BaseModel): + edges: List[DiscussionsEdge] + + class IssuesRepository(BaseModel): issues: Issues +class DiscussionsRepository(BaseModel): + discussions: Discussions + + class IssuesResponseData(BaseModel): repository: IssuesRepository +class DiscussionsResponseData(BaseModel): + repository: DiscussionsRepository + + class IssuesResponse(BaseModel): data: IssuesResponseData +class DiscussionsResponse(BaseModel): + data: DiscussionsResponseData + + +# PRs + + class LabelNode(BaseModel): name: str @@ -219,6 +311,9 @@ class PRsResponse(BaseModel): data: PRsResponseData +# Sponsors + + class SponsorEntity(BaseModel): login: str avatarUrl: str @@ -258,16 +353,21 @@ class SponsorsResponse(BaseModel): class Settings(BaseSettings): input_token: SecretStr - input_standard_token: SecretStr github_repository: str httpx_timeout: int = 30 def get_graphql_response( - *, settings: Settings, query: str, after: Union[str, None] = None -): + *, + settings: Settings, + query: str, + after: Union[str, None] = None, + category_id: Union[str, None] = None, +) -> Dict[str, Any]: headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} - variables = {"after": after} + # category_id is only used by one query, but GraphQL allows unused variables, so + # keep it here for simplicity + variables = {"after": after, "category_id": category_id} response = httpx.post( github_graphql_url, headers=headers, @@ -275,32 +375,54 @@ def get_graphql_response( json={"query": query, "variables": variables, "operationName": "Q"}, ) if response.status_code != 200: - logging.error(f"Response was not 200, after: {after}") + logging.error( + f"Response was not 200, after: {after}, category_id: {category_id}" + ) logging.error(response.text) raise RuntimeError(response.text) data = response.json() + if "errors" in data: + logging.error(f"Errors in response, after: {after}, category_id: {category_id}") + logging.error(data["errors"]) + logging.error(response.text) + raise RuntimeError(response.text) return data def get_graphql_issue_edges(*, settings: Settings, after: Union[str, None] = None): data = get_graphql_response(settings=settings, query=issues_query, after=after) - graphql_response = IssuesResponse.parse_obj(data) + graphql_response = IssuesResponse.model_validate(data) return graphql_response.data.repository.issues.edges +def get_graphql_question_discussion_edges( + *, + settings: Settings, + after: Union[str, None] = None, +): + data = get_graphql_response( + settings=settings, + query=discussions_query, + after=after, + category_id=questions_category_id, + ) + graphql_response = DiscussionsResponse.model_validate(data) + return graphql_response.data.repository.discussions.edges + + def get_graphql_pr_edges(*, settings: Settings, after: Union[str, None] = None): data = get_graphql_response(settings=settings, query=prs_query, after=after) - graphql_response = PRsResponse.parse_obj(data) + graphql_response = PRsResponse.model_validate(data) return graphql_response.data.repository.pullRequests.edges def get_graphql_sponsor_edges(*, settings: Settings, after: Union[str, None] = None): data = get_graphql_response(settings=settings, query=sponsors_query, after=after) - graphql_response = SponsorsResponse.parse_obj(data) + graphql_response = SponsorsResponse.model_validate(data) return graphql_response.data.user.sponsorshipsAsMaintainer.edges -def get_experts(settings: Settings): +def get_issues_experts(settings: Settings): issue_nodes: List[IssuesNode] = [] issue_edges = get_graphql_issue_edges(settings=settings) @@ -326,13 +448,78 @@ def get_experts(settings: Settings): for comment in issue.comments.nodes: if comment.author: authors[comment.author.login] = comment.author - if comment.author.login == issue_author_name: - continue - issue_commentors.add(comment.author.login) + if comment.author.login != issue_author_name: + issue_commentors.add(comment.author.login) for author_name in issue_commentors: commentors[author_name] += 1 if issue.createdAt > one_month_ago: last_month_commentors[author_name] += 1 + + return commentors, last_month_commentors, authors + + +def get_discussions_experts(settings: Settings): + discussion_nodes: List[DiscussionsNode] = [] + discussion_edges = get_graphql_question_discussion_edges(settings=settings) + + while discussion_edges: + for discussion_edge in discussion_edges: + discussion_nodes.append(discussion_edge.node) + last_edge = discussion_edges[-1] + discussion_edges = get_graphql_question_discussion_edges( + settings=settings, after=last_edge.cursor + ) + + commentors = Counter() + last_month_commentors = Counter() + authors: Dict[str, Author] = {} + + now = datetime.now(tz=timezone.utc) + one_month_ago = now - timedelta(days=30) + + for discussion in discussion_nodes: + discussion_author_name = None + if discussion.author: + authors[discussion.author.login] = discussion.author + discussion_author_name = discussion.author.login + discussion_commentors = set() + for comment in discussion.comments.nodes: + if comment.author: + authors[comment.author.login] = comment.author + if comment.author.login != discussion_author_name: + discussion_commentors.add(comment.author.login) + for reply in comment.replies.nodes: + if reply.author: + authors[reply.author.login] = reply.author + if reply.author.login != discussion_author_name: + discussion_commentors.add(reply.author.login) + for author_name in discussion_commentors: + commentors[author_name] += 1 + if discussion.createdAt > one_month_ago: + last_month_commentors[author_name] += 1 + return commentors, last_month_commentors, authors + + +def get_experts(settings: Settings): + # Migrated to only use GitHub Discussions + # ( + # issues_commentors, + # issues_last_month_commentors, + # issues_authors, + # ) = get_issues_experts(settings=settings) + ( + discussions_commentors, + discussions_last_month_commentors, + discussions_authors, + ) = get_discussions_experts(settings=settings) + # commentors = issues_commentors + discussions_commentors + commentors = discussions_commentors + # last_month_commentors = ( + # issues_last_month_commentors + discussions_last_month_commentors + # ) + last_month_commentors = discussions_last_month_commentors + # authors = {**issues_authors, **discussions_authors} + authors = {**discussions_authors} return commentors, last_month_commentors, authors @@ -422,16 +609,16 @@ def get_top_users( if __name__ == "__main__": logging.basicConfig(level=logging.INFO) settings = Settings() - logging.info(f"Using config: {settings.json()}") - g = Github(settings.input_standard_token.get_secret_value()) + logging.info(f"Using config: {settings.model_dump_json()}") + g = Github(settings.input_token.get_secret_value()) repo = g.get_repo(settings.github_repository) - issue_commentors, issue_last_month_commentors, issue_authors = get_experts( + question_commentors, question_last_month_commentors, question_authors = get_experts( settings=settings ) contributors, pr_commentors, reviewers, pr_authors = get_contributors( settings=settings ) - authors = {**issue_authors, **pr_authors} + authors = {**question_authors, **pr_authors} maintainers_logins = {"tiangolo"} bot_names = {"codecov", "github-actions", "pre-commit-ci", "dependabot"} maintainers = [] @@ -440,7 +627,7 @@ if __name__ == "__main__": maintainers.append( { "login": login, - "answers": issue_commentors[login], + "answers": question_commentors[login], "prs": contributors[login], "avatarUrl": user.avatarUrl, "url": user.url, @@ -453,13 +640,13 @@ if __name__ == "__main__": min_count_reviewer = 4 skip_users = maintainers_logins | bot_names experts = get_top_users( - counter=issue_commentors, + counter=question_commentors, min_count=min_count_expert, authors=authors, skip_users=skip_users, ) last_month_active = get_top_users( - counter=issue_last_month_commentors, + counter=question_last_month_commentors, min_count=min_count_last_month, authors=authors, skip_users=skip_users, diff --git a/.github/actions/watch-previews/Dockerfile b/.github/actions/watch-previews/Dockerfile deleted file mode 100644 index b8cc64d948..0000000000 --- a/.github/actions/watch-previews/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.7 - -RUN pip install httpx PyGithub "pydantic==1.5.1" - -COPY ./app /app - -CMD ["python", "/app/main.py"] diff --git a/.github/actions/watch-previews/action.yml b/.github/actions/watch-previews/action.yml deleted file mode 100644 index 5c09ad4876..0000000000 --- a/.github/actions/watch-previews/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Watch docs previews in PRs" -description: "Check PRs and trigger new docs deploys" -author: "Sebastián Ramírez " -inputs: - token: - description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}' - required: true -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/actions/watch-previews/app/main.py b/.github/actions/watch-previews/app/main.py deleted file mode 100644 index 51285d02b8..0000000000 --- a/.github/actions/watch-previews/app/main.py +++ /dev/null @@ -1,101 +0,0 @@ -import logging -from datetime import datetime -from pathlib import Path -from typing import List, Union - -import httpx -from github import Github -from github.NamedUser import NamedUser -from pydantic import BaseModel, BaseSettings, SecretStr - -github_api = "https://api.github.com" -netlify_api = "https://api.netlify.com" - - -class Settings(BaseSettings): - input_token: SecretStr - github_repository: str - github_event_path: Path - github_event_name: Union[str, None] = None - - -class Artifact(BaseModel): - id: int - node_id: str - name: str - size_in_bytes: int - url: str - archive_download_url: str - expired: bool - created_at: datetime - updated_at: datetime - - -class ArtifactResponse(BaseModel): - total_count: int - artifacts: List[Artifact] - - -def get_message(commit: str) -> str: - return f"Docs preview for commit {commit} at" - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - settings = Settings() - logging.info(f"Using config: {settings.json()}") - g = Github(settings.input_token.get_secret_value()) - repo = g.get_repo(settings.github_repository) - owner: NamedUser = repo.owner - headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} - prs = list(repo.get_pulls(state="open")) - response = httpx.get( - f"{github_api}/repos/{settings.github_repository}/actions/artifacts", - headers=headers, - ) - data = response.json() - artifacts_response = ArtifactResponse.parse_obj(data) - for pr in prs: - logging.info("-----") - logging.info(f"Processing PR #{pr.number}: {pr.title}") - pr_comments = list(pr.get_issue_comments()) - pr_commits = list(pr.get_commits()) - last_commit = pr_commits[0] - for pr_commit in pr_commits: - if pr_commit.commit.author.date > last_commit.commit.author.date: - last_commit = pr_commit - commit = last_commit.commit.sha - logging.info(f"Last commit: {commit}") - message = get_message(commit) - notified = False - for pr_comment in pr_comments: - if message in pr_comment.body: - notified = True - logging.info(f"Docs preview was notified: {notified}") - if not notified: - artifact_name = f"docs-zip-{commit}" - use_artifact: Union[Artifact, None] = None - for artifact in artifacts_response.artifacts: - if artifact.name == artifact_name: - use_artifact = artifact - break - if not use_artifact: - logging.info("Artifact not available") - else: - logging.info(f"Existing artifact: {use_artifact.name}") - response = httpx.post( - "https://api.github.com/repos/tiangolo/fastapi/actions/workflows/preview-docs.yml/dispatches", - headers=headers, - json={ - "ref": "master", - "inputs": { - "pr": f"{pr.number}", - "name": artifact_name, - "commit": commit, - }, - }, - ) - logging.info( - f"Trigger sent, response status: {response.status_code} - content: {response.content}" - ) - logging.info("Finished") diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cd972a0ba4..0a59adbd6b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,6 +11,10 @@ updates: - package-ecosystem: "pip" directory: "/" schedule: - interval: "daily" + interval: "monthly" + groups: + python-packages: + patterns: + - "*" commit-message: prefix: ⬆ diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index b9bd521b3f..abf2b90f68 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -4,46 +4,121 @@ on: branches: - master pull_request: - types: [opened, synchronize] + types: + - opened + - synchronize jobs: + changes: + runs-on: ubuntu-latest + # Required permissions + permissions: + pull-requests: read + # Set job outputs to values from filter step + outputs: + docs: ${{ steps.filter.outputs.docs }} + steps: + - uses: actions/checkout@v4 + # For pull requests it's not necessary to checkout the code but for master it is + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + docs: + - README.md + - docs/** + - docs_src/** + - requirements-docs.txt + - .github/workflows/build-docs.yml + - .github/workflows/deploy-docs.yml + langs: + needs: + - changes + runs-on: ubuntu-latest + outputs: + langs: ${{ steps.show-langs.outputs.langs }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - uses: actions/cache@v3 + id: cache + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt', 'requirements-docs-tests.txt') }}-v06 + - name: Install docs extras + if: steps.cache.outputs.cache-hit != 'true' + run: pip install -r requirements-docs.txt + # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps + - name: Install Material for MkDocs Insiders + if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) && steps.cache.outputs.cache-hit != 'true' + run: | + pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git + pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/griffe-typing-deprecated.git + pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/mkdocstrings-python.git + - name: Verify README + run: python ./scripts/docs.py verify-readme + - name: Export Language Codes + id: show-langs + run: | + echo "langs=$(python ./scripts/docs.py langs-json)" >> $GITHUB_OUTPUT + build-docs: - runs-on: ubuntu-18.04 + needs: + - changes + - langs + if: ${{ needs.changes.outputs.docs == 'true' }} + runs-on: ubuntu-latest + strategy: + matrix: + lang: ${{ fromJson(needs.langs.outputs.langs) }} steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.7" + python-version: "3.11" - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v03 + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt', 'requirements-docs-tests.txt') }}-v06 - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' - run: pip install .[doc] + run: pip install -r requirements-docs.txt - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' - run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git + if: ( github.event_name != 'pull_request' || github.secret_source != 'Actions' ) && steps.cache.outputs.cache-hit != 'true' + run: | + pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git + pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/griffe-typing-deprecated.git + pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/mkdocstrings-python.git + - name: Update Languages + run: python ./scripts/docs.py update-languages + - uses: actions/cache@v3 + with: + key: mkdocs-cards-${{ matrix.lang }}-${{ github.ref }} + path: docs/${{ matrix.lang }}/.cache - name: Build Docs - run: python ./scripts/docs.py build-all - - name: Zip docs - run: bash ./scripts/zip-docs.sh + run: python ./scripts/docs.py build-lang ${{ matrix.lang }} - uses: actions/upload-artifact@v3 with: - name: docs-zip - path: ./site/docs.zip - - name: Deploy to Netlify - uses: nwtgck/actions-netlify@v2.0.0 + name: docs-site + path: ./site/** + + # https://github.com/marketplace/actions/alls-green#why + docs-all-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - build-docs + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 with: - publish-dir: './site' - production-branch: master - github-token: ${{ secrets.GITHUB_TOKEN }} - enable-commit-comment: false - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + jobs: ${{ toJSON(needs) }} + allowed-skips: build-docs diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000000..2bec6682c1 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,48 @@ +name: Deploy Docs +on: + workflow_run: + workflows: + - Build Docs + types: + - completed + +jobs: + deploy-docs: + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v4 + - name: Clean site + run: | + rm -rf ./site + mkdir ./site + - name: Download Artifact Docs + id: download + uses: dawidd6/action-download-artifact@v3.0.0 + with: + if_no_artifact_found: ignore + github_token: ${{ secrets.FASTAPI_PREVIEW_DOCS_DOWNLOAD_ARTIFACTS }} + workflow: build-docs.yml + run_id: ${{ github.event.workflow_run.id }} + name: docs-site + path: ./site/ + - name: Deploy to Cloudflare Pages + if: steps.download.outputs.found_artifact == 'true' + id: deploy + uses: cloudflare/pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: fastapitiangolo + directory: './site' + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'master' && 'main' ) || ( github.event.workflow_run.head_sha ) }} + - name: Comment Deploy + if: steps.deploy.outputs.url != '' + uses: ./.github/actions/comment-docs-preview-in-pr + with: + token: ${{ secrets.FASTAPI_PREVIEW_DOCS_COMMENT_DEPLOY }} + deploy_url: "${{ steps.deploy.outputs.url }}" diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index e2fb4f7a43..d1aad28fd3 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -2,7 +2,7 @@ name: Issue Manager on: schedule: - - cron: "0 0 * * *" + - cron: "10 3 * * *" issue_comment: types: - created @@ -16,15 +16,24 @@ on: jobs: issue-manager: + if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - uses: tiangolo/issue-manager@0.4.0 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.FASTAPI_ISSUE_MANAGER }} config: > { "answered": { "delay": 864000, "message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs." + }, + "changes-requested": { + "delay": 2628000, + "message": "As this PR had requested changes to be applied but has been inactive for a while, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." } } diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index b2646dd16d..62daf26080 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -3,11 +3,17 @@ name: Label Approved on: schedule: - cron: "0 12 * * *" + workflow_dispatch: jobs: label-approved: + if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: - - uses: docker://tiangolo/label-approved:0.0.2 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: docker://tiangolo/label-approved:0.0.4 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.FASTAPI_LABEL_APPROVED }} diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 4aa8475b62..27e062d090 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -14,27 +14,32 @@ on: debug_enabled: description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false - default: false + default: 'false' jobs: latest-changes: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v4 with: - # To allow latest-changes to commit to master - token: ${{ secrets.ACTIONS_TOKEN }} + # To allow latest-changes to commit to the main branch + token: ${{ secrets.FASTAPI_LATEST_CHANGES }} # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - token: ${{ secrets.ACTIONS_TOKEN }} - standard_token: ${{ secrets.GITHUB_TOKEN }} - - uses: docker://tiangolo/latest-changes:0.0.3 + - uses: docker://tiangolo/latest-changes:0.3.0 + # - uses: tiangolo/latest-changes@main with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/en/docs/release-notes.md - latest_changes_header: '## Latest Changes\n\n' + latest_changes_header: '## Latest Changes' + end_regex: '^## ' debug_logs: true + label_header_prefix: '### ' diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 2fcb7595e6..c0904ce486 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -4,18 +4,32 @@ on: pull_request_target: types: - labeled + - closed + workflow_dispatch: + inputs: + number: + description: PR number + required: true + debug_enabled: + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + required: false + default: 'false' jobs: notify-translations: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v4 # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - uses: ./.github/actions/notify-translations with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.FASTAPI_NOTIFY_TRANSLATIONS }} diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index 4b47b4072f..b0868771dc 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -8,22 +8,27 @@ on: debug_enabled: description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false - default: false + default: 'false' jobs: fastapi-people: + if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v4 + # Ref: https://github.com/actions/runner/issues/2033 + - name: Fix git safe.directory in container + run: mkdir -p /home/runner/work/_temp/_github_home && printf "[safe]\n\tdirectory = /github/workspace" > /home/runner/work/_temp/_github_home/.gitconfig # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - token: ${{ secrets.ACTIONS_TOKEN }} - standard_token: ${{ secrets.GITHUB_TOKEN }} - uses: ./.github/actions/people with: - token: ${{ secrets.ACTIONS_TOKEN }} - standard_token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.FASTAPI_PEOPLE }} diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml deleted file mode 100644 index 2af56e2bca..0000000000 --- a/.github/workflows/preview-docs.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Preview Docs -on: - workflow_run: - workflows: - - Build Docs - types: - - completed - -jobs: - preview-docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Clean site - run: | - rm -rf ./site - mkdir ./site - - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.24.3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - workflow: build-docs.yml - run_id: ${{ github.event.workflow_run.id }} - name: docs-zip - path: ./site/ - - name: Unzip docs - run: | - cd ./site - unzip docs.zip - rm -f docs.zip - - name: Deploy to Netlify - id: netlify - uses: nwtgck/actions-netlify@v2.0.0 - with: - publish-dir: './site' - production-deploy: false - github-token: ${{ secrets.GITHUB_TOKEN }} - enable-commit-comment: false - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - - name: Comment Deploy - uses: ./.github/actions/comment-docs-preview-in-pr - with: - token: ${{ secrets.GITHUB_TOKEN }} - deploy_url: "${{ steps.netlify.outputs.deploy-url }}" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c2fdb8e17f..8ebb28a80f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,12 +13,13 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.7" - cache: "pip" + python-version: "3.10" + # Issue ref: https://github.com/actions/setup-python/issues/436 + # cache: "pip" cache-dependency-path: pyproject.toml - uses: actions/cache@v3 id: cache @@ -31,16 +32,10 @@ jobs: - name: Build distribution run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.6.4 + uses: pypa/gh-action-pypi-publish@v1.8.11 with: password: ${{ secrets.PYPI_API_TOKEN }} - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - # - name: Notify - # env: - # GITTER_TOKEN: ${{ secrets.GITTER_TOKEN }} - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # TAG: ${{ github.event.release.name }} - # run: bash scripts/notify.sh diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index d6206d697b..10bff67aee 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -14,14 +14,19 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v4 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/setup-python@v5 with: python-version: '3.9' - run: pip install smokeshow - - uses: dawidd6/action-download-artifact@v2.24.3 + - uses: dawidd6/action-download-artifact@v3.0.0 with: + github_token: ${{ secrets.FASTAPI_SMOKESHOW_DOWNLOAD_ARTIFACTS }} workflow: test.yml commit: ${{ github.event.workflow_run.head_sha }} @@ -30,6 +35,6 @@ jobs: SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 100 SMOKESHOW_GITHUB_CONTEXT: coverage - SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SMOKESHOW_GITHUB_TOKEN: ${{ secrets.FASTAPI_SMOKESHOW_UPLOAD }} SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1235516d33..b6b1736851 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,34 +5,78 @@ on: branches: - master pull_request: - types: [opened, synchronize] + types: + - opened + - synchronize jobs: - test: + lint: runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] - fail-fast: false - steps: - - uses: actions/checkout@v3 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} - cache: "pip" - cache-dependency-path: pyproject.toml + python-version: "3.11" + # Issue ref: https://github.com/actions/setup-python/issues/436 + # cache: "pip" + # cache-dependency-path: pyproject.toml - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test-v03 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt', 'requirements-docs-tests.txt') }}-test-v07 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' - run: pip install -e .[all,dev,doc,test] + run: pip install -r requirements-tests.txt + - name: Install Pydantic v2 + run: pip install "pydantic>=2.0.2,<3.0.0" - name: Lint run: bash scripts/lint.sh + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - "3.12" + - "3.11" + - "3.10" + - "3.9" + - "3.8" + pydantic-version: ["pydantic-v1", "pydantic-v2"] + fail-fast: false + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + # Issue ref: https://github.com/actions/setup-python/issues/436 + # cache: "pip" + # cache-dependency-path: pyproject.toml + - uses: actions/cache@v3 + id: cache + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt', 'requirements-docs-tests.txt') }}-test-v07 + - name: Install Dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: pip install -r requirements-tests.txt + - name: Install Pydantic v1 + if: matrix.pydantic-version == 'pydantic-v1' + run: pip install "pydantic>=1.10.0,<2.0.0" + - name: Install Pydantic v2 + if: matrix.pydantic-version == 'pydantic-v2' + run: pip install "pydantic>=2.0.2,<3.0.0" - run: mkdir coverage - name: Test run: bash scripts/test.sh @@ -44,32 +88,32 @@ jobs: with: name: coverage path: coverage + coverage-combine: needs: [test] runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-python@v4 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.8' - cache: "pip" - cache-dependency-path: pyproject.toml - + # Issue ref: https://github.com/actions/setup-python/issues/436 + # cache: "pip" + # cache-dependency-path: pyproject.toml - name: Get coverage files uses: actions/download-artifact@v3 with: name: coverage path: coverage - - run: pip install coverage[toml] - - run: ls -la coverage - run: coverage combine coverage - run: coverage report - run: coverage html --show-contexts --title "Coverage for ${{ github.sha }}" - - name: Store coverage HTML uses: actions/upload-artifact@v3 with: @@ -78,15 +122,15 @@ jobs: # https://github.com/marketplace/actions/alls-green#why check: # This job does nothing and is only used for the branch protection - if: always() - needs: - coverage-combine - runs-on: ubuntu-latest - steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: diff --git a/.gitignore b/.gitignore index a26bb5cd64..9be494cec0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ Pipfile.lock env3.* env docs_build +site_build venv docs.zip archive.zip @@ -23,3 +24,7 @@ archive.zip # vim temporary files *~ .*.sw? +.cache + +# macOS +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 96f097caa1..a7f2fb3f22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,10 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks +default_language_version: + python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-added-large-files - id: check-toml @@ -11,34 +13,13 @@ repos: - --unsafe - id: end-of-file-fixer - id: trailing-whitespace -- repo: https://github.com/asottile/pyupgrade - rev: v3.2.2 - hooks: - - id: pyupgrade - args: - - --py3-plus - - --keep-runtime-typing - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.138 + rev: v0.1.2 hooks: - id: ruff args: - --fix -- repo: https://github.com/pycqa/isort - rev: 5.10.1 - hooks: - - id: isort - name: isort (python) - - id: isort - name: isort (cython) - types: [cython] - - id: isort - name: isort (pyi) - types: [pyi] -- repo: https://github.com/psf/black - rev: 22.10.0 - hooks: - - id: black + - id: ruff-format ci: autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..9028248b1d --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,24 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: FastAPI +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Sebastián + family-names: Ramírez + email: tiangolo@gmail.com +identifiers: +repository-code: 'https://github.com/tiangolo/fastapi' +url: 'https://fastapi.tiangolo.com' +abstract: >- + FastAPI framework, high performance, easy to learn, fast to code, + ready for production +keywords: + - fastapi + - pydantic + - starlette +license: MIT diff --git a/README.md b/README.md index 2b4de2a651..2df5cba0bd 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ --- -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.8+ based on standard Python type hints. The key features are: @@ -46,17 +46,22 @@ The key features are: - - + + + + + + - - - + + + + @@ -102,6 +107,12 @@ The key features are: --- +"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + ## **Typer**, the FastAPI of CLIs @@ -112,7 +123,7 @@ If you are building a CLI app to be ## Requirements -Python 3.7+ +Python 3.8+ FastAPI stands on the shoulders of giants: @@ -328,7 +339,7 @@ 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.7+**. +Just standard **Python 3.8+**. For example, for an `int`: @@ -442,8 +453,9 @@ To understand more about it, see the section ujson - for faster JSON "parsing". * email_validator - for email validation. +* pydantic-settings - for settings management. +* pydantic-extra-types - for extra types to be used with Pydantic. Used by Starlette: diff --git a/docs/az/docs/index.md b/docs/az/docs/index.md deleted file mode 100644 index 282c150322..0000000000 --- a/docs/az/docs/index.md +++ /dev/null @@ -1,466 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**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. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## 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.7+ - -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[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Optional - -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: Optional[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="9 14" -from typing import Optional - -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: Optional[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="4 9-12 25-27" -from typing import Optional - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Optional[bool] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[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: - -* httpx - Required if you want to use the `TestClient`. -* 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/az/mkdocs.yml b/docs/az/mkdocs.yml deleted file mode 100644 index d549f37a39..0000000000 --- a/docs/az/mkdocs.yml +++ /dev/null @@ -1,145 +0,0 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/az/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/bn/docs/index.md b/docs/bn/docs/index.md new file mode 100644 index 0000000000..4f778e8735 --- /dev/null +++ b/docs/bn/docs/index.md @@ -0,0 +1,464 @@ +

+ FastAPI +

+

+ FastAPI উচ্চক্ষমতা সম্পন্ন, সহজে শেখার এবং দ্রুত কোড করে প্রোডাকশনের জন্য ফ্রামওয়ার্ক। +

+

+ + Test + + + Coverage + + + Package version + +

+ +--- + +**নির্দেশিকা নথি**: https://fastapi.tiangolo.com + +**সোর্স কোড**: https://github.com/tiangolo/fastapi + +--- + +FastAPI একটি আধুনিক, দ্রুত ( বেশি ক্ষমতা ) সম্পন্ন, Python 3.6+ দিয়ে API তৈরির জন্য স্ট্যান্ডার্ড পাইথন টাইপ ইঙ্গিত ভিত্তিক ওয়েব ফ্রেমওয়ার্ক। + +এর মূল বৈশিষ্ট্য গুলো হলঃ + +- **গতি**: এটি **NodeJS** এবং **Go** এর মত কার্যক্ষমতা সম্পন্ন (Starlette এবং Pydantic এর সাহায্যে)। [পাইথন এর দ্রুততম ফ্রেমওয়ার্ক গুলোর মধ্যে এটি একটি](#_11)। +- **দ্রুত কোড করা**:বৈশিষ্ট্য তৈরির গতি ২০০% থেকে ৩০০% বৃদ্ধি করে৷ \* +- **স্বল্প bugs**: মানুব (ডেভেলপার) সৃষ্ট ত্রুটির প্রায় ৪০% হ্রাস করে। \* +- **স্বজ্ঞাত**: দুর্দান্ত এডিটর সাহায্য Completion নামেও পরিচিত। দ্রুত ডিবাগ করা যায়। + +- **সহজ**: এটি এমন ভাবে সজানো হয়েছে যেন নির্দেশিকা নথি পড়ে সহজে শেখা এবং ব্যবহার করা যায়। +- **সংক্ষিপ্ত**: কোড পুনরাবৃত্তি কমানোর পাশাপাশি, bug কমায় এবং প্রতিটি প্যারামিটার ঘোষণা থেকে একাধিক ফিচার পাওয়া যায় । +- **জোরালো**: স্বয়ংক্রিয় ভাবে তৈরি ক্রিয়াশীল নির্দেশনা নথি (documentation) সহ উৎপাদন উপযোগি (Production-ready) কোড পাওয়া যায়। +- **মান-ভিত্তিক**: এর ভিত্তি OpenAPI (যা পুর্বে Swagger নামে পরিচিত ছিল) এবং JSON Schema এর আদর্শের মানের ওপর + +\* উৎপাদনমুখি এপ্লিকেশন বানানোর এক দল ডেভেলপার এর মতামত ভিত্তিক ফলাফল। + +## স্পনসর গণ + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +অন্যান্য স্পনসর গণ + +## মতামত সমূহ + +"_আমি আজকাল **FastAPI** ব্যবহার করছি। [...] আমরা ভাবছি মাইক্রোসফ্টে **ML সার্ভিস** এ সকল দলের জন্য এটি ব্যবহার করব। যার মধ্যে কিছু পণ্য **Windows** এ সংযোযন হয় এবং কিছু **Office** এর সাথে সংযোযন হচ্ছে।_" + +
কবির খান - মাইক্রোসফ্টে (ref)
+ +--- + +"_আমরা **FastAPI** লাইব্রেরি গ্রহণ করেছি একটি **REST** সার্ভার তৈরি করতে, যা **ভবিষ্যদ্বাণী** পাওয়ার জন্য কুয়েরি করা যেতে পারে। [লুডউইগের জন্য]_" + +
পিয়েরো মোলিনো, ইয়ারোস্লাভ দুদিন, এবং সাই সুমন্থ মিরিয়ালা - উবার (ref)
+ +--- + +"_**Netflix** আমাদের **ক্রাইসিস ম্যানেজমেন্ট** অর্কেস্ট্রেশন ফ্রেমওয়ার্ক: **ডিসপ্যাচ** এর ওপেন সোর্স রিলিজ ঘোষণা করতে পেরে আনন্দিত! [যাকিনা **FastAPI** দিয়ে নির্মিত]_" + +
কেভিন গ্লিসন, মার্ক ভিলানোভা, ফরেস্ট মনসেন - নেটফ্লিক্স (ref)
+ +--- + +"_আমি **FastAPI** নিয়ে চাঁদের সমান উৎসাহিত। এটি খুবই মজার!_" + +
ব্রায়ান ওকেন - পাইথন বাইটস পডকাস্ট হোস্ট (ref)
+ +--- + +"\_সত্যিই, আপনি যা তৈরি করেছেন তা খুব মজবুত এবং পরিপূর্ন৷ অনেক উপায়ে, আমি যা **Hug** এ করতে চেয়েছিলাম - তা কাউকে তৈরি করতে দেখে আমি সত্যিই অনুপ্রানিত৷\_" + +
টিমোথি ক্রসলে - Hug স্রষ্টা (ref)
+ +--- + +"আপনি যদি REST API তৈরির জন্য একটি **আধুনিক ফ্রেমওয়ার্ক** শিখতে চান, তাহলে **FastAPI** দেখুন [...] এটি দ্রুত, ব্যবহার করা সহজ এবং শিখতেও সহজ [...]\_" + +"_আমরা আমাদের **APIs** [...] এর জন্য **FastAPI**- তে এসেছি [...] আমি মনে করি আপনিও এটি পছন্দ করবেন [...]_" + +
ইনেস মন্টানি - ম্যাথিউ হোনিবাল - Explosion AI প্রতিষ্ঠাতা - spaCy স্রষ্টা (ref) - (ref)
+ +--- + +## **Typer**, CLI এর জন্য FastAPI + + + +আপনি যদি CLI অ্যাপ বানাতে চান, যা কিনা ওয়েব API এর পরিবর্তে টার্মিনালে ব্যবহার হবে, তাহলে দেখুন**Typer**. + +**টাইপার** হল FastAPI এর ছোট ভাইয়ের মত। এবং এটির উদ্দেশ্য ছিল **CLIs এর FastAPI** হওয়া। ⌨️ 🚀 + +## প্রয়োজনীয়তা গুলো + +Python 3.7+ + +FastAPI কিছু দানবেদের কাঁধে দাঁড়িয়ে আছে: + +- Starlette ওয়েব অংশের জন্য. +- Pydantic ডেটা অংশগুলির জন্য. + +## ইনস্টলেশন প্রক্রিয়া + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +আপনার একটি ASGI সার্ভারেরও প্রয়োজন হবে, প্রোডাকশনের জন্য Uvicorn অথবা Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## উদাহরণ + +### তৈরি + +- `main.py` নামে একটি ফাইল তৈরি করুন: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+অথবা ব্যবহার করুন async def... + +যদি আপনার কোড `async` / `await`, ব্যবহার করে তাহলে `async def` ব্যবহার করুন: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**টীকা**: + +আপনি যদি না জানেন, _"তাড়াহুড়ো?"_ বিভাগটি দেখুন `async` এবং `await` নথির মধ্যে দেখুন . + +
+ +### এটি চালান + +সার্ভার চালু করুন: + +
+ +```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. +``` + +
+ +
+নির্দেশনা সম্পর্কে uvicorn main:app --reload... + +`uvicorn main:app` নির্দেশনাটি দ্বারা বোঝায়: + +- `main`: ফাইল `main.py` (পাইথন "মডিউল")। +- `app`: `app = FastAPI()` লাইন দিয়ে `main.py` এর ভিতরে তৈরি করা অবজেক্ট। +- `--reload`: কোড পরিবর্তনের পরে সার্ভার পুনরায় চালু করুন। এটি শুধুমাত্র ডেভেলপমেন্ট এর সময় ব্যবহার করুন। + +
+ +### এটা চেক করুন + +আপনার ব্রাউজার খুলুন http://127.0.0.1:8000/items/5?q=somequery এ। + +আপনি JSON রেসপন্স দেখতে পাবেন: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +আপনি ইতিমধ্যে একটি API তৈরি করেছেন যা: + +- `/` এবং `/items/{item_id}` _paths_ এ HTTP অনুরোধ গ্রহণ করে। +- উভয় *path*ই `GET` অপারেশন নেয় ( যা HTTP _methods_ নামেও পরিচিত)। +- _path_ `/items/{item_id}`-এ একটি _path প্যারামিটার_ `item_id` আছে যা কিনা `int` হতে হবে। +- _path_ `/items/{item_id}`-এর একটি ঐচ্ছিক `str` _query প্যারামিটার_ `q` আছে। + +### ক্রিয়াশীল 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) + +## উদাহরণস্বরূপ আপগ্রেড + +এখন `main.py` ফাইলটি পরিবর্তন করুন যেন এটি `PUT` রিকুয়েস্ট থেকে বডি পেতে পারে। + +Python স্ট্যান্ডার্ড লাইব্রেরি, Pydantic এর সাহায্যে বডি ঘোষণা করুন। + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +সার্ভারটি স্বয়ংক্রিয়ভাবে পুনরায় লোড হওয়া উচিত (কারণ আপনি উপরের `uvicorn` কমান্ডে `--reload` যোগ করেছেন)। + +### ক্রিয়াশীল API নির্দেশিকা নথি উন্নীতকরণ + +এখন http://127.0.0.1:8000/docs এডড্রেসে যান. + +- ক্রিয়াশীল API নির্দেশিকা নথিটি স্বয়ংক্রিয়ভাবে উন্নীত হযে যাবে, নতুন বডি সহ: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +- "Try it out" বাটনে চাপুন, এটি আপনাকে পেরামিটারগুলো পূরণ করতে এবং API এর সাথে সরাসরি ক্রিয়া-কলাপ করতে দিবে: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +- তারপরে "Execute" বাটনে চাপুন, ব্যবহারকারীর ইন্টারফেস আপনার API এর সাথে যোগাযোগ করবে, পেরামিটার পাঠাবে, ফলাফলগুলি পাবে এবং সেগুলি পর্রদায় দেখাবে: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### বিকল্প API নির্দেশিকা নথি আপগ্রেড + +এবং এখন http://127.0.0.1:8000/redoc এ যান। + +- বিকল্প নির্দেশিকা নথিতেও নতুন কুয়েরি প্যারামিটার এবং বডি প্রতিফলিত হবে: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### সংক্ষিপ্তকরণ + +সংক্ষেপে, আপনি **শুধু একবার** প্যারামিটারের ধরন, বডি ইত্যাদি ফাংশন প্যারামিটার হিসেবে ঘোষণা করেন। + +আপনি সেটি আধুনিক পাইথনের সাথে করেন। + +আপনাকে নতুন করে নির্দিষ্ট কোন লাইব্রেরির বাক্য গঠন, ফাংশন বা ক্লাস কিছুই শিখতে হচ্ছে না। + +শুধুই আধুনিক **Python 3.6+** + +উদাহরণস্বরূপ, `int` এর জন্য: + +```Python +item_id: int +``` + +অথবা আরও জটিল `Item` মডেলের জন্য: + +```Python +item: Item +``` + +...এবং সেই একই ঘোষণার সাথে আপনি পাবেন: + +- এডিটর সাহায্য, যেমন + - সমাপ্তি। + - ধরণ যাচাই +- তথ্য যাচাইকরণ: + - ডেটা অবৈধ হলে স্বয়ংক্রিয় এবং পরিষ্কার ত্রুটির নির্দেশনা। + - এমনকি গভীরভাবে নেস্ট করা JSON অবজেক্টের জন্য বৈধতা। +- প্রেরিত তথ্য রূপান্তর: যা নেটওয়ার্ক থেকে পাইথনের তথ্য এবং ধরনে আসে, এবং সেখান থেকে পড়া: + + - JSON। + - পাথ প্যারামিটার। + - কুয়েরি প্যারামিটার। + - কুকিজ + - হেডার + - ফর্ম + - ফাইল + +- আউটপুট ডেটার রূপান্তর: পাইথন ডেটা এবং টাইপ থেকে নেটওয়ার্ক ডেটাতে রূপান্তর করা (JSON হিসাবে): + -পাইথন টাইপে রূপান্তর করুন (`str`, `int`, `float`, `bool`, `list`, ইত্যাদি)। + - `datetime` অবজেক্ট। + - `UUID` objeঅবজেক্টcts। + - ডাটাবেস মডেল। + - ...এবং আরো অনেক। +- স্বয়ংক্রিয় ক্রিয়াশীল API নির্দেশিকা নথি, 2টি বিকল্প ব্যবহারকারীর ইন্টারফেস সহ: + - সোয়াগার ইউ আই (Swagger UI)। + - রিডক (ReDoc)। + +--- + +পূর্ববর্তী কোড উদাহরণে ফিরে আসা যাক, **FastAPI** যা করবে: + +- `GET` এবং `PUT` অনুরোধের জন্য পথে `item_id` আছে কিনা তা যাচাই করবে। +- `GET` এবং `PUT` অনুরোধের জন্য `item_id` টাইপ `int` এর হতে হবে তা যাচাই করবে। + - যদি না হয় তবে ক্লায়েন্ট একটি উপযুক্ত, পরিষ্কার ত্রুটি দেখতে পাবেন। +- `GET` অনুরোধের জন্য একটি ঐচ্ছিক ক্যুয়েরি প্যারামিটার নামক `q` (যেমন `http://127.0.0.1:8000/items/foo?q=somequery`) আছে কি তা চেক করবে। + - যেহেতু `q` প্যারামিটারটি `= None` দিয়ে ঘোষণা করা হয়েছে, তাই এটি ঐচ্ছিক। + - `None` ছাড়া এটি প্রয়োজনীয় হতো (যেমন `PUT` এর ক্ষেত্রে হয়েছে)। +- `/items/{item_id}` এর জন্য `PUT` অনুরোধের বডি JSON হিসাবে পড়ুন: + - লক্ষ করুন, `name` একটি প্রয়োজনীয় অ্যাট্রিবিউট হিসাবে বিবেচনা করেছে এবং এটি `str` হতে হবে। + - লক্ষ করুন এখানে, `price` অ্যাট্রিবিউটটি আবশ্যক এবং এটি `float` হতে হবে। + - লক্ষ করুন `is_offer` একটি ঐচ্ছিক অ্যাট্রিবিউট এবং এটি `bool` হতে হবে যদি উপস্থিত থাকে। + - এই সবটি গভীরভাবে অবস্থানরত JSON অবজেক্টগুলিতেও কাজ করবে। +- স্বয়ংক্রিয়ভাবে JSON হতে এবং JSON থেকে কনভার্ট করুন। +- OpenAPI দিয়ে সবকিছু ডকুমেন্ট করুন, যা ব্যবহার করা যেতে পারে: + - ক্রিয়াশীল নির্দেশিকা নথি। + - অনেক ভাষার জন্য স্বয়ংক্রিয় ক্লায়েন্ট কোড তৈরির ব্যবস্থা। +- সরাসরি 2টি ক্রিয়াশীল নির্দেশিকা নথি ওয়েব পৃষ্ঠ প্রদান করা হয়েছে। + +--- + +আমরা এতক্ষন শুধু এর পৃষ্ঠ তৈরি করেছি, কিন্তু আপনি ইতমধ্যেই এটি কিভাবে কাজ করে তার ধারণাও পেয়ে গিয়েছেন। + +নিম্নোক্ত লাইন গুলো পরিবর্তন করার চেষ্টা করুন: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...পুর্বে: + +```Python + ... "item_name": item.name ... +``` + +...পরবর্তীতে: + +```Python + ... "item_price": item.price ... +``` + +...এবং দেখুন কিভাবে আপনার এডিটর উপাদানগুলোকে সয়ংক্রিয়ভাবে-সম্পন্ন করবে এবং তাদের ধরন জানতে পারবে: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +আরও বৈশিষ্ট্য সম্পন্ন উদাহরণের জন্য, দেখুন টিউটোরিয়াল - ব্যবহারকারীর গাইড. + +**স্পয়লার সতর্কতা**: টিউটোরিয়াল - ব্যবহারকারীর গাইড নিম্নোক্ত বিষয়গুলি অন্তর্ভুক্ত করে: + +- **হেডার**, **কুকিজ**, **ফর্ম ফিল্ড** এবং **ফাইলগুলি** এমন অন্যান্য জায়গা থেকে প্যারামিটার ঘোষণা করা। +- `maximum_length` বা `regex` এর মতো **যাচাইকরণ বাধামুক্তি** সেট করা হয় কিভাবে, তা নিয়ে আলোচনা করা হবে। +- একটি খুব শক্তিশালী এবং ব্যবহার করা সহজ ডিপেন্ডেন্সি ইনজেকশন পদ্ধতি +- **OAuth2** এবং **JWT টোকেন** এবং **HTTP Basic** auth সহ নিরাপত্তা এবং অনুমোদনপ্রাপ্তি সম্পর্কিত বিষয়সমূহের উপর। +- **গভীরভাবে অবস্থানরত JSON মডেল** ঘোষণা করার জন্য আরও উন্নত (কিন্তু সমান সহজ) কৌশল (Pydantic কে ধন্যবাদ)। +- আরো অতিরিক্ত বৈশিষ্ট্য (স্টারলেটকে ধন্যবাদ) হিসাবে: + - **WebSockets** + - **GraphQL** + - HTTPX এবং `pytest` ভিত্তিক অত্যন্ত সহজ পরীক্ষা + - **CORS** + - **Cookie Sessions** + - ...এবং আরো। + +## কর্মক্ষমতা + +স্বাধীন TechEmpower Benchmarks দেখায় যে **FastAPI** অ্যাপ্লিকেশনগুলি Uvicorn-এর অধীনে চলমান দ্রুততমপাইথন ফ্রেমওয়ার্কগুলির মধ্যে একটি, শুধুমাত্র Starlette এবং Uvicorn-এর পর (FastAPI দ্বারা অভ্যন্তরীণভাবে ব্যবহৃত)। (\*) + +এটি সম্পর্কে আরও বুঝতে, দেখুন Benchmarks. + +## ঐচ্ছিক নির্ভরশীলতা + +Pydantic দ্বারা ব্যবহৃত: + +- ujson - দ্রুত JSON এর জন্য "parsing". +- email_validator - ইমেল যাচাইকরণের জন্য। + +স্টারলেট দ্বারা ব্যবহৃত: + +- httpx - আপনি যদি `TestClient` ব্যবহার করতে চান তাহলে আবশ্যক। +- jinja2 - আপনি যদি প্রদত্ত টেমপ্লেট রূপরেখা ব্যবহার করতে চান তাহলে প্রয়োজন। +- python-multipart - আপনি যদি ফর্ম সহায়তা করতে চান তাহলে প্রয়োজন "parsing", `request.form()` সহ। +- itsdangerous - `SessionMiddleware` সহায়তার জন্য প্রয়োজন। +- pyyaml - স্টারলেটের SchemaGenerator সাপোর্ট এর জন্য প্রয়োজন (আপনার সম্ভাবত FastAPI প্রয়োজন নেই)। +- graphene - `GraphQLApp` সহায়তার জন্য প্রয়োজন। +- ujson - আপনি `UJSONResponse` ব্যবহার করতে চাইলে প্রয়োজন। + +FastAPI / Starlette দ্বারা ব্যবহৃত: + +- uvicorn - সার্ভারের জন্য যা আপনার অ্যাপ্লিকেশন লোড করে এবং পরিবেশন করে। +- orjson - আপনি `ORJSONResponse` ব্যবহার করতে চাইলে প্রয়োজন। + +আপনি এই সব ইনস্টল করতে পারেন `pip install fastapi[all]` দিয়ে. + +## লাইসেন্স + +এই প্রজেক্ট MIT লাইসেন্স নীতিমালার অধীনে শর্তায়িত। diff --git a/docs/bn/mkdocs.yml b/docs/bn/mkdocs.yml new file mode 100644 index 0000000000..de18856f44 --- /dev/null +++ b/docs/bn/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml diff --git a/docs/de/docs/features.md b/docs/de/docs/features.md index f281afd1ed..64fa8092d3 100644 --- a/docs/de/docs/features.md +++ b/docs/de/docs/features.md @@ -25,7 +25,7 @@ Mit einer interaktiven API-Dokumentation und explorativen webbasierten Benutzers ### Nur modernes Python -Alles basiert auf **Python 3.6 Typ**-Deklarationen (dank Pydantic). Es muss keine neue Syntax gelernt werden, nur standardisiertes modernes Python. +Alles basiert auf **Python 3.8 Typ**-Deklarationen (dank Pydantic). Es muss keine neue Syntax gelernt werden, nur standardisiertes modernes Python. diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md deleted file mode 100644 index 68fc8b753a..0000000000 --- a/docs/de/docs/index.md +++ /dev/null @@ -1,464 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**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. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## 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.7+ - -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[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**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="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -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** - * 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: - -* httpx - Required if you want to use the `TestClient`. -* 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). -* 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/de/docs/tutorial/background-tasks.md b/docs/de/docs/tutorial/background-tasks.md new file mode 100644 index 0000000000..a7bfd55a7d --- /dev/null +++ b/docs/de/docs/tutorial/background-tasks.md @@ -0,0 +1,126 @@ +# Hintergrundtasks + +Sie können Hintergrundtasks (Hintergrund-Aufgaben) definieren, die *nach* der Rückgabe einer Response ausgeführt werden sollen. + +Das ist nützlich für Vorgänge, die nach einem Request ausgeführt werden müssen, bei denen der Client jedoch nicht unbedingt auf den Abschluss des Vorgangs warten muss, bevor er die Response erhält. + +Hierzu zählen beispielsweise: + +* E-Mail-Benachrichtigungen, die nach dem Ausführen einer Aktion gesendet werden: + * Da die Verbindung zu einem E-Mail-Server und das Senden einer E-Mail in der Regel „langsam“ ist (einige Sekunden), können Sie die Response sofort zurücksenden und die E-Mail-Benachrichtigung im Hintergrund senden. +* Daten verarbeiten: + * Angenommen, Sie erhalten eine Datei, die einen langsamen Prozess durchlaufen muss. Sie können als Response „Accepted“ (HTTP 202) zurückgeben und die Datei im Hintergrund verarbeiten. + +## `BackgroundTasks` verwenden + +Importieren Sie zunächst `BackgroundTasks` und definieren Sie einen Parameter in Ihrer *Pfadoperation-Funktion* mit der Typdeklaration `BackgroundTasks`: + +```Python hl_lines="1 13" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +**FastAPI** erstellt für Sie das Objekt vom Typ `BackgroundTasks` und übergibt es als diesen Parameter. + +## Eine Taskfunktion erstellen + +Erstellen Sie eine Funktion, die als Hintergrundtask ausgeführt werden soll. + +Es handelt sich schlicht um eine Standard-Funktion, die Parameter empfangen kann. + +Es kann sich um eine `async def`- oder normale `def`-Funktion handeln. **FastAPI** weiß, wie damit zu verfahren ist. + +In diesem Fall schreibt die Taskfunktion in eine Datei (den Versand einer E-Mail simulierend). + +Und da der Schreibvorgang nicht `async` und `await` verwendet, definieren wir die Funktion mit normalem `def`: + +```Python hl_lines="6-9" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +## Den Hintergrundtask hinzufügen + +Übergeben Sie innerhalb Ihrer *Pfadoperation-Funktion* Ihre Taskfunktion mit der Methode `.add_task()` an das *Hintergrundtasks*-Objekt: + +```Python hl_lines="14" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +`.add_task()` erhält als Argumente: + +* Eine Taskfunktion, die im Hintergrund ausgeführt wird (`write_notification`). +* Eine beliebige Folge von Argumenten, die der Reihe nach an die Taskfunktion übergeben werden sollen (`email`). +* Alle Schlüsselwort-Argumente, die an die Taskfunktion übergeben werden sollen (`message="some notification"`). + +## Dependency Injection + +Die Verwendung von `BackgroundTasks` funktioniert auch mit dem Dependency Injection System. Sie können einen Parameter vom Typ `BackgroundTasks` auf mehreren Ebenen deklarieren: in einer *Pfadoperation-Funktion*, in einer Abhängigkeit (Dependable), in einer Unterabhängigkeit usw. + +**FastAPI** weiß, was jeweils zu tun ist und wie dasselbe Objekt wiederverwendet werden kann, sodass alle Hintergrundtasks zusammengeführt und anschließend im Hintergrund ausgeführt werden: + +=== "Python 3.10+" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="14 16 23 26" + {!> ../../../docs_src/background_tasks/tutorial002_an.py!} + ``` + +=== "Python 3.10+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="11 13 20 23" + {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} + ``` + +=== "Python 3.8+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + +In obigem Beispiel werden die Nachrichten, *nachdem* die Response gesendet wurde, in die Datei `log.txt` geschrieben. + +Wenn im Request ein Query-Parameter enthalten war, wird dieser in einem Hintergrundtask in das Log geschrieben. + +Und dann schreibt ein weiterer Hintergrundtask, der in der *Pfadoperation-Funktion* erstellt wird, eine Nachricht unter Verwendung des Pfad-Parameters `email`. + +## Technische Details + +Die Klasse `BackgroundTasks` stammt direkt von `starlette.background`. + +Sie wird direkt in FastAPI importiert/inkludiert, sodass Sie sie von `fastapi` importieren können und vermeiden, versehentlich das alternative `BackgroundTask` (ohne das `s` am Ende) von `starlette.background` zu importieren. + +Indem Sie nur `BackgroundTasks` (und nicht `BackgroundTask`) verwenden, ist es dann möglich, es als *Pfadoperation-Funktion*-Parameter zu verwenden und **FastAPI** den Rest für Sie erledigen zu lassen, genau wie bei der direkten Verwendung des `Request`-Objekts. + +Es ist immer noch möglich, `BackgroundTask` allein in FastAPI zu verwenden, aber Sie müssen das Objekt in Ihrem Code erstellen und eine Starlette-`Response` zurückgeben, die es enthält. + +Weitere Details finden Sie in der offiziellen Starlette-Dokumentation für Hintergrundtasks. + +## Vorbehalt + +Wenn Sie umfangreiche Hintergrundberechnungen durchführen müssen und diese nicht unbedingt vom selben Prozess ausgeführt werden müssen (z. B. müssen Sie Speicher, Variablen, usw. nicht gemeinsam nutzen), könnte die Verwendung anderer größerer Tools wie z. B. Celery von Vorteil sein. + +Sie erfordern in der Regel komplexere Konfigurationen und einen Nachrichten-/Job-Queue-Manager wie RabbitMQ oder Redis, ermöglichen Ihnen jedoch die Ausführung von Hintergrundtasks in mehreren Prozessen und insbesondere auf mehreren Servern. + +Um ein Beispiel zu sehen, sehen Sie sich die [Projektgeneratoren](../project-generation.md){.internal-link target=_blank} an. Sie alle enthalten Celery, bereits konfiguriert. + +Wenn Sie jedoch über dieselbe **FastAPI**-Anwendung auf Variablen und Objekte zugreifen oder kleine Hintergrundtasks ausführen müssen (z. B. das Senden einer E-Mail-Benachrichtigung), können Sie einfach `BackgroundTasks` verwenden. + +## Zusammenfassung + +Importieren und verwenden Sie `BackgroundTasks` mit Parametern in *Pfadoperation-Funktionen* und Abhängigkeiten, um Hintergrundtasks hinzuzufügen. diff --git a/docs/de/docs/tutorial/first-steps.md b/docs/de/docs/tutorial/first-steps.md new file mode 100644 index 0000000000..27ba3ec167 --- /dev/null +++ b/docs/de/docs/tutorial/first-steps.md @@ -0,0 +1,333 @@ +# Erste Schritte + +Die einfachste FastAPI-Datei könnte wie folgt aussehen: + +```Python +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Kopieren Sie dies in eine Datei `main.py`. + +Starten Sie den Live-Server: + +
+ +```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 "Hinweis" + Der Befehl `uvicorn main:app` bezieht sich auf: + + * `main`: die Datei `main.py` (das sogenannte Python-„Modul“). + * `app`: das Objekt, welches in der Datei `main.py` mit der Zeile `app = FastAPI()` erzeugt wurde. + * `--reload`: lässt den Server nach Codeänderungen neu starten. Verwenden Sie das nur während der Entwicklung. + +In der Konsolenausgabe sollte es eine Zeile geben, die ungefähr so aussieht: + +```hl_lines="4" +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +Diese Zeile zeigt die URL, unter der Ihre Anwendung auf Ihrem lokalen Computer bereitgestellt wird. + +### Testen Sie es + +Öffnen Sie Ihren Browser unter http://127.0.0.1:8000. + +Sie werden folgende JSON-Response sehen: + +```JSON +{"message": "Hello World"} +``` + +### Interaktive API-Dokumentation + +Gehen Sie als Nächstes auf http://127.0.0.1:8000/docs . + +Sie werden die automatisch erzeugte, interaktive API-Dokumentation sehen (bereitgestellt durch Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternative API-Dokumentation + +Gehen Sie nun auf http://127.0.0.1:8000/redoc. + +Dort sehen Sie die alternative, automatische Dokumentation (bereitgestellt durch ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +### OpenAPI + +**FastAPI** generiert ein „Schema“ mit all Ihren APIs unter Verwendung des **OpenAPI**-Standards zur Definition von APIs. + +#### „Schema“ + +Ein „Schema“ ist eine Definition oder Beschreibung von etwas. Nicht der eigentliche Code, der es implementiert, sondern lediglich eine abstrakte Beschreibung. + +#### API-„Schema“ + +In diesem Fall ist OpenAPI eine Spezifikation, die vorschreibt, wie ein Schema für Ihre API zu definieren ist. + +Diese Schemadefinition enthält Ihre API-Pfade, die möglichen Parameter, welche diese entgegennehmen, usw. + +#### Daten-„Schema“ + +Der Begriff „Schema“ kann sich auch auf die Form von Daten beziehen, wie z. B. einen JSON-Inhalt. + +In diesem Fall sind die JSON-Attribute und deren Datentypen, usw. gemeint. + +#### OpenAPI und JSON Schema + +OpenAPI definiert ein API-Schema für Ihre API. Dieses Schema enthält Definitionen (oder „Schemas“) der Daten, die von Ihrer API unter Verwendung von **JSON Schema**, dem Standard für JSON-Datenschemata, gesendet und empfangen werden. + +#### Überprüfen Sie die `openapi.json` + +Falls Sie wissen möchten, wie das rohe OpenAPI-Schema aussieht: FastAPI generiert automatisch ein JSON (Schema) mit den Beschreibungen Ihrer gesamten API. + +Sie können es direkt einsehen unter: http://127.0.0.1:8000/openapi.json. + +Es wird ein JSON angezeigt, welches ungefähr so aussieht: + +```JSON +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + + + +... +``` + +#### Wofür OpenAPI gedacht ist + +Das OpenAPI-Schema ist die Grundlage für die beiden enthaltenen interaktiven Dokumentationssysteme. + +Es gibt dutzende Alternativen, die alle auf OpenAPI basieren. Sie können jede dieser Alternativen problemlos zu Ihrer mit **FastAPI** erstellten Anwendung hinzufügen. + +Ebenfalls können Sie es verwenden, um automatisch Code für Clients zu generieren, die mit Ihrer API kommunizieren. Zum Beispiel für Frontend-, Mobile- oder IoT-Anwendungen. + +## Rückblick, Schritt für Schritt + +### Schritt 1: Importieren von `FastAPI` + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI` ist eine Python-Klasse, die die gesamte Funktionalität für Ihre API bereitstellt. + +!!! note "Technische Details" + `FastAPI` ist eine Klasse, die direkt von `Starlette` erbt. + + Sie können alle Starlette-Funktionalitäten auch mit `FastAPI` nutzen. + +### Schritt 2: Erzeugen einer `FastAPI`-„Instanz“ + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +In diesem Beispiel ist die Variable `app` eine „Instanz“ der Klasse `FastAPI`. + +Dies wird der Hauptinteraktionspunkt für die Erstellung all Ihrer APIs sein. + +Die Variable `app` ist dieselbe, auf die sich der Befehl `uvicorn` bezieht: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Wenn Sie Ihre Anwendung wie folgt erstellen: + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial002.py!} +``` + +Und in eine Datei `main.py` einfügen, dann würden Sie `uvicorn` wie folgt aufrufen: + +
+ +```console +$ uvicorn main:my_awesome_api --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Schritt 3: Erstellen einer *Pfadoperation* + +#### Pfad + +„Pfad“ bezieht sich hier auf den letzten Teil der URL, beginnend mit dem ersten `/`. + +In einer URL wie: + +``` +https://example.com/items/foo +``` + +... wäre der Pfad folglich: + +``` +/items/foo +``` + +!!! info + Ein „Pfad“ wird häufig auch als „Endpunkt“ oder „Route“ bezeichnet. + +Bei der Erstellung einer API ist der „Pfad“ die wichtigste Möglichkeit zur Trennung von „Anliegen“ und „Ressourcen“. + +#### Operation + +„Operation“ bezieht sich hier auf eine der HTTP-„Methoden“. + +Eine von diesen: + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +... und die etwas Exotischeren: + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +Im HTTP-Protokoll können Sie mit jedem Pfad über eine (oder mehrere) dieser „Methoden“ kommunizieren. + +--- + +Bei der Erstellung von APIs verwenden Sie normalerweise diese spezifischen HTTP-Methoden, um eine bestimmte Aktion durchzuführen. + +Normalerweise verwenden Sie: + +* `POST`: um Daten zu erzeugen (create). +* `GET`: um Daten zu lesen (read). +* `PUT`: um Daten zu aktualisieren (update). +* `DELETE`: um Daten zu löschen (delete). + +In OpenAPI wird folglich jede dieser HTTP-Methoden als „Operation“ bezeichnet. + +Wir werden sie auch „**Operationen**“ nennen. + +#### Definieren eines *Pfadoperation-Dekorators* + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Das `@app.get("/")` sagt **FastAPI**, dass die Funktion direkt darunter für die Bearbeitung von Anfragen zuständig ist, die an: + + * den Pfad `/` + * unter der Verwendung der get-Operation gehen + +!!! info "`@decorator` Information" + Diese `@something`-Syntax wird in Python „Dekorator“ genannt. + + Sie platzieren ihn über einer Funktion. Wie ein hübscher, dekorativer Hut (daher kommt wohl der Begriff). + + Ein „Dekorator“ nimmt die darunter stehende Funktion und macht etwas damit. + + In unserem Fall teilt dieser Dekorator **FastAPI** mit, dass die folgende Funktion mit dem **Pfad** `/` und der **Operation** `get` zusammenhängt. + + Dies ist der „**Pfadoperation-Dekorator**“. + +Sie können auch die anderen Operationen verwenden: + +* `@app.post()` +* `@app.put()` +* `@app.delete()` + +Oder die exotischeren: + +* `@app.options()` +* `@app.head()` +* `@app.patch()` +* `@app.trace()` + +!!! tip "Tipp" + Es steht Ihnen frei, jede Operation (HTTP-Methode) so zu verwenden, wie Sie es möchten. + + **FastAPI** erzwingt keine bestimmte Bedeutung. + + Die hier aufgeführten Informationen dienen als Leitfaden und sind nicht verbindlich. + + Wenn Sie beispielsweise GraphQL verwenden, führen Sie normalerweise alle Aktionen nur mit „POST“-Operationen durch. + +### Schritt 4: Definieren der **Pfadoperation-Funktion** + +Das ist unsere „**Pfadoperation-Funktion**“: + +* **Pfad**: ist `/`. +* **Operation**: ist `get`. +* **Funktion**: ist die Funktion direkt unter dem „Dekorator“ (unter `@app.get("/")`). + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Dies ist eine Python-Funktion. + +Sie wird von **FastAPI** immer dann aufgerufen, wenn sie eine Anfrage an die URL "`/`" mittels einer `GET`-Operation erhält. + +In diesem Fall handelt es sich um eine `async`-Funktion. + +--- + +Sie könnten sie auch als normale Funktion anstelle von `async def` definieren: + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note "Hinweis" + Wenn Sie den Unterschied nicht kennen, lesen Sie [Async: *„In Eile?“*](../async.md#in-eile){.internal-link target=_blank}. + +### Schritt 5: den Inhalt zurückgeben + +```Python hl_lines="8" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Sie können ein `dict`, eine `list`, einzelne Werte wie `str`, `int`, usw. zurückgeben. + +Sie können auch Pydantic-Modelle zurückgeben (dazu später mehr). + +Es gibt viele andere Objekte und Modelle, die automatisch zu JSON konvertiert werden (einschließlich ORMs usw.). Versuchen Sie, Ihre Lieblingsobjekte zu verwenden. Es ist sehr wahrscheinlich, dass sie bereits unterstützt werden. + +## Zusammenfassung + +* Importieren Sie `FastAPI`. +* Erstellen Sie eine `app` Instanz. +* Schreiben Sie einen **Pfadoperation-Dekorator** (wie z. B. `@app.get("/")`). +* Schreiben Sie eine **Pfadoperation-Funktion** (wie z. B. oben `def root(): ...`). +* Starten Sie den Entwicklungsserver (z. B. `uvicorn main:app --reload`). diff --git a/docs/de/docs/tutorial/index.md b/docs/de/docs/tutorial/index.md new file mode 100644 index 0000000000..dd7ed43bda --- /dev/null +++ b/docs/de/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Tutorial - Benutzerhandbuch - Intro + +Diese Anleitung zeigt Ihnen Schritt für Schritt, wie Sie **FastAPI** mit den meisten Funktionen nutzen können. + +Jeder Abschnitt baut schrittweise auf den vorhergehenden auf. Diese Abschnitte sind aber nach einzelnen Themen gegliedert, sodass Sie direkt zu einem bestimmten Thema übergehen können, um Ihre speziellen API-Anforderungen zu lösen. + +Außerdem dienen diese als zukünftige Referenz. + +Dadurch können Sie jederzeit zurückkommen und sehen genau das, was Sie benötigen. + +## Den Code ausführen + +Alle Codeblöcke können kopiert und direkt verwendet werden (da es sich um getestete Python-Dateien handelt). + +Um eines der Beispiele auszuführen, kopieren Sie den Code in die Datei `main.py`, und starten Sie `uvicorn` mit: + +
+ +```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. +``` + +
+ +Es wird **ausdrücklich empfohlen**, dass Sie den Code schreiben oder kopieren, ihn bearbeiten und lokal ausführen. + +Die Verwendung in Ihrem eigenen Editor zeigt Ihnen die Vorteile von FastAPI am besten, wenn Sie sehen, wie wenig Code Sie schreiben müssen, all die Typprüfungen, die automatische Vervollständigung usw. + +--- + +## FastAPI installieren + +Der erste Schritt besteht aus der Installation von FastAPI. + +Für dieses Tutorial empfiehlt es sich, FastAPI mit allen optionalen Abhängigkeiten und Funktionen zu installieren: + +
+ +```console +$ pip install "fastapi[all]" + +---> 100% +``` + +
+ +...dies beinhaltet auch `uvicorn`, das Sie als Server verwenden können, auf dem Ihr Code läuft. + +!!! Hinweis + Sie können die Installation auch in einzelnen Schritten ausführen. + + Dies werden Sie wahrscheinlich tun, wenn Sie Ihre Anwendung produktiv einsetzen möchten: + + ``` + pip install fastapi + ``` + + Installieren Sie auch `uvicorn`, dies arbeitet als Server: + + ``` + pip install "uvicorn[standard]" + ``` + + Dasselbe gilt für jede der optionalen Abhängigkeiten, die Sie verwenden möchten. + +## Erweitertes Benutzerhandbuch + +Zusätzlich gibt es ein **Erweitertes Benutzerhandbuch**, dies können Sie später nach diesem **Tutorial - Benutzerhandbuch** lesen. + +Das **Erweiterte Benutzerhandbuch** baut auf dieses Tutorial auf, verwendet dieselben Konzepte und bringt Ihnen zusätzliche Funktionen bei. + +Allerdings sollten Sie zuerst das **Tutorial - Benutzerhandbuch** lesen (was Sie gerade lesen). + +Es ist so konzipiert, dass Sie nur mit dem **Tutorial - Benutzerhandbuch** eine vollständige Anwendung erstellen können und diese dann je nach Bedarf mit einigen der zusätzlichen Ideen aus dem **Erweiterten Benutzerhandbuch** erweitern können. diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 8c3c42b5f3..de18856f44 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -1,146 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/de/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: de -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs/em/docs/advanced/additional-responses.md b/docs/em/docs/advanced/additional-responses.md new file mode 100644 index 0000000000..26963c2e31 --- /dev/null +++ b/docs/em/docs/advanced/additional-responses.md @@ -0,0 +1,240 @@ +# 🌖 📨 🗄 + +!!! warning + 👉 👍 🏧 ❔. + + 🚥 👆 ▶️ ⏮️ **FastAPI**, 👆 💪 🚫 💪 👉. + +👆 💪 📣 🌖 📨, ⏮️ 🌖 👔 📟, 🔉 🆎, 📛, ♒️. + +👈 🌖 📨 🔜 🔌 🗄 🔗, 👫 🔜 😑 🛠️ 🩺. + +✋️ 👈 🌖 📨 👆 ✔️ ⚒ 💭 👆 📨 `Response` 💖 `JSONResponse` 🔗, ⏮️ 👆 👔 📟 & 🎚. + +## 🌖 📨 ⏮️ `model` + +👆 💪 🚶‍♀️ 👆 *➡ 🛠️ 👨‍🎨* 🔢 `responses`. + +⚫️ 📨 `dict`, 🔑 👔 📟 🔠 📨, 💖 `200`, & 💲 🎏 `dict`Ⓜ ⏮️ ℹ 🔠 👫. + +🔠 👈 📨 `dict`Ⓜ 💪 ✔️ 🔑 `model`, ⚗ Pydantic 🏷, 💖 `response_model`. + +**FastAPI** 🔜 ✊ 👈 🏷, 🏗 🚮 🎻 🔗 & 🔌 ⚫️ ☑ 🥉 🗄. + +🖼, 📣 ➕1️⃣ 📨 ⏮️ 👔 📟 `404` & Pydantic 🏷 `Message`, 👆 💪 ✍: + +```Python hl_lines="18 22" +{!../../../docs_src/additional_responses/tutorial001.py!} +``` + +!!! note + ✔️ 🤯 👈 👆 ✔️ 📨 `JSONResponse` 🔗. + +!!! info + `model` 🔑 🚫 🍕 🗄. + + **FastAPI** 🔜 ✊ Pydantic 🏷 ⚪️➡️ 📤, 🏗 `JSON Schema`, & 🚮 ⚫️ ☑ 🥉. + + ☑ 🥉: + + * 🔑 `content`, 👈 ✔️ 💲 ➕1️⃣ 🎻 🎚 (`dict`) 👈 🔌: + * 🔑 ⏮️ 📻 🆎, ✅ `application/json`, 👈 🔌 💲 ➕1️⃣ 🎻 🎚, 👈 🔌: + * 🔑 `schema`, 👈 ✔️ 💲 🎻 🔗 ⚪️➡️ 🏷, 📥 ☑ 🥉. + * **FastAPI** 🚮 🔗 📥 🌐 🎻 🔗 ➕1️⃣ 🥉 👆 🗄 ↩️ ✅ ⚫️ 🔗. 👉 🌌, 🎏 🈸 & 👩‍💻 💪 ⚙️ 👈 🎻 🔗 🔗, 🚚 👻 📟 ⚡ 🧰, ♒️. + +🏗 📨 🗄 👉 *➡ 🛠️* 🔜: + +```JSON hl_lines="3-12" +{ + "responses": { + "404": { + "description": "Additional Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } +} +``` + +🔗 🔗 ➕1️⃣ 🥉 🔘 🗄 🔗: + +```JSON hl_lines="4-16" +{ + "components": { + "schemas": { + "Message": { + "title": "Message", + "required": [ + "message" + ], + "type": "object", + "properties": { + "message": { + "title": "Message", + "type": "string" + } + } + }, + "Item": { + "title": "Item", + "required": [ + "id", + "value" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + } + }, + "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" + } + } + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + } + } + } + } + } + } +} +``` + +## 🌖 🔉 🆎 👑 📨 + +👆 💪 ⚙️ 👉 🎏 `responses` 🔢 🚮 🎏 🔉 🆎 🎏 👑 📨. + +🖼, 👆 💪 🚮 🌖 📻 🆎 `image/png`, 📣 👈 👆 *➡ 🛠️* 💪 📨 🎻 🎚 (⏮️ 📻 🆎 `application/json`) ⚖️ 🇩🇴 🖼: + +```Python hl_lines="19-24 28" +{!../../../docs_src/additional_responses/tutorial002.py!} +``` + +!!! note + 👀 👈 👆 ✔️ 📨 🖼 ⚙️ `FileResponse` 🔗. + +!!! info + 🚥 👆 ✔ 🎏 📻 🆎 🎯 👆 `responses` 🔢, FastAPI 🔜 🤔 📨 ✔️ 🎏 📻 🆎 👑 📨 🎓 (🔢 `application/json`). + + ✋️ 🚥 👆 ✔️ ✔ 🛃 📨 🎓 ⏮️ `None` 🚮 📻 🆎, FastAPI 🔜 ⚙️ `application/json` 🙆 🌖 📨 👈 ✔️ 👨‍💼 🏷. + +## 🌀 ℹ + +👆 💪 🌀 📨 ℹ ⚪️➡️ 💗 🥉, 🔌 `response_model`, `status_code`, & `responses` 🔢. + +👆 💪 📣 `response_model`, ⚙️ 🔢 👔 📟 `200` (⚖️ 🛃 1️⃣ 🚥 👆 💪), & ⤴️ 📣 🌖 ℹ 👈 🎏 📨 `responses`, 🔗 🗄 🔗. + +**FastAPI** 🔜 🚧 🌖 ℹ ⚪️➡️ `responses`, & 🌀 ⚫️ ⏮️ 🎻 🔗 ⚪️➡️ 👆 🏷. + +🖼, 👆 💪 📣 📨 ⏮️ 👔 📟 `404` 👈 ⚙️ Pydantic 🏷 & ✔️ 🛃 `description`. + +& 📨 ⏮️ 👔 📟 `200` 👈 ⚙️ 👆 `response_model`, ✋️ 🔌 🛃 `example`: + +```Python hl_lines="20-31" +{!../../../docs_src/additional_responses/tutorial003.py!} +``` + +⚫️ 🔜 🌐 🌀 & 🔌 👆 🗄, & 🎦 🛠️ 🩺: + + + +## 🌀 🔢 📨 & 🛃 🕐 + +👆 💪 💚 ✔️ 🔁 📨 👈 ✔ 📚 *➡ 🛠️*, ✋️ 👆 💚 🌀 👫 ⏮️ 🛃 📨 💚 🔠 *➡ 🛠️*. + +📚 💼, 👆 💪 ⚙️ 🐍 ⚒ "🏗" `dict` ⏮️ `**dict_to_unpack`: + +```Python +old_dict = { + "old key": "old value", + "second old key": "second old value", +} +new_dict = {**old_dict, "new key": "new value"} +``` + +📥, `new_dict` 🔜 🔌 🌐 🔑-💲 👫 ⚪️➡️ `old_dict` ➕ 🆕 🔑-💲 👫: + +```Python +{ + "old key": "old value", + "second old key": "second old value", + "new key": "new value", +} +``` + +👆 💪 ⚙️ 👈 ⚒ 🏤-⚙️ 🔢 📨 👆 *➡ 🛠️* & 🌀 👫 ⏮️ 🌖 🛃 🕐. + +🖼: + +```Python hl_lines="13-17 26" +{!../../../docs_src/additional_responses/tutorial004.py!} +``` + +## 🌖 ℹ 🔃 🗄 📨 + +👀 ⚫️❔ ⚫️❔ 👆 💪 🔌 📨, 👆 💪 ✅ 👉 📄 🗄 🔧: + +* 🗄 📨 🎚, ⚫️ 🔌 `Response Object`. +* 🗄 📨 🎚, 👆 💪 🔌 🕳 ⚪️➡️ 👉 🔗 🔠 📨 🔘 👆 `responses` 🔢. ✅ `description`, `headers`, `content` (🔘 👉 👈 👆 📣 🎏 🔉 🆎 & 🎻 🔗), & `links`. diff --git a/docs/em/docs/advanced/additional-status-codes.md b/docs/em/docs/advanced/additional-status-codes.md new file mode 100644 index 0000000000..392579df6e --- /dev/null +++ b/docs/em/docs/advanced/additional-status-codes.md @@ -0,0 +1,37 @@ +# 🌖 👔 📟 + +🔢, **FastAPI** 🔜 📨 📨 ⚙️ `JSONResponse`, 🚮 🎚 👆 📨 ⚪️➡️ 👆 *➡ 🛠️* 🔘 👈 `JSONResponse`. + +⚫️ 🔜 ⚙️ 🔢 👔 📟 ⚖️ 1️⃣ 👆 ⚒ 👆 *➡ 🛠️*. + +## 🌖 👔 📟 + +🚥 👆 💚 📨 🌖 👔 📟 ↖️ ⚪️➡️ 👑 1️⃣, 👆 💪 👈 🛬 `Response` 🔗, 💖 `JSONResponse`, & ⚒ 🌖 👔 📟 🔗. + +🖼, ➡️ 💬 👈 👆 💚 ✔️ *➡ 🛠️* 👈 ✔ ℹ 🏬, & 📨 🇺🇸🔍 👔 📟 2️⃣0️⃣0️⃣ "👌" 🕐❔ 🏆. + +✋️ 👆 💚 ⚫️ 🚫 🆕 🏬. & 🕐❔ 🏬 🚫 🔀 ⏭, ⚫️ ✍ 👫, & 📨 🇺🇸🔍 👔 📟 2️⃣0️⃣1️⃣ "✍". + +🏆 👈, 🗄 `JSONResponse`, & 📨 👆 🎚 📤 🔗, ⚒ `status_code` 👈 👆 💚: + +```Python hl_lines="4 25" +{!../../../docs_src/additional_status_codes/tutorial001.py!} +``` + +!!! warning + 🕐❔ 👆 📨 `Response` 🔗, 💖 🖼 🔛, ⚫️ 🔜 📨 🔗. + + ⚫️ 🏆 🚫 🎻 ⏮️ 🏷, ♒️. + + ⚒ 💭 ⚫️ ✔️ 📊 👆 💚 ⚫️ ✔️, & 👈 💲 ☑ 🎻 (🚥 👆 ⚙️ `JSONResponse`). + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import JSONResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. 🎏 ⏮️ `status`. + +## 🗄 & 🛠️ 🩺 + +🚥 👆 📨 🌖 👔 📟 & 📨 🔗, 👫 🏆 🚫 🔌 🗄 🔗 (🛠️ 🩺), ↩️ FastAPI 🚫 ✔️ 🌌 💭 ⏪ ⚫️❔ 👆 🚶 📨. + +✋️ 👆 💪 📄 👈 👆 📟, ⚙️: [🌖 📨](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/em/docs/advanced/advanced-dependencies.md b/docs/em/docs/advanced/advanced-dependencies.md new file mode 100644 index 0000000000..fa1554734c --- /dev/null +++ b/docs/em/docs/advanced/advanced-dependencies.md @@ -0,0 +1,70 @@ +# 🏧 🔗 + +## 🔗 🔗 + +🌐 🔗 👥 ✔️ 👀 🔧 🔢 ⚖️ 🎓. + +✋️ 📤 💪 💼 🌐❔ 👆 💚 💪 ⚒ 🔢 🔛 🔗, 🍵 ✔️ 📣 📚 🎏 🔢 ⚖️ 🎓. + +➡️ 🌈 👈 👥 💚 ✔️ 🔗 👈 ✅ 🚥 🔢 🔢 `q` 🔌 🔧 🎚. + +✋️ 👥 💚 💪 🔗 👈 🔧 🎚. + +## "🇧🇲" 👐 + +🐍 📤 🌌 ⚒ 👐 🎓 "🇧🇲". + +🚫 🎓 ⚫️ (❔ ⏪ 🇧🇲), ✋️ 👐 👈 🎓. + +👈, 👥 📣 👩‍🔬 `__call__`: + +```Python hl_lines="10" +{!../../../docs_src/dependencies/tutorial011.py!} +``` + +👉 💼, 👉 `__call__` ⚫️❔ **FastAPI** 🔜 ⚙️ ✅ 🌖 🔢 & 🎧-🔗, & 👉 ⚫️❔ 🔜 🤙 🚶‍♀️ 💲 🔢 👆 *➡ 🛠️ 🔢* ⏪. + +## 🔗 👐 + +& 🔜, 👥 💪 ⚙️ `__init__` 📣 🔢 👐 👈 👥 💪 ⚙️ "🔗" 🔗: + +```Python hl_lines="7" +{!../../../docs_src/dependencies/tutorial011.py!} +``` + +👉 💼, **FastAPI** 🏆 🚫 ⏱ 👆 ⚖️ 💅 🔃 `__init__`, 👥 🔜 ⚙️ ⚫️ 🔗 👆 📟. + +## ✍ 👐 + +👥 💪 ✍ 👐 👉 🎓 ⏮️: + +```Python hl_lines="16" +{!../../../docs_src/dependencies/tutorial011.py!} +``` + +& 👈 🌌 👥 💪 "🔗" 👆 🔗, 👈 🔜 ✔️ `"bar"` 🔘 ⚫️, 🔢 `checker.fixed_content`. + +## ⚙️ 👐 🔗 + +⤴️, 👥 💪 ⚙️ 👉 `checker` `Depends(checker)`, ↩️ `Depends(FixedContentQueryChecker)`, ↩️ 🔗 👐, `checker`, 🚫 🎓 ⚫️. + +& 🕐❔ ❎ 🔗, **FastAPI** 🔜 🤙 👉 `checker` 💖: + +```Python +checker(q="somequery") +``` + +...& 🚶‍♀️ ⚫️❔ 👈 📨 💲 🔗 👆 *➡ 🛠️ 🔢* 🔢 `fixed_content_included`: + +```Python hl_lines="20" +{!../../../docs_src/dependencies/tutorial011.py!} +``` + +!!! tip + 🌐 👉 💪 😑 🎭. & ⚫️ 💪 🚫 📶 🆑 ❔ ⚫️ ⚠. + + 👫 🖼 😫 🙅, ✋️ 🎦 ❔ ⚫️ 🌐 👷. + + 📃 🔃 💂‍♂, 📤 🚙 🔢 👈 🛠️ 👉 🎏 🌌. + + 🚥 👆 🤔 🌐 👉, 👆 ⏪ 💭 ❔ 👈 🚙 🧰 💂‍♂ 👷 🔘. diff --git a/docs/em/docs/advanced/async-sql-databases.md b/docs/em/docs/advanced/async-sql-databases.md new file mode 100644 index 0000000000..848936de16 --- /dev/null +++ b/docs/em/docs/advanced/async-sql-databases.md @@ -0,0 +1,162 @@ +# 🔁 🗄 (🔗) 💽 + +👆 💪 ⚙️ `encode/databases` ⏮️ **FastAPI** 🔗 💽 ⚙️ `async` & `await`. + +⚫️ 🔗 ⏮️: + +* ✳ +* ✳ +* 🗄 + +👉 🖼, 👥 🔜 ⚙️ **🗄**, ↩️ ⚫️ ⚙️ 👁 📁 & 🐍 ✔️ 🛠️ 🐕‍🦺. , 👆 💪 📁 👉 🖼 & 🏃 ⚫️. + +⏪, 👆 🏭 🈸, 👆 💪 💚 ⚙️ 💽 💽 💖 **✳**. + +!!! tip + 👆 💪 🛠️ 💭 ⚪️➡️ 📄 🔃 🇸🇲 🐜 ([🗄 (🔗) 💽](../tutorial/sql-databases.md){.internal-link target=_blank}), 💖 ⚙️ 🚙 🔢 🎭 🛠️ 💽, 🔬 👆 **FastAPI** 📟. + + 👉 📄 🚫 ✔ 📚 💭, 🌓 😑 💃. + +## 🗄 & ⚒ 🆙 `SQLAlchemy` + +* 🗄 `SQLAlchemy`. +* ✍ `metadata` 🎚. +* ✍ 🏓 `notes` ⚙️ `metadata` 🎚. + +```Python hl_lines="4 14 16-22" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +!!! tip + 👀 👈 🌐 👉 📟 😁 🇸🇲 🐚. + + `databases` 🚫 🔨 🕳 📥. + +## 🗄 & ⚒ 🆙 `databases` + +* 🗄 `databases`. +* ✍ `DATABASE_URL`. +* ✍ `database` 🎚. + +```Python hl_lines="3 9 12" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +!!! tip + 🚥 👆 🔗 🎏 💽 (✅ ✳), 👆 🔜 💪 🔀 `DATABASE_URL`. + +## ✍ 🏓 + +👉 💼, 👥 🏗 🏓 🎏 🐍 📁, ✋️ 🏭, 👆 🔜 🎲 💚 ✍ 👫 ⏮️ ⚗, 🛠️ ⏮️ 🛠️, ♒️. + +📥, 👉 📄 🔜 🏃 🔗, ▶️️ ⏭ ▶️ 👆 **FastAPI** 🈸. + +* ✍ `engine`. +* ✍ 🌐 🏓 ⚪️➡️ `metadata` 🎚. + +```Python hl_lines="25-28" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +## ✍ 🏷 + +✍ Pydantic 🏷: + +* 🗒 ✍ (`NoteIn`). +* 🗒 📨 (`Note`). + +```Python hl_lines="31-33 36-39" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +🏗 👫 Pydantic 🏷, 🔢 💽 🔜 ✔, 🎻 (🗜), & ✍ (📄). + +, 👆 🔜 💪 👀 ⚫️ 🌐 🎓 🛠️ 🩺. + +## 🔗 & 🔌 + +* ✍ 👆 `FastAPI` 🈸. +* ✍ 🎉 🐕‍🦺 🔗 & 🔌 ⚪️➡️ 💽. + +```Python hl_lines="42 45-47 50-52" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +## ✍ 🗒 + +✍ *➡ 🛠️ 🔢* ✍ 🗒: + +```Python hl_lines="55-58" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +!!! Note + 👀 👈 👥 🔗 ⏮️ 💽 ⚙️ `await`, *➡ 🛠️ 🔢* 📣 ⏮️ `async`. + +### 👀 `response_model=List[Note]` + +⚫️ ⚙️ `typing.List`. + +👈 📄 (& ✔, 🎻, ⛽) 🔢 💽, `list` `Note`Ⓜ. + +## ✍ 🗒 + +✍ *➡ 🛠️ 🔢* ✍ 🗒: + +```Python hl_lines="61-65" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +!!! Note + 👀 👈 👥 🔗 ⏮️ 💽 ⚙️ `await`, *➡ 🛠️ 🔢* 📣 ⏮️ `async`. + +### 🔃 `{**note.dict(), "id": last_record_id}` + +`note` Pydantic `Note` 🎚. + +`note.dict()` 📨 `dict` ⏮️ 🚮 💽, 🕳 💖: + +```Python +{ + "text": "Some note", + "completed": False, +} +``` + +✋️ ⚫️ 🚫 ✔️ `id` 🏑. + +👥 ✍ 🆕 `dict`, 👈 🔌 🔑-💲 👫 ⚪️➡️ `note.dict()` ⏮️: + +```Python +{**note.dict()} +``` + +`**note.dict()` "unpacks" the key value pairs directly, so, `{**note.dict()}` would be, more or less, a copy of `note.dict()`. + +& ⤴️, 👥 ↔ 👈 📁 `dict`, ❎ ➕1️⃣ 🔑-💲 👫: `"id": last_record_id`: + +```Python +{**note.dict(), "id": last_record_id} +``` + +, 🏁 🏁 📨 🔜 🕳 💖: + +```Python +{ + "id": 1, + "text": "Some note", + "completed": False, +} +``` + +## ✅ ⚫️ + +👆 💪 📁 👉 📟, & 👀 🩺 http://127.0.0.1:8000/docs. + +📤 👆 💪 👀 🌐 👆 🛠️ 📄 & 🔗 ⏮️ ⚫️: + + + +## 🌅 ℹ + +👆 💪 ✍ 🌅 🔃 `encode/databases` 🚮 📂 📃. diff --git a/docs/em/docs/advanced/async-tests.md b/docs/em/docs/advanced/async-tests.md new file mode 100644 index 0000000000..df94c6ce7c --- /dev/null +++ b/docs/em/docs/advanced/async-tests.md @@ -0,0 +1,92 @@ +# 🔁 💯 + +👆 ✔️ ⏪ 👀 ❔ 💯 👆 **FastAPI** 🈸 ⚙️ 🚚 `TestClient`. 🆙 🔜, 👆 ✔️ 🕴 👀 ❔ ✍ 🔁 💯, 🍵 ⚙️ `async` 🔢. + +➖ 💪 ⚙️ 🔁 🔢 👆 💯 💪 ⚠, 🖼, 🕐❔ 👆 🔬 👆 💽 🔁. 🌈 👆 💚 💯 📨 📨 👆 FastAPI 🈸 & ⤴️ ✔ 👈 👆 👩‍💻 ⏪ ✍ ☑ 💽 💽, ⏪ ⚙️ 🔁 💽 🗃. + +➡️ 👀 ❔ 👥 💪 ⚒ 👈 👷. + +## pytest.mark.anyio + +🚥 👥 💚 🤙 🔁 🔢 👆 💯, 👆 💯 🔢 ✔️ 🔁. AnyIO 🚚 👌 📁 👉, 👈 ✔ 👥 ✔ 👈 💯 🔢 🤙 🔁. + +## 🇸🇲 + +🚥 👆 **FastAPI** 🈸 ⚙️ 😐 `def` 🔢 ↩️ `async def`, ⚫️ `async` 🈸 🔘. + +`TestClient` 🔨 🎱 🔘 🤙 🔁 FastAPI 🈸 👆 😐 `def` 💯 🔢, ⚙️ 🐩 ✳. ✋️ 👈 🎱 🚫 👷 🚫🔜 🕐❔ 👥 ⚙️ ⚫️ 🔘 🔁 🔢. 🏃 👆 💯 🔁, 👥 💪 🙅‍♂ 📏 ⚙️ `TestClient` 🔘 👆 💯 🔢. + +`TestClient` ⚓️ 🔛 🇸🇲, & ↩️, 👥 💪 ⚙️ ⚫️ 🔗 💯 🛠️. + +## 🖼 + +🙅 🖼, ➡️ 🤔 📁 📊 🎏 1️⃣ 🔬 [🦏 🈸](../tutorial/bigger-applications.md){.internal-link target=_blank} & [🔬](../tutorial/testing.md){.internal-link target=_blank}: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +📁 `main.py` 🔜 ✔️: + +```Python +{!../../../docs_src/async_tests/main.py!} +``` + +📁 `test_main.py` 🔜 ✔️ 💯 `main.py`, ⚫️ 💪 👀 💖 👉 🔜: + +```Python +{!../../../docs_src/async_tests/test_main.py!} +``` + +## 🏃 ⚫️ + +👆 💪 🏃 👆 💯 🐌 📨: + +
+ +```console +$ pytest + +---> 100% +``` + +
+ +## ℹ + +📑 `@pytest.mark.anyio` 💬 ✳ 👈 👉 💯 🔢 🔜 🤙 🔁: + +```Python hl_lines="7" +{!../../../docs_src/async_tests/test_main.py!} +``` + +!!! tip + 🗒 👈 💯 🔢 🔜 `async def` ↩️ `def` ⏭ 🕐❔ ⚙️ `TestClient`. + +⤴️ 👥 💪 ✍ `AsyncClient` ⏮️ 📱, & 📨 🔁 📨 ⚫️, ⚙️ `await`. + +```Python hl_lines="9-10" +{!../../../docs_src/async_tests/test_main.py!} +``` + +👉 🌓: + +```Python +response = client.get('/') +``` + +...👈 👥 ⚙️ ⚒ 👆 📨 ⏮️ `TestClient`. + +!!! tip + 🗒 👈 👥 ⚙️ 🔁/⌛ ⏮️ 🆕 `AsyncClient` - 📨 🔁. + +## 🎏 🔁 🔢 🤙 + +🔬 🔢 🔜 🔁, 👆 💪 🔜 🤙 (& `await`) 🎏 `async` 🔢 ↖️ ⚪️➡️ 📨 📨 👆 FastAPI 🈸 👆 💯, ⚫️❔ 👆 🔜 🤙 👫 🙆 🙆 👆 📟. + +!!! tip + 🚥 👆 ⚔ `RuntimeError: Task attached to a different loop` 🕐❔ 🛠️ 🔁 🔢 🤙 👆 💯 (✅ 🕐❔ ⚙️ ✳ MotorClient) 💭 🔗 🎚 👈 💪 🎉 ➰ 🕴 🏞 🔁 🔢, ✅ `'@app.on_event("startup")` ⏲. diff --git a/docs/em/docs/advanced/behind-a-proxy.md b/docs/em/docs/advanced/behind-a-proxy.md new file mode 100644 index 0000000000..12afe638c6 --- /dev/null +++ b/docs/em/docs/advanced/behind-a-proxy.md @@ -0,0 +1,346 @@ +# ⛅ 🗳 + +⚠, 👆 5️⃣📆 💪 ⚙️ **🗳** 💽 💖 Traefik ⚖️ 👌 ⏮️ 📳 👈 🚮 ➕ ➡ 🔡 👈 🚫 👀 👆 🈸. + +👫 💼 👆 💪 ⚙️ `root_path` 🔗 👆 🈸. + +`root_path` 🛠️ 🚚 🔫 🔧 (👈 FastAPI 🏗 🔛, 🔘 💃). + +`root_path` ⚙️ 🍵 👫 🎯 💼. + +& ⚫️ ⚙️ 🔘 🕐❔ 🗜 🎧-🈸. + +## 🗳 ⏮️ 🎞 ➡ 🔡 + +✔️ 🗳 ⏮️ 🎞 ➡ 🔡, 👉 💼, ⛓ 👈 👆 💪 📣 ➡ `/app` 👆 📟, ✋️ ⤴️, 👆 🚮 🧽 🔛 🔝 (🗳) 👈 🔜 🚮 👆 **FastAPI** 🈸 🔽 ➡ 💖 `/api/v1`. + +👉 💼, ⏮️ ➡ `/app` 🔜 🤙 🍦 `/api/v1/app`. + +✋️ 🌐 👆 📟 ✍ 🤔 📤 `/app`. + +& 🗳 🔜 **"❎"** **➡ 🔡** 🔛 ✈ ⏭ 📶 📨 Uvicorn, 🚧 👆 🈸 🤔 👈 ⚫️ 🍦 `/app`, 👈 👆 🚫 ✔️ ℹ 🌐 👆 📟 🔌 🔡 `/api/v1`. + +🆙 📥, 🌐 🔜 👷 🛎. + +✋️ ⤴️, 🕐❔ 👆 📂 🛠️ 🩺 🎚 (🕸), ⚫️ 🔜 ⌛ 🤚 🗄 🔗 `/openapi.json`, ↩️ `/api/v1/openapi.json`. + +, 🕸 (👈 🏃 🖥) 🔜 🔄 🏆 `/openapi.json` & 🚫🔜 💪 🤚 🗄 🔗. + +↩️ 👥 ✔️ 🗳 ⏮️ ➡ 🔡 `/api/v1` 👆 📱, 🕸 💪 ☕ 🗄 🔗 `/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 + 📢 `0.0.0.0` 🛎 ⚙️ ⛓ 👈 📋 👂 🔛 🌐 📢 💪 👈 🎰/💽. + +🩺 🎚 🔜 💪 🗄 🔗 📣 👈 👉 🛠️ `server` 🔎 `/api/v1` (⛅ 🗳). 🖼: + +```JSON hl_lines="4-8" +{ + "openapi": "3.0.2", + // More stuff here + "servers": [ + { + "url": "/api/v1" + } + ], + "paths": { + // More stuff here + } +} +``` + +👉 🖼, "🗳" 💪 🕳 💖 **Traefik**. & 💽 🔜 🕳 💖 **Uvicorn**, 🏃‍♂ 👆 FastAPI 🈸. + +### 🚚 `root_path` + +🏆 👉, 👆 💪 ⚙️ 📋 ⏸ 🎛 `--root-path` 💖: + +
+ +```console +$ uvicorn main:app --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +🚥 👆 ⚙️ Hypercorn, ⚫️ ✔️ 🎛 `--root-path`. + +!!! note "📡 ℹ" + 🔫 🔧 🔬 `root_path` 👉 ⚙️ 💼. + + & `--root-path` 📋 ⏸ 🎛 🚚 👈 `root_path`. + +### ✅ ⏮️ `root_path` + +👆 💪 🤚 ⏮️ `root_path` ⚙️ 👆 🈸 🔠 📨, ⚫️ 🍕 `scope` 📖 (👈 🍕 🔫 🔌). + +📥 👥 ✅ ⚫️ 📧 🎦 🎯. + +```Python hl_lines="8" +{!../../../docs_src/behind_a_proxy/tutorial001.py!} +``` + +⤴️, 🚥 👆 ▶️ Uvicorn ⏮️: + +
+ +```console +$ uvicorn main:app --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +📨 🔜 🕳 💖: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +### ⚒ `root_path` FastAPI 📱 + +👐, 🚥 👆 🚫 ✔️ 🌌 🚚 📋 ⏸ 🎛 💖 `--root-path` ⚖️ 🌓, 👆 💪 ⚒ `root_path` 🔢 🕐❔ 🏗 👆 FastAPI 📱: + +```Python hl_lines="3" +{!../../../docs_src/behind_a_proxy/tutorial002.py!} +``` + +🚶‍♀️ `root_path` `FastAPI` 🔜 🌓 🚶‍♀️ `--root-path` 📋 ⏸ 🎛 Uvicorn ⚖️ Hypercorn. + +### 🔃 `root_path` + +✔️ 🤯 👈 💽 (Uvicorn) 🏆 🚫 ⚙️ 👈 `root_path` 🕳 🙆 🌘 🚶‍♀️ ⚫️ 📱. + +✋️ 🚥 👆 🚶 ⏮️ 👆 🖥 http://127.0.0.1:8000/app 👆 🔜 👀 😐 📨: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +, ⚫️ 🏆 🚫 ⌛ 🔐 `http://127.0.0.1:8000/api/v1/app`. + +Uvicorn 🔜 ⌛ 🗳 🔐 Uvicorn `http://127.0.0.1:8000/app`, & ⤴️ ⚫️ 🔜 🗳 🎯 🚮 ➕ `/api/v1` 🔡 🔛 🔝. + +## 🔃 🗳 ⏮️ 🎞 ➡ 🔡 + +✔️ 🤯 👈 🗳 ⏮️ 🎞 ➡ 🔡 🕴 1️⃣ 🌌 🔗 ⚫️. + +🎲 📚 💼 🔢 🔜 👈 🗳 🚫 ✔️ 🏚 ➡ 🔡. + +💼 💖 👈 (🍵 🎞 ➡ 🔡), 🗳 🔜 👂 🔛 🕳 💖 `https://myawesomeapp.com`, & ⤴️ 🚥 🖥 🚶 `https://myawesomeapp.com/api/v1/app` & 👆 💽 (✅ Uvicorn) 👂 🔛 `http://127.0.0.1:8000` 🗳 (🍵 🎞 ➡ 🔡) 🔜 🔐 Uvicorn 🎏 ➡: `http://127.0.0.1:8000/api/v1/app`. + +## 🔬 🌐 ⏮️ Traefik + +👆 💪 💪 🏃 🥼 🌐 ⏮️ 🎞 ➡ 🔡 ⚙️ Traefik. + +⏬ Traefik, ⚫️ 👁 💱, 👆 💪 ⚗ 🗜 📁 & 🏃 ⚫️ 🔗 ⚪️➡️ 📶. + +⤴️ ✍ 📁 `traefik.toml` ⏮️: + +```TOML hl_lines="3" +[entryPoints] + [entryPoints.http] + address = ":9999" + +[providers] + [providers.file] + filename = "routes.toml" +``` + +👉 💬 Traefik 👂 🔛 ⛴ 9️⃣9️⃣9️⃣9️⃣ & ⚙️ ➕1️⃣ 📁 `routes.toml`. + +!!! tip + 👥 ⚙️ ⛴ 9️⃣9️⃣9️⃣9️⃣ ↩️ 🐩 🇺🇸🔍 ⛴ 8️⃣0️⃣ 👈 👆 🚫 ✔️ 🏃 ⚫️ ⏮️ 📡 (`sudo`) 😌. + +🔜 ✍ 👈 🎏 📁 `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" +``` + +👉 📁 🔗 Traefik ⚙️ ➡ 🔡 `/api/v1`. + +& ⤴️ ⚫️ 🔜 ❎ 🚮 📨 👆 Uvicorn 🏃‍♂ 🔛 `http://127.0.0.1:8000`. + +🔜 ▶️ Traefik: + +
+ +```console +$ ./traefik --configFile=traefik.toml + +INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml +``` + +
+ +& 🔜 ▶️ 👆 📱 ⏮️ Uvicorn, ⚙️ `--root-path` 🎛: + +
+ +```console +$ uvicorn main:app --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### ✅ 📨 + +🔜, 🚥 👆 🚶 📛 ⏮️ ⛴ Uvicorn: http://127.0.0.1:8000/app, 👆 🔜 👀 😐 📨: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +!!! tip + 👀 👈 ✋️ 👆 🔐 ⚫️ `http://127.0.0.1:8000/app` ⚫️ 🎦 `root_path` `/api/v1`, ✊ ⚪️➡️ 🎛 `--root-path`. + +& 🔜 📂 📛 ⏮️ ⛴ Traefik, ✅ ➡ 🔡: http://127.0.0.1:9999/api/v1/app. + +👥 🤚 🎏 📨: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +✋️ 👉 🕰 📛 ⏮️ 🔡 ➡ 🚚 🗳: `/api/v1`. + +↗️, 💭 📥 👈 👱 🔜 🔐 📱 🔘 🗳, ⏬ ⏮️ ➡ 🔡 `/app/v1` "☑" 1️⃣. + +& ⏬ 🍵 ➡ 🔡 (`http://127.0.0.1:8000/app`), 🚚 Uvicorn 🔗, 🔜 🎯 _🗳_ (Traefik) 🔐 ⚫️. + +👈 🎦 ❔ 🗳 (Traefik) ⚙️ ➡ 🔡 & ❔ 💽 (Uvicorn) ⚙️ `root_path` ⚪️➡️ 🎛 `--root-path`. + +### ✅ 🩺 🎚 + +✋️ 📥 🎊 🍕. 👶 + +"🛂" 🌌 🔐 📱 🔜 🔘 🗳 ⏮️ ➡ 🔡 👈 👥 🔬. , 👥 🔜 ⌛, 🚥 👆 🔄 🩺 🎚 🍦 Uvicorn 🔗, 🍵 ➡ 🔡 📛, ⚫️ 🏆 🚫 👷, ↩️ ⚫️ ⌛ 🔐 🔘 🗳. + +👆 💪 ✅ ⚫️ http://127.0.0.1:8000/docs: + + + +✋️ 🚥 👥 🔐 🩺 🎚 "🛂" 📛 ⚙️ 🗳 ⏮️ ⛴ `9999`, `/api/v1/docs`, ⚫️ 👷 ☑ ❗ 👶 + +👆 💪 ✅ ⚫️ http://127.0.0.1:9999/api/v1/docs: + + + +▶️️ 👥 💚 ⚫️. 👶 👶 + +👉 ↩️ FastAPI ⚙️ 👉 `root_path` ✍ 🔢 `server` 🗄 ⏮️ 📛 🚚 `root_path`. + +## 🌖 💽 + +!!! warning + 👉 🌅 🏧 ⚙️ 💼. 💭 🆓 🚶 ⚫️. + +🔢, **FastAPI** 🔜 ✍ `server` 🗄 🔗 ⏮️ 📛 `root_path`. + +✋️ 👆 💪 🚚 🎏 🎛 `servers`, 🖼 🚥 👆 💚 *🎏* 🩺 🎚 🔗 ⏮️ 🏗 & 🏭 🌐. + +🚥 👆 🚶‍♀️ 🛃 📇 `servers` & 📤 `root_path` (↩️ 👆 🛠️ 👨‍❤‍👨 ⛅ 🗳), **FastAPI** 🔜 📩 "💽" ⏮️ 👉 `root_path` ▶️ 📇. + +🖼: + +```Python hl_lines="4-7" +{!../../../docs_src/behind_a_proxy/tutorial003.py!} +``` + +🔜 🏗 🗄 🔗 💖: + +```JSON hl_lines="5-7" +{ + "openapi": "3.0.2", + // More stuff here + "servers": [ + { + "url": "/api/v1" + }, + { + "url": "https://stag.example.com", + "description": "Staging environment" + }, + { + "url": "https://prod.example.com", + "description": "Production environment" + } + ], + "paths": { + // More stuff here + } +} +``` + +!!! tip + 👀 🚘-🏗 💽 ⏮️ `url` 💲 `/api/v1`, ✊ ⚪️➡️ `root_path`. + +🩺 🎚 http://127.0.0.1:9999/api/v1/docs ⚫️ 🔜 👀 💖: + + + +!!! tip + 🩺 🎚 🔜 🔗 ⏮️ 💽 👈 👆 🖊. + +### ❎ 🏧 💽 ⚪️➡️ `root_path` + +🚥 👆 🚫 💚 **FastAPI** 🔌 🏧 💽 ⚙️ `root_path`, 👆 💪 ⚙️ 🔢 `root_path_in_servers=False`: + +```Python hl_lines="9" +{!../../../docs_src/behind_a_proxy/tutorial004.py!} +``` + +& ⤴️ ⚫️ 🏆 🚫 🔌 ⚫️ 🗄 🔗. + +## 🗜 🎧-🈸 + +🚥 👆 💪 🗻 🎧-🈸 (🔬 [🎧 🈸 - 🗻](./sub-applications.md){.internal-link target=_blank}) ⏪ ⚙️ 🗳 ⏮️ `root_path`, 👆 💪 ⚫️ 🛎, 👆 🔜 ⌛. + +FastAPI 🔜 🔘 ⚙️ `root_path` 🎆, ⚫️ 🔜 👷. 👶 diff --git a/docs/em/docs/advanced/custom-response.md b/docs/em/docs/advanced/custom-response.md new file mode 100644 index 0000000000..cf76c01d06 --- /dev/null +++ b/docs/em/docs/advanced/custom-response.md @@ -0,0 +1,300 @@ +# 🛃 📨 - 🕸, 🎏, 📁, 🎏 + +🔢, **FastAPI** 🔜 📨 📨 ⚙️ `JSONResponse`. + +👆 💪 🔐 ⚫️ 🛬 `Response` 🔗 👀 [📨 📨 🔗](response-directly.md){.internal-link target=_blank}. + +✋️ 🚥 👆 📨 `Response` 🔗, 📊 🏆 🚫 🔁 🗜, & 🧾 🏆 🚫 🔁 🏗 (🖼, 🔌 🎯 "📻 🆎", 🇺🇸🔍 🎚 `Content-Type` 🍕 🏗 🗄). + +✋️ 👆 💪 📣 `Response` 👈 👆 💚 ⚙️, *➡ 🛠️ 👨‍🎨*. + +🎚 👈 👆 📨 ⚪️➡️ 👆 *➡ 🛠️ 🔢* 🔜 🚮 🔘 👈 `Response`. + +& 🚥 👈 `Response` ✔️ 🎻 📻 🆎 (`application/json`), 💖 💼 ⏮️ `JSONResponse` & `UJSONResponse`, 💽 👆 📨 🔜 🔁 🗜 (& ⛽) ⏮️ 🙆 Pydantic `response_model` 👈 👆 📣 *➡ 🛠️ 👨‍🎨*. + +!!! note + 🚥 👆 ⚙️ 📨 🎓 ⏮️ 🙅‍♂ 📻 🆎, FastAPI 🔜 ⌛ 👆 📨 ✔️ 🙅‍♂ 🎚, ⚫️ 🔜 🚫 📄 📨 📁 🚮 🏗 🗄 🩺. + +## ⚙️ `ORJSONResponse` + +🖼, 🚥 👆 ✊ 🎭, 👆 💪 ❎ & ⚙️ `orjson` & ⚒ 📨 `ORJSONResponse`. + +🗄 `Response` 🎓 (🎧-🎓) 👆 💚 ⚙️ & 📣 ⚫️ *➡ 🛠️ 👨‍🎨*. + +⭕ 📨, 📨 `Response` 🔗 🌅 ⏩ 🌘 🛬 📖. + +👉 ↩️ 🔢, FastAPI 🔜 ✔ 🔠 🏬 🔘 & ⚒ 💭 ⚫️ 🎻 ⏮️ 🎻, ⚙️ 🎏 [🎻 🔗 🔢](../tutorial/encoder.md){.internal-link target=_blank} 🔬 🔰. 👉 ⚫️❔ ✔ 👆 📨 **❌ 🎚**, 🖼 💽 🏷. + +✋️ 🚥 👆 🎯 👈 🎚 👈 👆 🛬 **🎻 ⏮️ 🎻**, 👆 💪 🚶‍♀️ ⚫️ 🔗 📨 🎓 & ❎ ➕ 🌥 👈 FastAPI 🔜 ✔️ 🚶‍♀️ 👆 📨 🎚 🔘 `jsonable_encoder` ⏭ 🚶‍♀️ ⚫️ 📨 🎓. + +```Python hl_lines="2 7" +{!../../../docs_src/custom_response/tutorial001b.py!} +``` + +!!! info + 🔢 `response_class` 🔜 ⚙️ 🔬 "📻 🆎" 📨. + + 👉 💼, 🇺🇸🔍 🎚 `Content-Type` 🔜 ⚒ `application/json`. + + & ⚫️ 🔜 📄 ✅ 🗄. + +!!! tip + `ORJSONResponse` ⏳ 🕴 💪 FastAPI, 🚫 💃. + +## 🕸 📨 + +📨 📨 ⏮️ 🕸 🔗 ⚪️➡️ **FastAPI**, ⚙️ `HTMLResponse`. + +* 🗄 `HTMLResponse`. +* 🚶‍♀️ `HTMLResponse` 🔢 `response_class` 👆 *➡ 🛠️ 👨‍🎨*. + +```Python hl_lines="2 7" +{!../../../docs_src/custom_response/tutorial002.py!} +``` + +!!! info + 🔢 `response_class` 🔜 ⚙️ 🔬 "📻 🆎" 📨. + + 👉 💼, 🇺🇸🔍 🎚 `Content-Type` 🔜 ⚒ `text/html`. + + & ⚫️ 🔜 📄 ✅ 🗄. + +### 📨 `Response` + +👀 [📨 📨 🔗](response-directly.md){.internal-link target=_blank}, 👆 💪 🔐 📨 🔗 👆 *➡ 🛠️*, 🛬 ⚫️. + +🎏 🖼 ⚪️➡️ 🔛, 🛬 `HTMLResponse`, 💪 👀 💖: + +```Python hl_lines="2 7 19" +{!../../../docs_src/custom_response/tutorial003.py!} +``` + +!!! warning + `Response` 📨 🔗 👆 *➡ 🛠️ 🔢* 🏆 🚫 📄 🗄 (🖼, `Content-Type` 🏆 🚫 📄) & 🏆 🚫 ⭐ 🏧 🎓 🩺. + +!!! info + ↗️, ☑ `Content-Type` 🎚, 👔 📟, ♒️, 🔜 👟 ⚪️➡️ `Response` 🎚 👆 📨. + +### 📄 🗄 & 🔐 `Response` + +🚥 👆 💚 🔐 📨 ⚪️➡️ 🔘 🔢 ✋️ 🎏 🕰 📄 "📻 🆎" 🗄, 👆 💪 ⚙️ `response_class` 🔢 & 📨 `Response` 🎚. + +`response_class` 🔜 ⤴️ ⚙️ 🕴 📄 🗄 *➡ 🛠️*, ✋️ 👆 `Response` 🔜 ⚙️. + +#### 📨 `HTMLResponse` 🔗 + +🖼, ⚫️ 💪 🕳 💖: + +```Python hl_lines="7 21 23" +{!../../../docs_src/custom_response/tutorial004.py!} +``` + +👉 🖼, 🔢 `generate_html_response()` ⏪ 🏗 & 📨 `Response` ↩️ 🛬 🕸 `str`. + +🛬 🏁 🤙 `generate_html_response()`, 👆 ⏪ 🛬 `Response` 👈 🔜 🔐 🔢 **FastAPI** 🎭. + +✋️ 👆 🚶‍♀️ `HTMLResponse` `response_class` 💁‍♂️, **FastAPI** 🔜 💭 ❔ 📄 ⚫️ 🗄 & 🎓 🩺 🕸 ⏮️ `text/html`: + + + +## 💪 📨 + +📥 💪 📨. + +✔️ 🤯 👈 👆 💪 ⚙️ `Response` 📨 🕳 🙆, ⚖️ ✍ 🛃 🎧-🎓. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import HTMLResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. + +### `Response` + +👑 `Response` 🎓, 🌐 🎏 📨 😖 ⚪️➡️ ⚫️. + +👆 💪 📨 ⚫️ 🔗. + +⚫️ 🚫 📄 🔢: + +* `content` - `str` ⚖️ `bytes`. +* `status_code` - `int` 🇺🇸🔍 👔 📟. +* `headers` - `dict` 🎻. +* `media_type` - `str` 🤝 📻 🆎. 🤶 Ⓜ. `"text/html"`. + +FastAPI (🤙 💃) 🔜 🔁 🔌 🎚-📐 🎚. ⚫️ 🔜 🔌 🎚-🆎 🎚, ⚓️ 🔛 = & 🔁 = ✍ 🆎. + +```Python hl_lines="1 18" +{!../../../docs_src/response_directly/tutorial002.py!} +``` + +### `HTMLResponse` + +✊ ✍ ⚖️ 🔢 & 📨 🕸 📨, 👆 ✍ 🔛. + +### `PlainTextResponse` + +✊ ✍ ⚖️ 🔢 & 📨 ✅ ✍ 📨. + +```Python hl_lines="2 7 9" +{!../../../docs_src/custom_response/tutorial005.py!} +``` + +### `JSONResponse` + +✊ 💽 & 📨 `application/json` 🗜 📨. + +👉 🔢 📨 ⚙️ **FastAPI**, 👆 ✍ 🔛. + +### `ORJSONResponse` + +⏩ 🎛 🎻 📨 ⚙️ `orjson`, 👆 ✍ 🔛. + +### `UJSONResponse` + +🎛 🎻 📨 ⚙️ `ujson`. + +!!! warning + `ujson` 🌘 💛 🌘 🐍 🏗-🛠️ ❔ ⚫️ 🍵 📐-💼. + +```Python hl_lines="2 7" +{!../../../docs_src/custom_response/tutorial001.py!} +``` + +!!! tip + ⚫️ 💪 👈 `ORJSONResponse` 💪 ⏩ 🎛. + +### `RedirectResponse` + +📨 🇺🇸🔍 ❎. ⚙️ 3️⃣0️⃣7️⃣ 👔 📟 (🍕 ❎) 🔢. + +👆 💪 📨 `RedirectResponse` 🔗: + +```Python hl_lines="2 9" +{!../../../docs_src/custom_response/tutorial006.py!} +``` + +--- + +⚖️ 👆 💪 ⚙️ ⚫️ `response_class` 🔢: + + +```Python hl_lines="2 7 9" +{!../../../docs_src/custom_response/tutorial006b.py!} +``` + +🚥 👆 👈, ⤴️ 👆 💪 📨 📛 🔗 ⚪️➡️ 👆 *➡ 🛠️* 🔢. + +👉 💼, `status_code` ⚙️ 🔜 🔢 1️⃣ `RedirectResponse`, ❔ `307`. + +--- + +👆 💪 ⚙️ `status_code` 🔢 🌀 ⏮️ `response_class` 🔢: + +```Python hl_lines="2 7 9" +{!../../../docs_src/custom_response/tutorial006c.py!} +``` + +### `StreamingResponse` + +✊ 🔁 🚂 ⚖️ 😐 🚂/🎻 & 🎏 📨 💪. + +```Python hl_lines="2 14" +{!../../../docs_src/custom_response/tutorial007.py!} +``` + +#### ⚙️ `StreamingResponse` ⏮️ 📁-💖 🎚 + +🚥 👆 ✔️ 📁-💖 🎚 (✅ 🎚 📨 `open()`), 👆 💪 ✍ 🚂 🔢 🔁 🤭 👈 📁-💖 🎚. + +👈 🌌, 👆 🚫 ✔️ ✍ ⚫️ 🌐 🥇 💾, & 👆 💪 🚶‍♀️ 👈 🚂 🔢 `StreamingResponse`, & 📨 ⚫️. + +👉 🔌 📚 🗃 🔗 ⏮️ ☁ 💾, 📹 🏭, & 🎏. + +```{ .python .annotate hl_lines="2 10-12 14" } +{!../../../docs_src/custom_response/tutorial008.py!} +``` + +1️⃣. 👉 🚂 🔢. ⚫️ "🚂 🔢" ↩️ ⚫️ 🔌 `yield` 📄 🔘. +2️⃣. ⚙️ `with` 🍫, 👥 ⚒ 💭 👈 📁-💖 🎚 📪 ⏮️ 🚂 🔢 🔨. , ⏮️ ⚫️ 🏁 📨 📨. +3️⃣. 👉 `yield from` 💬 🔢 🔁 🤭 👈 👜 🌟 `file_like`. & ⤴️, 🔠 🍕 🔁, 🌾 👈 🍕 👟 ⚪️➡️ 👉 🚂 🔢. + + , ⚫️ 🚂 🔢 👈 📨 "🏭" 👷 🕳 🙆 🔘. + + 🔨 ⚫️ 👉 🌌, 👥 💪 🚮 ⚫️ `with` 🍫, & 👈 🌌, 🚚 👈 ⚫️ 📪 ⏮️ 🏁. + +!!! tip + 👀 👈 📥 👥 ⚙️ 🐩 `open()` 👈 🚫 🐕‍🦺 `async` & `await`, 👥 📣 ➡ 🛠️ ⏮️ 😐 `def`. + +### `FileResponse` + +🔁 🎏 📁 📨. + +✊ 🎏 ⚒ ❌ 🔗 🌘 🎏 📨 🆎: + +* `path` - 📁 📁 🎏. +* `headers` - 🙆 🛃 🎚 🔌, 📖. +* `media_type` - 🎻 🤝 📻 🆎. 🚥 🔢, 📁 ⚖️ ➡ 🔜 ⚙️ 🔑 📻 🆎. +* `filename` - 🚥 ⚒, 👉 🔜 🔌 📨 `Content-Disposition`. + +📁 📨 🔜 🔌 ☑ `Content-Length`, `Last-Modified` & `ETag` 🎚. + +```Python hl_lines="2 10" +{!../../../docs_src/custom_response/tutorial009.py!} +``` + +👆 💪 ⚙️ `response_class` 🔢: + +```Python hl_lines="2 8 10" +{!../../../docs_src/custom_response/tutorial009b.py!} +``` + +👉 💼, 👆 💪 📨 📁 ➡ 🔗 ⚪️➡️ 👆 *➡ 🛠️* 🔢. + +## 🛃 📨 🎓 + +👆 💪 ✍ 👆 👍 🛃 📨 🎓, 😖 ⚪️➡️ `Response` & ⚙️ ⚫️. + +🖼, ➡️ 💬 👈 👆 💚 ⚙️ `orjson`, ✋️ ⏮️ 🛃 ⚒ 🚫 ⚙️ 🔌 `ORJSONResponse` 🎓. + +➡️ 💬 👆 💚 ⚫️ 📨 🔂 & 📁 🎻, 👆 💚 ⚙️ Orjson 🎛 `orjson.OPT_INDENT_2`. + +👆 💪 ✍ `CustomORJSONResponse`. 👑 👜 👆 ✔️ ✍ `Response.render(content)` 👩‍🔬 👈 📨 🎚 `bytes`: + +```Python hl_lines="9-14 17" +{!../../../docs_src/custom_response/tutorial009c.py!} +``` + +🔜 ↩️ 🛬: + +```json +{"message": "Hello World"} +``` + +...👉 📨 🔜 📨: + +```json +{ + "message": "Hello World" +} +``` + +↗️, 👆 🔜 🎲 🔎 🌅 👍 🌌 ✊ 📈 👉 🌘 ❕ 🎻. 👶 + +## 🔢 📨 🎓 + +🕐❔ 🏗 **FastAPI** 🎓 👐 ⚖️ `APIRouter` 👆 💪 ✔ ❔ 📨 🎓 ⚙️ 🔢. + +🔢 👈 🔬 👉 `default_response_class`. + +🖼 🔛, **FastAPI** 🔜 ⚙️ `ORJSONResponse` 🔢, 🌐 *➡ 🛠️*, ↩️ `JSONResponse`. + +```Python hl_lines="2 4" +{!../../../docs_src/custom_response/tutorial010.py!} +``` + +!!! tip + 👆 💪 🔐 `response_class` *➡ 🛠️* ⏭. + +## 🌖 🧾 + +👆 💪 📣 📻 🆎 & 📚 🎏 ℹ 🗄 ⚙️ `responses`: [🌖 📨 🗄](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/em/docs/advanced/dataclasses.md b/docs/em/docs/advanced/dataclasses.md new file mode 100644 index 0000000000..a4c2871062 --- /dev/null +++ b/docs/em/docs/advanced/dataclasses.md @@ -0,0 +1,98 @@ +# ⚙️ 🎻 + +FastAPI 🏗 🔛 🔝 **Pydantic**, & 👤 ✔️ 🌏 👆 ❔ ⚙️ Pydantic 🏷 📣 📨 & 📨. + +✋️ FastAPI 🐕‍🦺 ⚙️ `dataclasses` 🎏 🌌: + +```Python hl_lines="1 7-12 19-20" +{!../../../docs_src/dataclasses/tutorial001.py!} +``` + +👉 🐕‍🦺 👏 **Pydantic**, ⚫️ ✔️ 🔗 🐕‍🦺 `dataclasses`. + +, ⏮️ 📟 🔛 👈 🚫 ⚙️ Pydantic 🎯, FastAPI ⚙️ Pydantic 🗜 📚 🐩 🎻 Pydantic 👍 🍛 🎻. + +& ↗️, ⚫️ 🐕‍🦺 🎏: + +* 💽 🔬 +* 💽 🛠️ +* 💽 🧾, ♒️. + +👉 👷 🎏 🌌 ⏮️ Pydantic 🏷. & ⚫️ 🤙 🏆 🎏 🌌 🔘, ⚙️ Pydantic. + +!!! info + ✔️ 🤯 👈 🎻 💪 🚫 🌐 Pydantic 🏷 💪. + + , 👆 5️⃣📆 💪 ⚙️ Pydantic 🏷. + + ✋️ 🚥 👆 ✔️ 📚 🎻 🤥 🤭, 👉 👌 🎱 ⚙️ 👫 🏋️ 🕸 🛠️ ⚙️ FastAPI. 👶 + +## 🎻 `response_model` + +👆 💪 ⚙️ `dataclasses` `response_model` 🔢: + +```Python hl_lines="1 7-13 19" +{!../../../docs_src/dataclasses/tutorial002.py!} +``` + +🎻 🔜 🔁 🗜 Pydantic 🎻. + +👉 🌌, 🚮 🔗 🔜 🎦 🆙 🛠️ 🩺 👩‍💻 🔢: + + + +## 🎻 🔁 📊 📊 + +👆 💪 🌀 `dataclasses` ⏮️ 🎏 🆎 ✍ ⚒ 🐦 📊 📊. + +💼, 👆 💪 ✔️ ⚙️ Pydantic ⏬ `dataclasses`. 🖼, 🚥 👆 ✔️ ❌ ⏮️ 🔁 🏗 🛠️ 🧾. + +👈 💼, 👆 💪 🎯 💱 🐩 `dataclasses` ⏮️ `pydantic.dataclasses`, ❔ 💧-♻: + +```{ .python .annotate hl_lines="1 5 8-11 14-17 23-25 28" } +{!../../../docs_src/dataclasses/tutorial003.py!} +``` + +1️⃣. 👥 🗄 `field` ⚪️➡️ 🐩 `dataclasses`. + +2️⃣. `pydantic.dataclasses` 💧-♻ `dataclasses`. + +3️⃣. `Author` 🎻 🔌 📇 `Item` 🎻. + +4️⃣. `Author` 🎻 ⚙️ `response_model` 🔢. + +5️⃣. 👆 💪 ⚙️ 🎏 🐩 🆎 ✍ ⏮️ 🎻 📨 💪. + + 👉 💼, ⚫️ 📇 `Item` 🎻. + +6️⃣. 📥 👥 🛬 📖 👈 🔌 `items` ❔ 📇 🎻. + + FastAPI 🎯 💽 🎻. + +7️⃣. 📥 `response_model` ⚙️ 🆎 ✍ 📇 `Author` 🎻. + + 🔄, 👆 💪 🌀 `dataclasses` ⏮️ 🐩 🆎 ✍. + +8️⃣. 👀 👈 👉 *➡ 🛠️ 🔢* ⚙️ 🥔 `def` ↩️ `async def`. + + 🕧, FastAPI 👆 💪 🌀 `def` & `async def` 💪. + + 🚥 👆 💪 ↗️ 🔃 🕐❔ ⚙️ ❔, ✅ 👅 📄 _"🏃 ❓" _ 🩺 🔃 `async` & `await`. + +9️⃣. 👉 *➡ 🛠️ 🔢* 🚫 🛬 🎻 (👐 ⚫️ 💪), ✋️ 📇 📖 ⏮️ 🔗 💽. + + FastAPI 🔜 ⚙️ `response_model` 🔢 (👈 🔌 🎻) 🗜 📨. + +👆 💪 🌀 `dataclasses` ⏮️ 🎏 🆎 ✍ 📚 🎏 🌀 📨 🏗 📊 📊. + +✅-📟 ✍ 💁‍♂ 🔛 👀 🌅 🎯 ℹ. + +## 💡 🌅 + +👆 💪 🌀 `dataclasses` ⏮️ 🎏 Pydantic 🏷, 😖 ⚪️➡️ 👫, 🔌 👫 👆 👍 🏷, ♒️. + +💡 🌅, ✅ Pydantic 🩺 🔃 🎻. + +## ⏬ + +👉 💪 ↩️ FastAPI ⏬ `0.67.0`. 👶 diff --git a/docs/em/docs/advanced/events.md b/docs/em/docs/advanced/events.md new file mode 100644 index 0000000000..671e81b186 --- /dev/null +++ b/docs/em/docs/advanced/events.md @@ -0,0 +1,160 @@ +# 🔆 🎉 + +👆 💪 🔬 ⚛ (📟) 👈 🔜 🛠️ ⏭ 🈸 **▶️ 🆙**. 👉 ⛓ 👈 👉 📟 🔜 🛠️ **🕐**, **⏭** 🈸 **▶️ 📨 📨**. + +🎏 🌌, 👆 💪 🔬 ⚛ (📟) 👈 🔜 🛠️ 🕐❔ 🈸 **🤫 🔽**. 👉 💼, 👉 📟 🔜 🛠️ **🕐**, **⏮️** ✔️ 🍵 🎲 **📚 📨**. + +↩️ 👉 📟 🛠️ ⏭ 🈸 **▶️** ✊ 📨, & ▶️️ ⏮️ ⚫️ **🏁** 🚚 📨, ⚫️ 📔 🎂 🈸 **🔆** (🔤 "🔆" 🔜 ⚠ 🥈 👶). + +👉 💪 📶 ⚠ ⚒ 🆙 **ℹ** 👈 👆 💪 ⚙️ 🎂 📱, & 👈 **💰** 👪 📨, &/⚖️ 👈 👆 💪 **🧹 🆙** ⏮️. 🖼, 💽 🔗 🎱, ⚖️ 🚚 🔗 🎰 🏫 🏷. + +## ⚙️ 💼 + +➡️ ▶️ ⏮️ 🖼 **⚙️ 💼** & ⤴️ 👀 ❔ ❎ ⚫️ ⏮️ 👉. + +➡️ 🌈 👈 👆 ✔️ **🎰 🏫 🏷** 👈 👆 💚 ⚙️ 🍵 📨. 👶 + +🎏 🏷 🔗 👪 📨,, ⚫️ 🚫 1️⃣ 🏷 📍 📨, ⚖️ 1️⃣ 📍 👩‍💻 ⚖️ 🕳 🎏. + +➡️ 🌈 👈 🚚 🏷 💪 **✊ 🕰**, ↩️ ⚫️ ✔️ ✍ 📚 **💽 ⚪️➡️ 💾**. 👆 🚫 💚 ⚫️ 🔠 📨. + +👆 💪 📐 ⚫️ 🔝 🎚 🕹/📁, ✋️ 👈 🔜 ⛓ 👈 ⚫️ 🔜 **📐 🏷** 🚥 👆 🏃‍♂ 🙅 🏧 💯, ⤴️ 👈 💯 🔜 **🐌** ↩️ ⚫️ 🔜 ✔️ ⌛ 🏷 📐 ⏭ 💆‍♂ 💪 🏃 🔬 🍕 📟. + +👈 ⚫️❔ 👥 🔜 ❎, ➡️ 📐 🏷 ⏭ 📨 🍵, ✋️ 🕴 ▶️️ ⏭ 🈸 ▶️ 📨 📨, 🚫 ⏪ 📟 ➖ 📐. + +## 🔆 + +👆 💪 🔬 👉 *🕴* & *🤫* ⚛ ⚙️ `lifespan` 🔢 `FastAPI` 📱, & "🔑 👨‍💼" (👤 🔜 🎦 👆 ⚫️❔ 👈 🥈). + +➡️ ▶️ ⏮️ 🖼 & ⤴️ 👀 ⚫️ ℹ. + +👥 ✍ 🔁 🔢 `lifespan()` ⏮️ `yield` 💖 👉: + +```Python hl_lines="16 19" +{!../../../docs_src/events/tutorial003.py!} +``` + +📥 👥 ⚖ 😥 *🕴* 🛠️ 🚚 🏷 🚮 (❌) 🏷 🔢 📖 ⏮️ 🎰 🏫 🏷 ⏭ `yield`. 👉 📟 🔜 🛠️ **⏭** 🈸 **▶️ ✊ 📨**, ⏮️ *🕴*. + +& ⤴️, ▶️️ ⏮️ `yield`, 👥 🚚 🏷. 👉 📟 🔜 🛠️ **⏮️** 🈸 **🏁 🚚 📨**, ▶️️ ⏭ *🤫*. 👉 💪, 🖼, 🚀 ℹ 💖 💾 ⚖️ 💻. + +!!! tip + `shutdown` 🔜 🔨 🕐❔ 👆 **⛔️** 🈸. + + 🎲 👆 💪 ▶️ 🆕 ⏬, ⚖️ 👆 🤚 🎡 🏃 ⚫️. 🤷 + +### 🔆 🔢 + +🥇 👜 👀, 👈 👥 ⚖ 🔁 🔢 ⏮️ `yield`. 👉 📶 🎏 🔗 ⏮️ `yield`. + +```Python hl_lines="14-19" +{!../../../docs_src/events/tutorial003.py!} +``` + +🥇 🍕 🔢, ⏭ `yield`, 🔜 🛠️ **⏭** 🈸 ▶️. + +& 🍕 ⏮️ `yield` 🔜 🛠️ **⏮️** 🈸 ✔️ 🏁. + +### 🔁 🔑 👨‍💼 + +🚥 👆 ✅, 🔢 🎀 ⏮️ `@asynccontextmanager`. + +👈 🗜 🔢 🔘 🕳 🤙 "**🔁 🔑 👨‍💼**". + +```Python hl_lines="1 13" +{!../../../docs_src/events/tutorial003.py!} +``` + +**🔑 👨‍💼** 🐍 🕳 👈 👆 💪 ⚙️ `with` 📄, 🖼, `open()` 💪 ⚙️ 🔑 👨‍💼: + +```Python +with open("file.txt") as file: + file.read() +``` + +⏮️ ⏬ 🐍, 📤 **🔁 🔑 👨‍💼**. 👆 🔜 ⚙️ ⚫️ ⏮️ `async with`: + +```Python +async with lifespan(app): + await do_stuff() +``` + +🕐❔ 👆 ✍ 🔑 👨‍💼 ⚖️ 🔁 🔑 👨‍💼 💖 🔛, ⚫️❔ ⚫️ 🔨 👈, ⏭ 🛬 `with` 🍫, ⚫️ 🔜 🛠️ 📟 ⏭ `yield`, & ⏮️ ❎ `with` 🍫, ⚫️ 🔜 🛠️ 📟 ⏮️ `yield`. + +👆 📟 🖼 🔛, 👥 🚫 ⚙️ ⚫️ 🔗, ✋️ 👥 🚶‍♀️ ⚫️ FastAPI ⚫️ ⚙️ ⚫️. + +`lifespan` 🔢 `FastAPI` 📱 ✊ **🔁 🔑 👨‍💼**, 👥 💪 🚶‍♀️ 👆 🆕 `lifespan` 🔁 🔑 👨‍💼 ⚫️. + +```Python hl_lines="22" +{!../../../docs_src/events/tutorial003.py!} +``` + +## 🎛 🎉 (😢) + +!!! warning + 👍 🌌 🍵 *🕴* & *🤫* ⚙️ `lifespan` 🔢 `FastAPI` 📱 🔬 🔛. + + 👆 💪 🎲 🚶 👉 🍕. + +📤 🎛 🌌 🔬 👉 ⚛ 🛠️ ⏮️ *🕴* & ⏮️ *🤫*. + +👆 💪 🔬 🎉 🐕‍🦺 (🔢) 👈 💪 🛠️ ⏭ 🈸 ▶️ 🆙, ⚖️ 🕐❔ 🈸 🤫 🔽. + +👫 🔢 💪 📣 ⏮️ `async def` ⚖️ 😐 `def`. + +### `startup` 🎉 + +🚮 🔢 👈 🔜 🏃 ⏭ 🈸 ▶️, 📣 ⚫️ ⏮️ 🎉 `"startup"`: + +```Python hl_lines="8" +{!../../../docs_src/events/tutorial001.py!} +``` + +👉 💼, `startup` 🎉 🐕‍🦺 🔢 🔜 🔢 🏬 "💽" ( `dict`) ⏮️ 💲. + +👆 💪 🚮 🌅 🌘 1️⃣ 🎉 🐕‍🦺 🔢. + +& 👆 🈸 🏆 🚫 ▶️ 📨 📨 ⏭ 🌐 `startup` 🎉 🐕‍🦺 ✔️ 🏁. + +### `shutdown` 🎉 + +🚮 🔢 👈 🔜 🏃 🕐❔ 🈸 🤫 🔽, 📣 ⚫️ ⏮️ 🎉 `"shutdown"`: + +```Python hl_lines="6" +{!../../../docs_src/events/tutorial002.py!} +``` + +📥, `shutdown` 🎉 🐕‍🦺 🔢 🔜 ✍ ✍ ⏸ `"Application shutdown"` 📁 `log.txt`. + +!!! info + `open()` 🔢, `mode="a"` ⛓ "🎻",, ⏸ 🔜 🚮 ⏮️ ⚫️❔ 🔛 👈 📁, 🍵 📁 ⏮️ 🎚. + +!!! tip + 👀 👈 👉 💼 👥 ⚙️ 🐩 🐍 `open()` 🔢 👈 🔗 ⏮️ 📁. + + , ⚫️ 🔌 👤/🅾 (🔢/🔢), 👈 🚚 "⌛" 👜 ✍ 💾. + + ✋️ `open()` 🚫 ⚙️ `async` & `await`. + + , 👥 📣 🎉 🐕‍🦺 🔢 ⏮️ 🐩 `def` ↩️ `async def`. + +!!! info + 👆 💪 ✍ 🌅 🔃 👫 🎉 🐕‍🦺 💃 🎉' 🩺. + +### `startup` & `shutdown` 👯‍♂️ + +📤 ↕ 🤞 👈 ⚛ 👆 *🕴* & *🤫* 🔗, 👆 💪 💚 ▶️ 🕳 & ⤴️ 🏁 ⚫️, 📎 ℹ & ⤴️ 🚀 ⚫️, ♒️. + +🔨 👈 👽 🔢 👈 🚫 💰 ⚛ ⚖️ 🔢 👯‍♂️ 🌅 ⚠ 👆 🔜 💪 🏪 💲 🌐 🔢 ⚖️ 🎏 🎱. + +↩️ 👈, ⚫️ 🔜 👍 ↩️ ⚙️ `lifespan` 🔬 🔛. + +## 📡 ℹ + +📡 ℹ 😟 🤓. 👶 + +🔘, 🔫 📡 🔧, 👉 🍕 🔆 🛠️, & ⚫️ 🔬 🎉 🤙 `startup` & `shutdown`. + +## 🎧 🈸 + +👶 ✔️ 🤯 👈 👫 🔆 🎉 (🕴 & 🤫) 🔜 🕴 🛠️ 👑 🈸, 🚫 [🎧 🈸 - 🗻](./sub-applications.md){.internal-link target=_blank}. diff --git a/docs/em/docs/advanced/generate-clients.md b/docs/em/docs/advanced/generate-clients.md new file mode 100644 index 0000000000..30560c8c65 --- /dev/null +++ b/docs/em/docs/advanced/generate-clients.md @@ -0,0 +1,267 @@ +# 🏗 👩‍💻 + +**FastAPI** ⚓️ 🔛 🗄 🔧, 👆 🤚 🏧 🔗 ⏮️ 📚 🧰, 🔌 🏧 🛠️ 🩺 (🚚 🦁 🎚). + +1️⃣ 🎯 📈 👈 🚫 🎯 ⭐ 👈 👆 💪 **🏗 👩‍💻** (🕣 🤙 **📱** ) 👆 🛠️, 📚 🎏 **🛠️ 🇪🇸**. + +## 🗄 👩‍💻 🚂 + +📤 📚 🧰 🏗 👩‍💻 ⚪️➡️ **🗄**. + +⚠ 🧰 🗄 🚂. + +🚥 👆 🏗 **🕸**, 📶 😌 🎛 🗄-📕-🇦🇪. + +## 🏗 📕 🕸 👩‍💻 + +➡️ ▶️ ⏮️ 🙅 FastAPI 🈸: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9-11 14-15 18 19 23" + {!> ../../../docs_src/generate_clients/tutorial001.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="7-9 12-13 16-17 21" + {!> ../../../docs_src/generate_clients/tutorial001_py39.py!} + ``` + +👀 👈 *➡ 🛠️* 🔬 🏷 👫 ⚙️ 📨 🚀 & 📨 🚀, ⚙️ 🏷 `Item` & `ResponseMessage`. + +### 🛠️ 🩺 + +🚥 👆 🚶 🛠️ 🩺, 👆 🔜 👀 👈 ⚫️ ✔️ **🔗** 📊 📨 📨 & 📨 📨: + + + +👆 💪 👀 👈 🔗 ↩️ 👫 📣 ⏮️ 🏷 📱. + +👈 ℹ 💪 📱 **🗄 🔗**, & ⤴️ 🎦 🛠️ 🩺 (🦁 🎚). + +& 👈 🎏 ℹ ⚪️➡️ 🏷 👈 🔌 🗄 ⚫️❔ 💪 ⚙️ **🏗 👩‍💻 📟**. + +### 🏗 📕 👩‍💻 + +🔜 👈 👥 ✔️ 📱 ⏮️ 🏷, 👥 💪 🏗 👩‍💻 📟 🕸. + +#### ❎ `openapi-typescript-codegen` + +👆 💪 ❎ `openapi-typescript-codegen` 👆 🕸 📟 ⏮️: + +
+ +```console +$ npm install openapi-typescript-codegen --save-dev + +---> 100% +``` + +
+ +#### 🏗 👩‍💻 📟 + +🏗 👩‍💻 📟 👆 💪 ⚙️ 📋 ⏸ 🈸 `openapi` 👈 🔜 🔜 ❎. + +↩️ ⚫️ ❎ 🇧🇿 🏗, 👆 🎲 🚫🔜 💪 🤙 👈 📋 🔗, ✋️ 👆 🔜 🚮 ⚫️ 🔛 👆 `package.json` 📁. + +⚫️ 💪 👀 💖 👉: + +```JSON hl_lines="7" +{ + "name": "frontend-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios" + }, + "author": "", + "license": "", + "devDependencies": { + "openapi-typescript-codegen": "^0.20.1", + "typescript": "^4.6.2" + } +} +``` + +⏮️ ✔️ 👈 ☕ `generate-client` ✍ 📤, 👆 💪 🏃 ⚫️ ⏮️: + +
+ +```console +$ npm run generate-client + +frontend-app@1.0.0 generate-client /home/user/code/frontend-app +> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios +``` + +
+ +👈 📋 🔜 🏗 📟 `./src/client` & 🔜 ⚙️ `axios` (🕸 🇺🇸🔍 🗃) 🔘. + +### 🔄 👅 👩‍💻 📟 + +🔜 👆 💪 🗄 & ⚙️ 👩‍💻 📟, ⚫️ 💪 👀 💖 👉, 👀 👈 👆 🤚 ✍ 👩‍🔬: + + + +👆 🔜 🤚 ✍ 🚀 📨: + + + +!!! tip + 👀 ✍ `name` & `price`, 👈 🔬 FastAPI 🈸, `Item` 🏷. + +👆 🔜 ✔️ ⏸ ❌ 📊 👈 👆 📨: + + + +📨 🎚 🔜 ✔️ ✍: + + + +## FastAPI 📱 ⏮️ 🔖 + +📚 💼 👆 FastAPI 📱 🔜 🦏, & 👆 🔜 🎲 ⚙️ 🔖 🎏 🎏 👪 *➡ 🛠️*. + +🖼, 👆 💪 ✔️ 📄 **🏬** & ➕1️⃣ 📄 **👩‍💻**, & 👫 💪 👽 🔖: + + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="23 28 36" + {!> ../../../docs_src/generate_clients/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="21 26 34" + {!> ../../../docs_src/generate_clients/tutorial002_py39.py!} + ``` + +### 🏗 📕 👩‍💻 ⏮️ 🔖 + +🚥 👆 🏗 👩‍💻 FastAPI 📱 ⚙️ 🔖, ⚫️ 🔜 🛎 🎏 👩‍💻 📟 ⚓️ 🔛 🔖. + +👉 🌌 👆 🔜 💪 ✔️ 👜 ✔ & 👪 ☑ 👩‍💻 📟: + + + +👉 💼 👆 ✔️: + +* `ItemsService` +* `UsersService` + +### 👩‍💻 👩‍🔬 📛 + +▶️️ 🔜 🏗 👩‍🔬 📛 💖 `createItemItemsPost` 🚫 👀 📶 🧹: + +```TypeScript +ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) +``` + +...👈 ↩️ 👩‍💻 🚂 ⚙️ 🗄 🔗 **🛠️ 🆔** 🔠 *➡ 🛠️*. + +🗄 🚚 👈 🔠 🛠️ 🆔 😍 🤭 🌐 *➡ 🛠️*, FastAPI ⚙️ **🔢 📛**, **➡**, & **🇺🇸🔍 👩‍🔬/🛠️** 🏗 👈 🛠️ 🆔, ↩️ 👈 🌌 ⚫️ 💪 ⚒ 💭 👈 🛠️ 🆔 😍. + +✋️ 👤 🔜 🎦 👆 ❔ 📉 👈 ⏭. 👶 + +## 🛃 🛠️ 🆔 & 👍 👩‍🔬 📛 + +👆 💪 **🔀** 🌌 👫 🛠️ 🆔 **🏗** ⚒ 👫 🙅 & ✔️ **🙅 👩‍🔬 📛** 👩‍💻. + +👉 💼 👆 🔜 ✔️ 🚚 👈 🔠 🛠️ 🆔 **😍** 🎏 🌌. + +🖼, 👆 💪 ⚒ 💭 👈 🔠 *➡ 🛠️* ✔️ 🔖, & ⤴️ 🏗 🛠️ 🆔 ⚓️ 🔛 **🔖** & *➡ 🛠️* **📛** (🔢 📛). + +### 🛃 🏗 😍 🆔 🔢 + +FastAPI ⚙️ **😍 🆔** 🔠 *➡ 🛠️*, ⚫️ ⚙️ **🛠️ 🆔** & 📛 🙆 💪 🛃 🏷, 📨 ⚖️ 📨. + +👆 💪 🛃 👈 🔢. ⚫️ ✊ `APIRoute` & 🔢 🎻. + +🖼, 📥 ⚫️ ⚙️ 🥇 🔖 (👆 🔜 🎲 ✔️ 🕴 1️⃣ 🔖) & *➡ 🛠️* 📛 (🔢 📛). + +👆 💪 ⤴️ 🚶‍♀️ 👈 🛃 🔢 **FastAPI** `generate_unique_id_function` 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="8-9 12" + {!> ../../../docs_src/generate_clients/tutorial003.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="6-7 10" + {!> ../../../docs_src/generate_clients/tutorial003_py39.py!} + ``` + +### 🏗 📕 👩‍💻 ⏮️ 🛃 🛠️ 🆔 + +🔜 🚥 👆 🏗 👩‍💻 🔄, 👆 🔜 👀 👈 ⚫️ ✔️ 📉 👩‍🔬 📛: + + + +👆 👀, 👩‍🔬 📛 🔜 ✔️ 🔖 & ⤴️ 🔢 📛, 🔜 👫 🚫 🔌 ℹ ⚪️➡️ 📛 ➡ & 🇺🇸🔍 🛠️. + +### 🗜 🗄 🔧 👩‍💻 🚂 + +🏗 📟 ✔️ **❎ ℹ**. + +👥 ⏪ 💭 👈 👉 👩‍🔬 🔗 **🏬** ↩️ 👈 🔤 `ItemsService` (✊ ⚪️➡️ 🔖), ✋️ 👥 ✔️ 📛 🔡 👩‍🔬 📛 💁‍♂️. 👶 + +👥 🔜 🎲 💚 🚧 ⚫️ 🗄 🏢, 👈 🔜 🚚 👈 🛠️ 🆔 **😍**. + +✋️ 🏗 👩‍💻 👥 💪 **🔀** 🗄 🛠️ 🆔 ▶️️ ⏭ 🏭 👩‍💻, ⚒ 👈 👩‍🔬 📛 👌 & **🧹**. + +👥 💪 ⏬ 🗄 🎻 📁 `openapi.json` & ⤴️ 👥 💪 **❎ 👈 🔡 🔖** ⏮️ ✍ 💖 👉: + +```Python +{!../../../docs_src/generate_clients/tutorial004.py!} +``` + +⏮️ 👈, 🛠️ 🆔 🔜 📁 ⚪️➡️ 👜 💖 `items-get_items` `get_items`, 👈 🌌 👩‍💻 🚂 💪 🏗 🙅 👩‍🔬 📛. + +### 🏗 📕 👩‍💻 ⏮️ 🗜 🗄 + +🔜 🔚 🏁 📁 `openapi.json`, 👆 🔜 🔀 `package.json` ⚙️ 👈 🇧🇿 📁, 🖼: + +```JSON hl_lines="7" +{ + "name": "frontend-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "generate-client": "openapi --input ./openapi.json --output ./src/client --client axios" + }, + "author": "", + "license": "", + "devDependencies": { + "openapi-typescript-codegen": "^0.20.1", + "typescript": "^4.6.2" + } +} +``` + +⏮️ 🏭 🆕 👩‍💻, 👆 🔜 🔜 ✔️ **🧹 👩‍🔬 📛**, ⏮️ 🌐 **✍**, **⏸ ❌**, ♒️: + + + +## 💰 + +🕐❔ ⚙️ 🔁 🏗 👩‍💻 👆 🔜 **✍** : + +* 👩‍🔬. +* 📨 🚀 💪, 🔢 🔢, ♒️. +* 📨 🚀. + +👆 🔜 ✔️ **⏸ ❌** 🌐. + +& 🕐❔ 👆 ℹ 👩‍💻 📟, & **♻** 🕸, ⚫️ 🔜 ✔️ 🙆 🆕 *➡ 🛠️* 💪 👩‍🔬, 🗝 🕐 ❎, & 🙆 🎏 🔀 🔜 🎨 🔛 🏗 📟. 👶 + +👉 ⛓ 👈 🚥 🕳 🔀 ⚫️ 🔜 **🎨** 🔛 👩‍💻 📟 🔁. & 🚥 👆 **🏗** 👩‍💻 ⚫️ 🔜 ❌ 👅 🚥 👆 ✔️ 🙆 **🔖** 📊 ⚙️. + +, 👆 🔜 **🔍 📚 ❌** 📶 ⏪ 🛠️ 🛵 ↩️ ✔️ ⌛ ❌ 🎦 🆙 👆 🏁 👩‍💻 🏭 & ⤴️ 🔄 ℹ 🌐❔ ⚠. 👶 diff --git a/docs/em/docs/advanced/index.md b/docs/em/docs/advanced/index.md new file mode 100644 index 0000000000..abe8d357c9 --- /dev/null +++ b/docs/em/docs/advanced/index.md @@ -0,0 +1,24 @@ +# 🏧 👩‍💻 🦮 + +## 🌖 ⚒ + +👑 [🔰 - 👩‍💻 🦮](../tutorial/){.internal-link target=_blank} 🔜 🥃 🤝 👆 🎫 🔘 🌐 👑 ⚒ **FastAPI**. + +⏭ 📄 👆 🔜 👀 🎏 🎛, 📳, & 🌖 ⚒. + +!!! tip + ⏭ 📄 **🚫 🎯 "🏧"**. + + & ⚫️ 💪 👈 👆 ⚙️ 💼, ⚗ 1️⃣ 👫. + +## ✍ 🔰 🥇 + +👆 💪 ⚙️ 🏆 ⚒ **FastAPI** ⏮️ 💡 ⚪️➡️ 👑 [🔰 - 👩‍💻 🦮](../tutorial/){.internal-link target=_blank}. + +& ⏭ 📄 🤔 👆 ⏪ ✍ ⚫️, & 🤔 👈 👆 💭 👈 👑 💭. + +## 🏎.🅾 ↗️ + +🚥 👆 🔜 💖 ✊ 🏧-🔰 ↗️ 🔗 👉 📄 🩺, 👆 💪 💚 ✅: 💯-💾 🛠️ ⏮️ FastAPI & ☁ **🏎.🅾**. + +👫 ⏳ 🩸 1️⃣0️⃣ 💯 🌐 💰 🛠️ **FastAPI**. 👶 👶 diff --git a/docs/em/docs/advanced/middleware.md b/docs/em/docs/advanced/middleware.md new file mode 100644 index 0000000000..b3e722ed08 --- /dev/null +++ b/docs/em/docs/advanced/middleware.md @@ -0,0 +1,99 @@ +# 🏧 🛠️ + +👑 🔰 👆 ✍ ❔ 🚮 [🛃 🛠️](../tutorial/middleware.md){.internal-link target=_blank} 👆 🈸. + +& ⤴️ 👆 ✍ ❔ 🍵 [⚜ ⏮️ `CORSMiddleware`](../tutorial/cors.md){.internal-link target=_blank}. + +👉 📄 👥 🔜 👀 ❔ ⚙️ 🎏 🛠️. + +## ❎ 🔫 🛠️ + +**FastAPI** ⚓️ 🔛 💃 & 🛠️ 🔫 🔧, 👆 💪 ⚙️ 🙆 🔫 🛠️. + +🛠️ 🚫 ✔️ ⚒ FastAPI ⚖️ 💃 👷, 📏 ⚫️ ⏩ 🔫 🔌. + +🏢, 🔫 🛠️ 🎓 👈 ⌛ 📨 🔫 📱 🥇 ❌. + +, 🧾 🥉-🥳 🔫 🛠️ 👫 🔜 🎲 💬 👆 🕳 💖: + +```Python +from unicorn import UnicornMiddleware + +app = SomeASGIApp() + +new_app = UnicornMiddleware(app, some_config="rainbow") +``` + +✋️ FastAPI (🤙 💃) 🚚 🙅 🌌 ⚫️ 👈 ⚒ 💭 👈 🔗 🛠️ 🍵 💽 ❌ & 🛃 ⚠ 🐕‍🦺 👷 ☑. + +👈, 👆 ⚙️ `app.add_middleware()` (🖼 ⚜). + +```Python +from fastapi import FastAPI +from unicorn import UnicornMiddleware + +app = FastAPI() + +app.add_middleware(UnicornMiddleware, some_config="rainbow") +``` + +`app.add_middleware()` 📨 🛠️ 🎓 🥇 ❌ & 🙆 🌖 ❌ 🚶‍♀️ 🛠️. + +## 🛠️ 🛠️ + +**FastAPI** 🔌 📚 🛠️ ⚠ ⚙️ 💼, 👥 🔜 👀 ⏭ ❔ ⚙️ 👫. + +!!! note "📡 ℹ" + ⏭ 🖼, 👆 💪 ⚙️ `from starlette.middleware.something import SomethingMiddleware`. + + **FastAPI** 🚚 📚 🛠️ `fastapi.middleware` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 🛠️ 👟 🔗 ⚪️➡️ 💃. + +## `HTTPSRedirectMiddleware` + +🛠️ 👈 🌐 📨 📨 🔜 👯‍♂️ `https` ⚖️ `wss`. + +🙆 📨 📨 `http` ⚖️ `ws` 🔜 ❎ 🔐 ⚖ ↩️. + +```Python hl_lines="2 6" +{!../../../docs_src/advanced_middleware/tutorial001.py!} +``` + +## `TrustedHostMiddleware` + +🛠️ 👈 🌐 📨 📨 ✔️ ☑ ⚒ `Host` 🎚, ✔ 💂‍♂ 🛡 🇺🇸🔍 🦠 🎚 👊. + +```Python hl_lines="2 6-8" +{!../../../docs_src/advanced_middleware/tutorial002.py!} +``` + +📄 ❌ 🐕‍🦺: + +* `allowed_hosts` - 📇 🆔 📛 👈 🔜 ✔ 📛. 🃏 🆔 ✅ `*.example.com` 🐕‍🦺 🎀 📁. ✔ 🙆 📛 👯‍♂️ ⚙️ `allowed_hosts=["*"]` ⚖️ 🚫 🛠️. + +🚥 📨 📨 🔨 🚫 ✔ ☑ ⤴️ `400` 📨 🔜 📨. + +## `GZipMiddleware` + +🍵 🗜 📨 🙆 📨 👈 🔌 `"gzip"` `Accept-Encoding` 🎚. + +🛠️ 🔜 🍵 👯‍♂️ 🐩 & 🎥 📨. + +```Python hl_lines="2 6" +{!../../../docs_src/advanced_middleware/tutorial003.py!} +``` + +📄 ❌ 🐕‍🦺: + +* `minimum_size` - 🚫 🗜 📨 👈 🤪 🌘 👉 💯 📐 🔢. 🔢 `500`. + +## 🎏 🛠️ + +📤 📚 🎏 🔫 🛠️. + +🖼: + +* 🔫 +* Uvicorn `ProxyHeadersMiddleware` +* 🇸🇲 + +👀 🎏 💪 🛠️ ✅ 💃 🛠️ 🩺 & 🔫 👌 📇. diff --git a/docs/em/docs/advanced/nosql-databases.md b/docs/em/docs/advanced/nosql-databases.md new file mode 100644 index 0000000000..9c828a9094 --- /dev/null +++ b/docs/em/docs/advanced/nosql-databases.md @@ -0,0 +1,156 @@ +# ☁ (📎 / 🦏 💽) 💽 + +**FastAPI** 💪 🛠️ ⏮️ 🙆 . + +📥 👥 🔜 👀 🖼 ⚙️ **🗄**, 📄 🧢 ☁ 💽. + +👆 💪 🛠️ ⚫️ 🙆 🎏 ☁ 💽 💖: + +* **✳** +* **👸** +* **✳** +* **🇸🇲** +* **✳**, ♒️. + +!!! tip + 📤 🛂 🏗 🚂 ⏮️ **FastAPI** & **🗄**, 🌐 ⚓️ 🔛 **☁**, 🔌 🕸 & 🌖 🧰: https://github.com/tiangolo/full-stack-fastapi-couchbase + +## 🗄 🗄 🦲 + +🔜, 🚫 💸 🙋 🎂, 🕴 🗄: + +```Python hl_lines="3-5" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +## 🔬 📉 ⚙️ "📄 🆎" + +👥 🔜 ⚙️ ⚫️ ⏪ 🔧 🏑 `type` 👆 📄. + +👉 🚫 ✔ 🗄, ✋️ 👍 💡 👈 🔜 ℹ 👆 ⏮️. + +```Python hl_lines="9" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +## 🚮 🔢 🤚 `Bucket` + +**🗄**, 🥡 ⚒ 📄, 👈 💪 🎏 🆎. + +👫 🛎 🌐 🔗 🎏 🈸. + +🔑 🔗 💽 🌏 🔜 "💽" (🎯 💽, 🚫 💽 💽). + +🔑 **✳** 🔜 "🗃". + +📟, `Bucket` 🎨 👑 🇨🇻 📻 ⏮️ 💽. + +👉 🚙 🔢 🔜: + +* 🔗 **🗄** 🌑 (👈 💪 👁 🎰). + * ⚒ 🔢 ⏲. +* 🔓 🌑. +* 🤚 `Bucket` 👐. + * ⚒ 🔢 ⏲. +* 📨 ⚫️. + +```Python hl_lines="12-21" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +## ✍ Pydantic 🏷 + +**🗄** "📄" 🤙 "🎻 🎚", 👥 💪 🏷 👫 ⏮️ Pydantic. + +### `User` 🏷 + +🥇, ➡️ ✍ `User` 🏷: + +```Python hl_lines="24-28" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +👥 🔜 ⚙️ 👉 🏷 👆 *➡ 🛠️ 🔢*,, 👥 🚫 🔌 ⚫️ `hashed_password`. + +### `UserInDB` 🏷 + +🔜, ➡️ ✍ `UserInDB` 🏷. + +👉 🔜 ✔️ 💽 👈 🤙 🏪 💽. + +👥 🚫 ✍ ⚫️ 🏿 Pydantic `BaseModel` ✋️ 🏿 👆 👍 `User`, ↩️ ⚫️ 🔜 ✔️ 🌐 🔢 `User` ➕ 👩‍❤‍👨 🌅: + +```Python hl_lines="31-33" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +!!! note + 👀 👈 👥 ✔️ `hashed_password` & `type` 🏑 👈 🔜 🏪 💽. + + ✋️ ⚫️ 🚫 🍕 🏢 `User` 🏷 (1️⃣ 👥 🔜 📨 *➡ 🛠️*). + +## 🤚 👩‍💻 + +🔜 ✍ 🔢 👈 🔜: + +* ✊ 🆔. +* 🏗 📄 🆔 ⚪️➡️ ⚫️. +* 🤚 📄 ⏮️ 👈 🆔. +* 🚮 🎚 📄 `UserInDB` 🏷. + +🏗 🔢 👈 🕴 💡 🤚 👆 👩‍💻 ⚪️➡️ `username` (⚖️ 🙆 🎏 🔢) 🔬 👆 *➡ 🛠️ 🔢*, 👆 💪 🌖 💪 🏤-⚙️ ⚫️ 💗 🍕 & 🚮 ⚒ 💯 ⚫️: + +```Python hl_lines="36-42" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +### Ⓜ-🎻 + +🚥 👆 🚫 😰 ⏮️ `f"userprofile::{username}"`, ⚫️ 🐍 "Ⓜ-🎻". + +🙆 🔢 👈 🚮 🔘 `{}` Ⓜ-🎻 🔜 ↔ / 💉 🎻. + +### `dict` 🏗 + +🚥 👆 🚫 😰 ⏮️ `UserInDB(**result.value)`, ⚫️ ⚙️ `dict` "🏗". + +⚫️ 🔜 ✊ `dict` `result.value`, & ✊ 🔠 🚮 🔑 & 💲 & 🚶‍♀️ 👫 🔑-💲 `UserInDB` 🇨🇻 ❌. + +, 🚥 `dict` 🔌: + +```Python +{ + "username": "johndoe", + "hashed_password": "some_hash", +} +``` + +⚫️ 🔜 🚶‍♀️ `UserInDB` : + +```Python +UserInDB(username="johndoe", hashed_password="some_hash") +``` + +## ✍ 👆 **FastAPI** 📟 + +### ✍ `FastAPI` 📱 + +```Python hl_lines="46" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +### ✍ *➡ 🛠️ 🔢* + +👆 📟 🤙 🗄 & 👥 🚫 ⚙️ 🥼 🐍 await 🐕‍🦺, 👥 🔜 📣 👆 🔢 ⏮️ 😐 `def` ↩️ `async def`. + +, 🗄 👍 🚫 ⚙️ 👁 `Bucket` 🎚 💗 "🧵Ⓜ",, 👥 💪 🤚 🥡 🔗 & 🚶‍♀️ ⚫️ 👆 🚙 🔢: + +```Python hl_lines="49-53" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +## 🌃 + +👆 💪 🛠️ 🙆 🥉 🥳 ☁ 💽, ⚙️ 👫 🐩 📦. + +🎏 ✔ 🙆 🎏 🔢 🧰, ⚙️ ⚖️ 🛠️. diff --git a/docs/em/docs/advanced/openapi-callbacks.md b/docs/em/docs/advanced/openapi-callbacks.md new file mode 100644 index 0000000000..630b75ed29 --- /dev/null +++ b/docs/em/docs/advanced/openapi-callbacks.md @@ -0,0 +1,179 @@ +# 🗄 ⏲ + +👆 💪 ✍ 🛠️ ⏮️ *➡ 🛠️* 👈 💪 ⏲ 📨 *🔢 🛠️* ✍ 👱 🙆 (🎲 🎏 👩‍💻 👈 🔜 *⚙️* 👆 🛠️). + +🛠️ 👈 🔨 🕐❔ 👆 🛠️ 📱 🤙 *🔢 🛠️* 📛 "⏲". ↩️ 🖥 👈 🔢 👩‍💻 ✍ 📨 📨 👆 🛠️ & ⤴️ 👆 🛠️ *🤙 🔙*, 📨 📨 *🔢 🛠️* (👈 🎲 ✍ 🎏 👩‍💻). + +👉 💼, 👆 💪 💚 📄 ❔ 👈 🔢 🛠️ *🔜* 👀 💖. ⚫️❔ *➡ 🛠️* ⚫️ 🔜 ✔️, ⚫️❔ 💪 ⚫️ 🔜 ⌛, ⚫️❔ 📨 ⚫️ 🔜 📨, ♒️. + +## 📱 ⏮️ ⏲ + +➡️ 👀 🌐 👉 ⏮️ 🖼. + +🌈 👆 🛠️ 📱 👈 ✔ 🏗 🧾. + +👉 🧾 🔜 ✔️ `id`, `title` (📦), `customer`, & `total`. + +👩‍💻 👆 🛠️ (🔢 👩‍💻) 🔜 ✍ 🧾 👆 🛠️ ⏮️ 🏤 📨. + +⤴️ 👆 🛠️ 🔜 (➡️ 🌈): + +* 📨 🧾 🕴 🔢 👩‍💻. +* 📈 💸. +* 📨 📨 🔙 🛠️ 👩‍💻 (🔢 👩‍💻). + * 👉 🔜 🔨 📨 🏤 📨 (⚪️➡️ *👆 🛠️*) *🔢 🛠️* 🚚 👈 🔢 👩‍💻 (👉 "⏲"). + +## 😐 **FastAPI** 📱 + +➡️ 🥇 👀 ❔ 😐 🛠️ 📱 🔜 👀 💖 ⏭ ❎ ⏲. + +⚫️ 🔜 ✔️ *➡ 🛠️* 👈 🔜 📨 `Invoice` 💪, & 🔢 🔢 `callback_url` 👈 🔜 🔌 📛 ⏲. + +👉 🍕 📶 😐, 🌅 📟 🎲 ⏪ 😰 👆: + +```Python hl_lines="9-13 36-53" +{!../../../docs_src/openapi_callbacks/tutorial001.py!} +``` + +!!! tip + `callback_url` 🔢 🔢 ⚙️ Pydantic 📛 🆎. + +🕴 🆕 👜 `callbacks=messages_callback_router.routes` ❌ *➡ 🛠️ 👨‍🎨*. 👥 🔜 👀 ⚫️❔ 👈 ⏭. + +## 🔬 ⏲ + +☑ ⏲ 📟 🔜 🪀 🙇 🔛 👆 👍 🛠️ 📱. + +& ⚫️ 🔜 🎲 🪀 📚 ⚪️➡️ 1️⃣ 📱 ⏭. + +⚫️ 💪 1️⃣ ⚖️ 2️⃣ ⏸ 📟, 💖: + +```Python +callback_url = "https://example.com/api/v1/invoices/events/" +httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) +``` + +✋️ 🎲 🏆 ⚠ 🍕 ⏲ ⚒ 💭 👈 👆 🛠️ 👩‍💻 (🔢 👩‍💻) 🛠️ *🔢 🛠️* ☑, 🛄 💽 👈 *👆 🛠️* 🔜 📨 📨 💪 ⏲, ♒️. + +, ⚫️❔ 👥 🔜 ⏭ 🚮 📟 📄 ❔ 👈 *🔢 🛠️* 🔜 👀 💖 📨 ⏲ ⚪️➡️ *👆 🛠️*. + +👈 🧾 🔜 🎦 🆙 🦁 🎚 `/docs` 👆 🛠️, & ⚫️ 🔜 ➡️ 🔢 👩‍💻 💭 ❔ 🏗 *🔢 🛠️*. + +👉 🖼 🚫 🛠️ ⏲ ⚫️ (👈 💪 ⏸ 📟), 🕴 🧾 🍕. + +!!! tip + ☑ ⏲ 🇺🇸🔍 📨. + + 🕐❔ 🛠️ ⏲ 👆, 👆 💪 ⚙️ 🕳 💖 🇸🇲 ⚖️ 📨. + +## ✍ ⏲ 🧾 📟 + +👉 📟 🏆 🚫 🛠️ 👆 📱, 👥 🕴 💪 ⚫️ *📄* ❔ 👈 *🔢 🛠️* 🔜 👀 💖. + +✋️, 👆 ⏪ 💭 ❔ 💪 ✍ 🏧 🧾 🛠️ ⏮️ **FastAPI**. + +👥 🔜 ⚙️ 👈 🎏 💡 📄 ❔ *🔢 🛠️* 🔜 👀 💖... 🏗 *➡ 🛠️(Ⓜ)* 👈 🔢 🛠️ 🔜 🛠️ (🕐 👆 🛠️ 🔜 🤙). + +!!! tip + 🕐❔ ✍ 📟 📄 ⏲, ⚫️ 💪 ⚠ 🌈 👈 👆 👈 *🔢 👩‍💻*. & 👈 👆 ⏳ 🛠️ *🔢 🛠️*, 🚫 *👆 🛠️*. + + 🍕 🛠️ 👉 ☝ 🎑 ( *🔢 👩‍💻*) 💪 ℹ 👆 💭 💖 ⚫️ 🌅 ⭐ 🌐❔ 🚮 🔢, Pydantic 🏷 💪, 📨, ♒️. 👈 *🔢 🛠️*. + +### ✍ ⏲ `APIRouter` + +🥇 ✍ 🆕 `APIRouter` 👈 🔜 🔌 1️⃣ ⚖️ 🌅 ⏲. + +```Python hl_lines="3 25" +{!../../../docs_src/openapi_callbacks/tutorial001.py!} +``` + +### ✍ ⏲ *➡ 🛠️* + +✍ ⏲ *➡ 🛠️* ⚙️ 🎏 `APIRouter` 👆 ✍ 🔛. + +⚫️ 🔜 👀 💖 😐 FastAPI *➡ 🛠️*: + +* ⚫️ 🔜 🎲 ✔️ 📄 💪 ⚫️ 🔜 📨, ✅ `body: InvoiceEvent`. +* & ⚫️ 💪 ✔️ 📄 📨 ⚫️ 🔜 📨, ✅ `response_model=InvoiceEventReceived`. + +```Python hl_lines="16-18 21-22 28-32" +{!../../../docs_src/openapi_callbacks/tutorial001.py!} +``` + +📤 2️⃣ 👑 🔺 ⚪️➡️ 😐 *➡ 🛠️*: + +* ⚫️ 🚫 💪 ✔️ 🙆 ☑ 📟, ↩️ 👆 📱 🔜 🙅 🤙 👉 📟. ⚫️ 🕴 ⚙️ 📄 *🔢 🛠️*. , 🔢 💪 ✔️ `pass`. +* *➡* 💪 🔌 🗄 3️⃣ 🧬 (👀 🌖 🔛) 🌐❔ ⚫️ 💪 ⚙️ 🔢 ⏮️ 🔢 & 🍕 ⏮️ 📨 📨 *👆 🛠️*. + +### ⏲ ➡ 🧬 + +⏲ *➡* 💪 ✔️ 🗄 3️⃣ 🧬 👈 💪 🔌 🍕 ⏮️ 📨 📨 *👆 🛠️*. + +👉 💼, ⚫️ `str`: + +```Python +"{$callback_url}/invoices/{$request.body.id}" +``` + +, 🚥 👆 🛠️ 👩‍💻 (🔢 👩‍💻) 📨 📨 *👆 🛠️* : + +``` +https://yourapi.com/invoices/?callback_url=https://www.external.org/events +``` + +⏮️ 🎻 💪: + +```JSON +{ + "id": "2expen51ve", + "customer": "Mr. Richie Rich", + "total": "9999" +} +``` + +⤴️ *👆 🛠️* 🔜 🛠️ 🧾, & ☝ ⏪, 📨 ⏲ 📨 `callback_url` ( *🔢 🛠️*): + +``` +https://www.external.org/events/invoices/2expen51ve +``` + +⏮️ 🎻 💪 ⚗ 🕳 💖: + +```JSON +{ + "description": "Payment celebration", + "paid": true +} +``` + +& ⚫️ 🔜 ⌛ 📨 ⚪️➡️ 👈 *🔢 🛠️* ⏮️ 🎻 💪 💖: + +```JSON +{ + "ok": true +} +``` + +!!! tip + 👀 ❔ ⏲ 📛 ⚙️ 🔌 📛 📨 🔢 🔢 `callback_url` (`https://www.external.org/events`) & 🧾 `id` ⚪️➡️ 🔘 🎻 💪 (`2expen51ve`). + +### 🚮 ⏲ 📻 + +👉 ☝ 👆 ✔️ *⏲ ➡ 🛠️(Ⓜ)* 💪 (1️⃣(Ⓜ) 👈 *🔢 👩‍💻* 🔜 🛠️ *🔢 🛠️*) ⏲ 📻 👆 ✍ 🔛. + +🔜 ⚙️ 🔢 `callbacks` *👆 🛠️ ➡ 🛠️ 👨‍🎨* 🚶‍♀️ 🔢 `.routes` (👈 🤙 `list` 🛣/*➡ 🛠️*) ⚪️➡️ 👈 ⏲ 📻: + +```Python hl_lines="35" +{!../../../docs_src/openapi_callbacks/tutorial001.py!} +``` + +!!! tip + 👀 👈 👆 🚫 🚶‍♀️ 📻 ⚫️ (`invoices_callback_router`) `callback=`, ✋️ 🔢 `.routes`, `invoices_callback_router.routes`. + +### ✅ 🩺 + +🔜 👆 💪 ▶️ 👆 📱 ⏮️ Uvicorn & 🚶 http://127.0.0.1:8000/docs. + +👆 🔜 👀 👆 🩺 ✅ "⏲" 📄 👆 *➡ 🛠️* 👈 🎦 ❔ *🔢 🛠️* 🔜 👀 💖: + + diff --git a/docs/em/docs/advanced/path-operation-advanced-configuration.md b/docs/em/docs/advanced/path-operation-advanced-configuration.md new file mode 100644 index 0000000000..ec72318706 --- /dev/null +++ b/docs/em/docs/advanced/path-operation-advanced-configuration.md @@ -0,0 +1,170 @@ +# ➡ 🛠️ 🏧 📳 + +## 🗄 { + +!!! warning + 🚥 👆 🚫 "🕴" 🗄, 👆 🎲 🚫 💪 👉. + +👆 💪 ⚒ 🗄 `operationId` ⚙️ 👆 *➡ 🛠️* ⏮️ 🔢 `operation_id`. + +👆 🔜 ✔️ ⚒ 💭 👈 ⚫️ 😍 🔠 🛠️. + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial001.py!} +``` + +### ⚙️ *➡ 🛠️ 🔢* 📛 { + +🚥 👆 💚 ⚙️ 👆 🔗' 🔢 📛 `operationId`Ⓜ, 👆 💪 🔁 🤭 🌐 👫 & 🔐 🔠 *➡ 🛠️* `operation_id` ⚙️ 👫 `APIRoute.name`. + +👆 🔜 ⚫️ ⏮️ ❎ 🌐 👆 *➡ 🛠️*. + +```Python hl_lines="2 12-21 24" +{!../../../docs_src/path_operation_advanced_configuration/tutorial002.py!} +``` + +!!! tip + 🚥 👆 ❎ 🤙 `app.openapi()`, 👆 🔜 ℹ `operationId`Ⓜ ⏭ 👈. + +!!! warning + 🚥 👆 👉, 👆 ✔️ ⚒ 💭 🔠 1️⃣ 👆 *➡ 🛠️ 🔢* ✔️ 😍 📛. + + 🚥 👫 🎏 🕹 (🐍 📁). + +## 🚫 ⚪️➡️ 🗄 + +🚫 *➡ 🛠️* ⚪️➡️ 🏗 🗄 🔗 (& ➡️, ⚪️➡️ 🏧 🧾 ⚙️), ⚙️ 🔢 `include_in_schema` & ⚒ ⚫️ `False`: + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial003.py!} +``` + +## 🏧 📛 ⚪️➡️ #️⃣ + +👆 💪 📉 ⏸ ⚙️ ⚪️➡️ #️⃣ *➡ 🛠️ 🔢* 🗄. + +❎ `\f` (😖 "📨 🍼" 🦹) 🤕 **FastAPI** 🔁 🔢 ⚙️ 🗄 👉 ☝. + +⚫️ 🏆 🚫 🎦 🆙 🧾, ✋️ 🎏 🧰 (✅ 🐉) 🔜 💪 ⚙️ 🎂. + +```Python hl_lines="19-29" +{!../../../docs_src/path_operation_advanced_configuration/tutorial004.py!} +``` + +## 🌖 📨 + +👆 🎲 ✔️ 👀 ❔ 📣 `response_model` & `status_code` *➡ 🛠️*. + +👈 🔬 🗃 🔃 👑 📨 *➡ 🛠️*. + +👆 💪 📣 🌖 📨 ⏮️ 👫 🏷, 👔 📟, ♒️. + +📤 🎂 📃 📥 🧾 🔃 ⚫️, 👆 💪 ✍ ⚫️ [🌖 📨 🗄](./additional-responses.md){.internal-link target=_blank}. + +## 🗄 ➕ + +🕐❔ 👆 📣 *➡ 🛠️* 👆 🈸, **FastAPI** 🔁 🏗 🔗 🗃 🔃 👈 *➡ 🛠️* 🔌 🗄 🔗. + +!!! note "📡 ℹ" + 🗄 🔧 ⚫️ 🤙 🛠️ 🎚. + +⚫️ ✔️ 🌐 ℹ 🔃 *➡ 🛠️* & ⚙️ 🏗 🏧 🧾. + +⚫️ 🔌 `tags`, `parameters`, `requestBody`, `responses`, ♒️. + +👉 *➡ 🛠️*-🎯 🗄 🔗 🛎 🏗 🔁 **FastAPI**, ✋️ 👆 💪 ↔ ⚫️. + +!!! tip + 👉 🔅 🎚 ↔ ☝. + + 🚥 👆 🕴 💪 📣 🌖 📨, 🌅 🏪 🌌 ⚫️ ⏮️ [🌖 📨 🗄](./additional-responses.md){.internal-link target=_blank}. + +👆 💪 ↔ 🗄 🔗 *➡ 🛠️* ⚙️ 🔢 `openapi_extra`. + +### 🗄 ↔ + +👉 `openapi_extra` 💪 👍, 🖼, 📣 [🗄 ↔](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions): + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial005.py!} +``` + +🚥 👆 📂 🏧 🛠️ 🩺, 👆 ↔ 🔜 🎦 🆙 🔝 🎯 *➡ 🛠️*. + + + +& 🚥 👆 👀 📉 🗄 ( `/openapi.json` 👆 🛠️), 👆 🔜 👀 👆 ↔ 🍕 🎯 *➡ 🛠️* 💁‍♂️: + +```JSON hl_lines="22" +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "x-aperture-labs-portal": "blue" + } + } + } +} +``` + +### 🛃 🗄 *➡ 🛠️* 🔗 + +📖 `openapi_extra` 🔜 🙇 🔗 ⏮️ 🔁 🏗 🗄 🔗 *➡ 🛠️*. + +, 👆 💪 🚮 🌖 💽 🔁 🏗 🔗. + +🖼, 👆 💪 💭 ✍ & ✔ 📨 ⏮️ 👆 👍 📟, 🍵 ⚙️ 🏧 ⚒ FastAPI ⏮️ Pydantic, ✋️ 👆 💪 💚 🔬 📨 🗄 🔗. + +👆 💪 👈 ⏮️ `openapi_extra`: + +```Python hl_lines="20-37 39-40" +{!../../../docs_src/path_operation_advanced_configuration/tutorial006.py!} +``` + +👉 🖼, 👥 🚫 📣 🙆 Pydantic 🏷. 👐, 📨 💪 🚫 🎻 🎻, ⚫️ ✍ 🔗 `bytes`, & 🔢 `magic_data_reader()` 🔜 🈚 🎻 ⚫️ 🌌. + +👐, 👥 💪 📣 📈 🔗 📨 💪. + +### 🛃 🗄 🎚 🆎 + +⚙️ 👉 🎏 🎱, 👆 💪 ⚙️ Pydantic 🏷 🔬 🎻 🔗 👈 ⤴️ 🔌 🛃 🗄 🔗 📄 *➡ 🛠️*. + +& 👆 💪 👉 🚥 💽 🆎 📨 🚫 🎻. + +🖼, 👉 🈸 👥 🚫 ⚙️ FastAPI 🛠️ 🛠️ ⚗ 🎻 🔗 ⚪️➡️ Pydantic 🏷 🚫 🏧 🔬 🎻. 👐, 👥 📣 📨 🎚 🆎 📁, 🚫 🎻: + +```Python hl_lines="17-22 24" +{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} +``` + +👐, 👐 👥 🚫 ⚙️ 🔢 🛠️ 🛠️, 👥 ⚙️ Pydantic 🏷 ❎ 🏗 🎻 🔗 💽 👈 👥 💚 📨 📁. + +⤴️ 👥 ⚙️ 📨 🔗, & ⚗ 💪 `bytes`. 👉 ⛓ 👈 FastAPI 🏆 🚫 🔄 🎻 📨 🚀 🎻. + +& ⤴️ 👆 📟, 👥 🎻 👈 📁 🎚 🔗, & ⤴️ 👥 🔄 ⚙️ 🎏 Pydantic 🏷 ✔ 📁 🎚: + +```Python hl_lines="26-33" +{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} +``` + +!!! tip + 📥 👥 🏤-⚙️ 🎏 Pydantic 🏷. + + ✋️ 🎏 🌌, 👥 💪 ✔️ ✔ ⚫️ 🎏 🌌. diff --git a/docs/em/docs/advanced/response-change-status-code.md b/docs/em/docs/advanced/response-change-status-code.md new file mode 100644 index 0000000000..156efcc16a --- /dev/null +++ b/docs/em/docs/advanced/response-change-status-code.md @@ -0,0 +1,33 @@ +# 📨 - 🔀 👔 📟 + +👆 🎲 ✍ ⏭ 👈 👆 💪 ⚒ 🔢 [📨 👔 📟](../tutorial/response-status-code.md){.internal-link target=_blank}. + +✋️ 💼 👆 💪 📨 🎏 👔 📟 🌘 🔢. + +## ⚙️ 💼 + +🖼, 🌈 👈 👆 💚 📨 🇺🇸🔍 👔 📟 "👌" `200` 🔢. + +✋️ 🚥 💽 🚫 🔀, 👆 💚 ✍ ⚫️, & 📨 🇺🇸🔍 👔 📟 "✍" `201`. + +✋️ 👆 💚 💪 ⛽ & 🗜 💽 👆 📨 ⏮️ `response_model`. + +📚 💼, 👆 💪 ⚙️ `Response` 🔢. + +## ⚙️ `Response` 🔢 + +👆 💪 📣 🔢 🆎 `Response` 👆 *➡ 🛠️ 🔢* (👆 💪 🍪 & 🎚). + +& ⤴️ 👆 💪 ⚒ `status_code` 👈 *🔀* 📨 🎚. + +```Python hl_lines="1 9 12" +{!../../../docs_src/response_change_status_code/tutorial001.py!} +``` + +& ⤴️ 👆 💪 📨 🙆 🎚 👆 💪, 👆 🛎 🔜 ( `dict`, 💽 🏷, ♒️). + +& 🚥 👆 📣 `response_model`, ⚫️ 🔜 ⚙️ ⛽ & 🗜 🎚 👆 📨. + +**FastAPI** 🔜 ⚙️ 👈 *🔀* 📨 ⚗ 👔 📟 (🍪 & 🎚), & 🔜 🚮 👫 🏁 📨 👈 🔌 💲 👆 📨, ⛽ 🙆 `response_model`. + +👆 💪 📣 `Response` 🔢 🔗, & ⚒ 👔 📟 👫. ✋️ ✔️ 🤯 👈 🏁 1️⃣ ⚒ 🔜 🏆. diff --git a/docs/em/docs/advanced/response-cookies.md b/docs/em/docs/advanced/response-cookies.md new file mode 100644 index 0000000000..23fffe1ddc --- /dev/null +++ b/docs/em/docs/advanced/response-cookies.md @@ -0,0 +1,49 @@ +# 📨 🍪 + +## ⚙️ `Response` 🔢 + +👆 💪 📣 🔢 🆎 `Response` 👆 *➡ 🛠️ 🔢*. + +& ⤴️ 👆 💪 ⚒ 🍪 👈 *🔀* 📨 🎚. + +```Python hl_lines="1 8-9" +{!../../../docs_src/response_cookies/tutorial002.py!} +``` + +& ⤴️ 👆 💪 📨 🙆 🎚 👆 💪, 👆 🛎 🔜 ( `dict`, 💽 🏷, ♒️). + +& 🚥 👆 📣 `response_model`, ⚫️ 🔜 ⚙️ ⛽ & 🗜 🎚 👆 📨. + +**FastAPI** 🔜 ⚙️ 👈 *🔀* 📨 ⚗ 🍪 (🎚 & 👔 📟), & 🔜 🚮 👫 🏁 📨 👈 🔌 💲 👆 📨, ⛽ 🙆 `response_model`. + +👆 💪 📣 `Response` 🔢 🔗, & ⚒ 🍪 (& 🎚) 👫. + +## 📨 `Response` 🔗 + +👆 💪 ✍ 🍪 🕐❔ 🛬 `Response` 🔗 👆 📟. + +👈, 👆 💪 ✍ 📨 🔬 [📨 📨 🔗](response-directly.md){.internal-link target=_blank}. + +⤴️ ⚒ 🍪 ⚫️, & ⤴️ 📨 ⚫️: + +```Python hl_lines="10-12" +{!../../../docs_src/response_cookies/tutorial001.py!} +``` + +!!! tip + ✔️ 🤯 👈 🚥 👆 📨 📨 🔗 ↩️ ⚙️ `Response` 🔢, FastAPI 🔜 📨 ⚫️ 🔗. + + , 👆 🔜 ✔️ ⚒ 💭 👆 💽 ☑ 🆎. 🤶 Ⓜ. ⚫️ 🔗 ⏮️ 🎻, 🚥 👆 🛬 `JSONResponse`. + + & 👈 👆 🚫 📨 🙆 📊 👈 🔜 ✔️ ⛽ `response_model`. + +### 🌅 ℹ + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import Response` ⚖️ `from starlette.responses import JSONResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. + + & `Response` 💪 ⚙️ 🛎 ⚒ 🎚 & 🍪, **FastAPI** 🚚 ⚫️ `fastapi.Response`. + +👀 🌐 💪 🔢 & 🎛, ✅ 🧾 💃. diff --git a/docs/em/docs/advanced/response-directly.md b/docs/em/docs/advanced/response-directly.md new file mode 100644 index 0000000000..ba09734fb3 --- /dev/null +++ b/docs/em/docs/advanced/response-directly.md @@ -0,0 +1,63 @@ +# 📨 📨 🔗 + +🕐❔ 👆 ✍ **FastAPI** *➡ 🛠️* 👆 💪 🛎 📨 🙆 📊 ⚪️➡️ ⚫️: `dict`, `list`, Pydantic 🏷, 💽 🏷, ♒️. + +🔢, **FastAPI** 🔜 🔁 🗜 👈 📨 💲 🎻 ⚙️ `jsonable_encoder` 🔬 [🎻 🔗 🔢](../tutorial/encoder.md){.internal-link target=_blank}. + +⤴️, ⛅ 🎑, ⚫️ 🔜 🚮 👈 🎻-🔗 💽 (✅ `dict`) 🔘 `JSONResponse` 👈 🔜 ⚙️ 📨 📨 👩‍💻. + +✋️ 👆 💪 📨 `JSONResponse` 🔗 ⚪️➡️ 👆 *➡ 🛠️*. + +⚫️ 💪 ⚠, 🖼, 📨 🛃 🎚 ⚖️ 🍪. + +## 📨 `Response` + +👐, 👆 💪 📨 🙆 `Response` ⚖️ 🙆 🎧-🎓 ⚫️. + +!!! tip + `JSONResponse` ⚫️ 🎧-🎓 `Response`. + +& 🕐❔ 👆 📨 `Response`, **FastAPI** 🔜 🚶‍♀️ ⚫️ 🔗. + +⚫️ 🏆 🚫 🙆 💽 🛠️ ⏮️ Pydantic 🏷, ⚫️ 🏆 🚫 🗜 🎚 🙆 🆎, ♒️. + +👉 🤝 👆 📚 💪. 👆 💪 📨 🙆 📊 🆎, 🔐 🙆 💽 📄 ⚖️ 🔬, ♒️. + +## ⚙️ `jsonable_encoder` `Response` + +↩️ **FastAPI** 🚫 🙆 🔀 `Response` 👆 📨, 👆 ✔️ ⚒ 💭 ⚫️ 🎚 🔜 ⚫️. + +🖼, 👆 🚫🔜 🚮 Pydantic 🏷 `JSONResponse` 🍵 🥇 🏭 ⚫️ `dict` ⏮️ 🌐 📊 🆎 (💖 `datetime`, `UUID`, ♒️) 🗜 🎻-🔗 🆎. + +📚 💼, 👆 💪 ⚙️ `jsonable_encoder` 🗜 👆 📊 ⏭ 🚶‍♀️ ⚫️ 📨: + +```Python hl_lines="6-7 21-22" +{!../../../docs_src/response_directly/tutorial001.py!} +``` + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import JSONResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. + +## 🛬 🛃 `Response` + +🖼 🔛 🎦 🌐 🍕 👆 💪, ✋️ ⚫️ 🚫 📶 ⚠, 👆 💪 ✔️ 📨 `item` 🔗, & **FastAPI** 🔜 🚮 ⚫️ `JSONResponse` 👆, 🏭 ⚫️ `dict`, ♒️. 🌐 👈 🔢. + +🔜, ➡️ 👀 ❔ 👆 💪 ⚙️ 👈 📨 🛃 📨. + +➡️ 💬 👈 👆 💚 📨 📂 📨. + +👆 💪 🚮 👆 📂 🎚 🎻, 🚮 ⚫️ `Response`, & 📨 ⚫️: + +```Python hl_lines="1 18" +{!../../../docs_src/response_directly/tutorial002.py!} +``` + +## 🗒 + +🕐❔ 👆 📨 `Response` 🔗 🚮 📊 🚫 ✔, 🗜 (🎻), 🚫 📄 🔁. + +✋️ 👆 💪 📄 ⚫️ 🔬 [🌖 📨 🗄](additional-responses.md){.internal-link target=_blank}. + +👆 💪 👀 ⏪ 📄 ❔ ⚙️/📣 👉 🛃 `Response`Ⓜ ⏪ ✔️ 🏧 💽 🛠️, 🧾, ♒️. diff --git a/docs/em/docs/advanced/response-headers.md b/docs/em/docs/advanced/response-headers.md new file mode 100644 index 0000000000..de798982a9 --- /dev/null +++ b/docs/em/docs/advanced/response-headers.md @@ -0,0 +1,42 @@ +# 📨 🎚 + +## ⚙️ `Response` 🔢 + +👆 💪 📣 🔢 🆎 `Response` 👆 *➡ 🛠️ 🔢* (👆 💪 🍪). + +& ⤴️ 👆 💪 ⚒ 🎚 👈 *🔀* 📨 🎚. + +```Python hl_lines="1 7-8" +{!../../../docs_src/response_headers/tutorial002.py!} +``` + +& ⤴️ 👆 💪 📨 🙆 🎚 👆 💪, 👆 🛎 🔜 ( `dict`, 💽 🏷, ♒️). + +& 🚥 👆 📣 `response_model`, ⚫️ 🔜 ⚙️ ⛽ & 🗜 🎚 👆 📨. + +**FastAPI** 🔜 ⚙️ 👈 *🔀* 📨 ⚗ 🎚 (🍪 & 👔 📟), & 🔜 🚮 👫 🏁 📨 👈 🔌 💲 👆 📨, ⛽ 🙆 `response_model`. + +👆 💪 📣 `Response` 🔢 🔗, & ⚒ 🎚 (& 🍪) 👫. + +## 📨 `Response` 🔗 + +👆 💪 🚮 🎚 🕐❔ 👆 📨 `Response` 🔗. + +✍ 📨 🔬 [📨 📨 🔗](response-directly.md){.internal-link target=_blank} & 🚶‍♀️ 🎚 🌖 🔢: + +```Python hl_lines="10-12" +{!../../../docs_src/response_headers/tutorial001.py!} +``` + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import Response` ⚖️ `from starlette.responses import JSONResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. + + & `Response` 💪 ⚙️ 🛎 ⚒ 🎚 & 🍪, **FastAPI** 🚚 ⚫️ `fastapi.Response`. + +## 🛃 🎚 + +✔️ 🤯 👈 🛃 © 🎚 💪 🚮 ⚙️ '✖-' 🔡. + +✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 (✍ 🌅 [⚜ (✖️-🇨🇳 ℹ 🤝)](../tutorial/cors.md){.internal-link target=_blank}), ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. diff --git a/docs/em/docs/advanced/security/http-basic-auth.md b/docs/em/docs/advanced/security/http-basic-auth.md new file mode 100644 index 0000000000..33470a7268 --- /dev/null +++ b/docs/em/docs/advanced/security/http-basic-auth.md @@ -0,0 +1,113 @@ +# 🇺🇸🔍 🔰 🔐 + +🙅 💼, 👆 💪 ⚙️ 🇺🇸🔍 🔰 🔐. + +🇺🇸🔍 🔰 🔐, 🈸 ⌛ 🎚 👈 🔌 🆔 & 🔐. + +🚥 ⚫️ 🚫 📨 ⚫️, ⚫️ 📨 🇺🇸🔍 4️⃣0️⃣1️⃣ "⛔" ❌. + +& 📨 🎚 `WWW-Authenticate` ⏮️ 💲 `Basic`, & 📦 `realm` 🔢. + +👈 💬 🖥 🎦 🛠️ 📋 🆔 & 🔐. + +⤴️, 🕐❔ 👆 🆎 👈 🆔 & 🔐, 🖥 📨 👫 🎚 🔁. + +## 🙅 🇺🇸🔍 🔰 🔐 + +* 🗄 `HTTPBasic` & `HTTPBasicCredentials`. +* ✍ "`security` ⚖" ⚙️ `HTTPBasic`. +* ⚙️ 👈 `security` ⏮️ 🔗 👆 *➡ 🛠️*. +* ⚫️ 📨 🎚 🆎 `HTTPBasicCredentials`: + * ⚫️ 🔌 `username` & `password` 📨. + +```Python hl_lines="2 6 10" +{!../../../docs_src/security/tutorial006.py!} +``` + +🕐❔ 👆 🔄 📂 📛 🥇 🕰 (⚖️ 🖊 "🛠️" 🔼 🩺) 🖥 🔜 💭 👆 👆 🆔 & 🔐: + + + +## ✅ 🆔 + +📥 🌅 🏁 🖼. + +⚙️ 🔗 ✅ 🚥 🆔 & 🔐 ☑. + +👉, ⚙️ 🐍 🐩 🕹 `secrets` ✅ 🆔 & 🔐. + +`secrets.compare_digest()` 💪 ✊ `bytes` ⚖️ `str` 👈 🕴 🔌 🔠 🦹 (🕐 🇪🇸), 👉 ⛓ ⚫️ 🚫🔜 👷 ⏮️ 🦹 💖 `á`, `Sebastián`. + +🍵 👈, 👥 🥇 🗜 `username` & `password` `bytes` 🔢 👫 ⏮️ 🔠-8️⃣. + +⤴️ 👥 💪 ⚙️ `secrets.compare_digest()` 🚚 👈 `credentials.username` `"stanleyjobson"`, & 👈 `credentials.password` `"swordfish"`. + +```Python hl_lines="1 11-21" +{!../../../docs_src/security/tutorial007.py!} +``` + +👉 🔜 🎏: + +```Python +if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): + # Return some error + ... +``` + +✋️ ⚙️ `secrets.compare_digest()` ⚫️ 🔜 🔐 🛡 🆎 👊 🤙 "🕰 👊". + +### ⏲ 👊 + +✋️ ⚫️❔ "⏲ 👊"❓ + +➡️ 🌈 👊 🔄 💭 🆔 & 🔐. + +& 👫 📨 📨 ⏮️ 🆔 `johndoe` & 🔐 `love123`. + +⤴️ 🐍 📟 👆 🈸 🔜 🌓 🕳 💖: + +```Python +if "johndoe" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +✋️ ▶️️ 🙍 🐍 🔬 🥇 `j` `johndoe` 🥇 `s` `stanleyjobson`, ⚫️ 🔜 📨 `False`, ↩️ ⚫️ ⏪ 💭 👈 📚 2️⃣ 🎻 🚫 🎏, 💭 👈 "📤 🙅‍♂ 💪 🗑 🌅 📊 ⚖ 🎂 🔤". & 👆 🈸 🔜 💬 "❌ 👩‍💻 ⚖️ 🔐". + +✋️ ⤴️ 👊 🔄 ⏮️ 🆔 `stanleyjobsox` & 🔐 `love123`. + +& 👆 🈸 📟 🔨 🕳 💖: + +```Python +if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +🐍 🔜 ✔️ 🔬 🎂 `stanleyjobso` 👯‍♂️ `stanleyjobsox` & `stanleyjobson` ⏭ 🤔 👈 👯‍♂️ 🎻 🚫 🎏. ⚫️ 🔜 ✊ ➕ ⏲ 📨 🔙 "❌ 👩‍💻 ⚖️ 🔐". + +#### 🕰 ❔ ℹ 👊 + +👈 ☝, 👀 👈 💽 ✊ ⏲ 📏 📨 "❌ 👩‍💻 ⚖️ 🔐" 📨, 👊 🔜 💭 👈 👫 🤚 _🕳_ ▶️️, ▶️ 🔤 ▶️️. + +& ⤴️ 👫 💪 🔄 🔄 🤔 👈 ⚫️ 🎲 🕳 🌖 🎏 `stanleyjobsox` 🌘 `johndoe`. + +#### "🕴" 👊 + +↗️, 👊 🔜 🚫 🔄 🌐 👉 ✋, 👫 🔜 ✍ 📋 ⚫️, 🎲 ⏮️ 💯 ⚖️ 💯 💯 📍 🥈. & 🔜 🤚 1️⃣ ➕ ☑ 🔤 🕰. + +✋️ 🔨 👈, ⏲ ⚖️ 📆 👊 🔜 ✔️ 💭 ☑ 🆔 & 🔐, ⏮️ "ℹ" 👆 🈸, ⚙️ 🕰 ✊ ❔. + +#### 🔧 ⚫️ ⏮️ `secrets.compare_digest()` + +✋️ 👆 📟 👥 🤙 ⚙️ `secrets.compare_digest()`. + +📏, ⚫️ 🔜 ✊ 🎏 🕰 🔬 `stanleyjobsox` `stanleyjobson` 🌘 ⚫️ ✊ 🔬 `johndoe` `stanleyjobson`. & 🎏 🔐. + +👈 🌌, ⚙️ `secrets.compare_digest()` 👆 🈸 📟, ⚫️ 🔜 🔒 🛡 👉 🎂 ↔ 💂‍♂ 👊. + +### 📨 ❌ + +⏮️ 🔍 👈 🎓 ❌, 📨 `HTTPException` ⏮️ 👔 📟 4️⃣0️⃣1️⃣ (🎏 📨 🕐❔ 🙅‍♂ 🎓 🚚) & 🚮 🎚 `WWW-Authenticate` ⚒ 🖥 🎦 💳 📋 🔄: + +```Python hl_lines="23-27" +{!../../../docs_src/security/tutorial007.py!} +``` diff --git a/docs/em/docs/advanced/security/index.md b/docs/em/docs/advanced/security/index.md new file mode 100644 index 0000000000..f2bb66df46 --- /dev/null +++ b/docs/em/docs/advanced/security/index.md @@ -0,0 +1,16 @@ +# 🏧 💂‍♂ + +## 🌖 ⚒ + +📤 ➕ ⚒ 🍵 💂‍♂ ↖️ ⚪️➡️ 🕐 📔 [🔰 - 👩‍💻 🦮: 💂‍♂](../../tutorial/security/){.internal-link target=_blank}. + +!!! tip + ⏭ 📄 **🚫 🎯 "🏧"**. + + & ⚫️ 💪 👈 👆 ⚙️ 💼, ⚗ 1️⃣ 👫. + +## ✍ 🔰 🥇 + +⏭ 📄 🤔 👆 ⏪ ✍ 👑 [🔰 - 👩‍💻 🦮: 💂‍♂](../../tutorial/security/){.internal-link target=_blank}. + +👫 🌐 ⚓️ 🔛 🎏 🔧, ✋️ ✔ ➕ 🛠️. diff --git a/docs/em/docs/advanced/security/oauth2-scopes.md b/docs/em/docs/advanced/security/oauth2-scopes.md new file mode 100644 index 0000000000..d82fe152be --- /dev/null +++ b/docs/em/docs/advanced/security/oauth2-scopes.md @@ -0,0 +1,269 @@ +# Oauth2️⃣ ↔ + +👆 💪 ⚙️ Oauth2️⃣ ↔ 🔗 ⏮️ **FastAPI**, 👫 🛠️ 👷 💎. + +👉 🔜 ✔ 👆 ✔️ 🌖 👌-🧽 ✔ ⚙️, 📄 Oauth2️⃣ 🐩, 🛠️ 🔘 👆 🗄 🈸 (& 🛠️ 🩺). + +Oauth2️⃣ ⏮️ ↔ 🛠️ ⚙️ 📚 🦏 🤝 🐕‍🦺, 💖 👱📔, 🇺🇸🔍, 📂, 🤸‍♂, 👱📔, ♒️. 👫 ⚙️ ⚫️ 🚚 🎯 ✔ 👩‍💻 & 🈸. + +🔠 🕰 👆 "🕹 ⏮️" 👱📔, 🇺🇸🔍, 📂, 🤸‍♂, 👱📔, 👈 🈸 ⚙️ Oauth2️⃣ ⏮️ ↔. + +👉 📄 👆 🔜 👀 ❔ 🛠️ 🤝 & ✔ ⏮️ 🎏 Oauth2️⃣ ⏮️ ↔ 👆 **FastAPI** 🈸. + +!!! warning + 👉 🌅 ⚖️ 🌘 🏧 📄. 🚥 👆 ▶️, 👆 💪 🚶 ⚫️. + + 👆 🚫 🎯 💪 Oauth2️⃣ ↔, & 👆 💪 🍵 🤝 & ✔ 👐 👆 💚. + + ✋️ Oauth2️⃣ ⏮️ ↔ 💪 🎆 🛠️ 🔘 👆 🛠️ (⏮️ 🗄) & 👆 🛠️ 🩺. + + 👐, 👆 🛠️ 📚 ↔, ⚖️ 🙆 🎏 💂‍♂/✔ 📄, 👐 👆 💪, 👆 📟. + + 📚 💼, Oauth2️⃣ ⏮️ ↔ 💪 👹. + + ✋️ 🚥 👆 💭 👆 💪 ⚫️, ⚖️ 👆 😟, 🚧 👂. + +## Oauth2️⃣ ↔ & 🗄 + +Oauth2️⃣ 🔧 🔬 "↔" 📇 🎻 🎏 🚀. + +🎚 🔠 👉 🎻 💪 ✔️ 🙆 📁, ✋️ 🔜 🚫 🔌 🚀. + +👫 ↔ 🎨 "✔". + +🗄 (✅ 🛠️ 🩺), 👆 💪 🔬 "💂‍♂ ⚖". + +🕐❔ 1️⃣ 👫 💂‍♂ ⚖ ⚙️ Oauth2️⃣, 👆 💪 📣 & ⚙️ ↔. + +🔠 "↔" 🎻 (🍵 🚀). + +👫 🛎 ⚙️ 📣 🎯 💂‍♂ ✔, 🖼: + +* `users:read` ⚖️ `users:write` ⚠ 🖼. +* `instagram_basic` ⚙️ 👱📔 / 👱📔. +* `https://www.googleapis.com/auth/drive` ⚙️ 🇺🇸🔍. + +!!! info + Oauth2️⃣ "↔" 🎻 👈 📣 🎯 ✔ ✔. + + ⚫️ 🚫 🤔 🚥 ⚫️ ✔️ 🎏 🦹 💖 `:` ⚖️ 🚥 ⚫️ 📛. + + 👈 ℹ 🛠️ 🎯. + + Oauth2️⃣ 👫 🎻. + +## 🌐 🎑 + +🥇, ➡️ 🔜 👀 🍕 👈 🔀 ⚪️➡️ 🖼 👑 **🔰 - 👩‍💻 🦮** [Oauth2️⃣ ⏮️ 🔐 (& 🔁), 📨 ⏮️ 🥙 🤝](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. 🔜 ⚙️ Oauth2️⃣ ↔: + +```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 155" +{!../../../docs_src/security/tutorial005.py!} +``` + +🔜 ➡️ 📄 👈 🔀 🔁 🔁. + +## Oauth2️⃣ 💂‍♂ ⚖ + +🥇 🔀 👈 🔜 👥 📣 Oauth2️⃣ 💂‍♂ ⚖ ⏮️ 2️⃣ 💪 ↔, `me` & `items`. + +`scopes` 🔢 📨 `dict` ⏮️ 🔠 ↔ 🔑 & 📛 💲: + +```Python hl_lines="62-65" +{!../../../docs_src/security/tutorial005.py!} +``` + +↩️ 👥 🔜 📣 📚 ↔, 👫 🔜 🎦 🆙 🛠️ 🩺 🕐❔ 👆 🕹-/✔. + +& 👆 🔜 💪 🖊 ❔ ↔ 👆 💚 🤝 🔐: `me` & `items`. + +👉 🎏 🛠️ ⚙️ 🕐❔ 👆 🤝 ✔ ⏪ 🚨 ⏮️ 👱📔, 🇺🇸🔍, 📂, ♒️: + + + +## 🥙 🤝 ⏮️ ↔ + +🔜, 🔀 🤝 *➡ 🛠️* 📨 ↔ 📨. + +👥 ⚙️ 🎏 `OAuth2PasswordRequestForm`. ⚫️ 🔌 🏠 `scopes` ⏮️ `list` `str`, ⏮️ 🔠 ↔ ⚫️ 📨 📨. + +& 👥 📨 ↔ 🍕 🥙 🤝. + +!!! danger + 🦁, 📥 👥 ❎ ↔ 📨 🔗 🤝. + + ✋️ 👆 🈸, 💂‍♂, 👆 🔜 ⚒ 💭 👆 🕴 🚮 ↔ 👈 👩‍💻 🤙 💪 ✔️, ⚖️ 🕐 👆 ✔️ 🔁. + +```Python hl_lines="155" +{!../../../docs_src/security/tutorial005.py!} +``` + +## 📣 ↔ *➡ 🛠️* & 🔗 + +🔜 👥 📣 👈 *➡ 🛠️* `/users/me/items/` 🚚 ↔ `items`. + +👉, 👥 🗄 & ⚙️ `Security` ⚪️➡️ `fastapi`. + +👆 💪 ⚙️ `Security` 📣 🔗 (💖 `Depends`), ✋️ `Security` 📨 🔢 `scopes` ⏮️ 📇 ↔ (🎻). + +👉 💼, 👥 🚶‍♀️ 🔗 🔢 `get_current_active_user` `Security` (🎏 🌌 👥 🔜 ⏮️ `Depends`). + +✋️ 👥 🚶‍♀️ `list` ↔, 👉 💼 ⏮️ 1️⃣ ↔: `items` (⚫️ 💪 ✔️ 🌅). + +& 🔗 🔢 `get_current_active_user` 💪 📣 🎧-🔗, 🚫 🕴 ⏮️ `Depends` ✋️ ⏮️ `Security`. 📣 🚮 👍 🎧-🔗 🔢 (`get_current_user`), & 🌖 ↔ 📄. + +👉 💼, ⚫️ 🚚 ↔ `me` (⚫️ 💪 🚚 🌅 🌘 1️⃣ ↔). + +!!! note + 👆 🚫 🎯 💪 🚮 🎏 ↔ 🎏 🥉. + + 👥 🔨 ⚫️ 📥 🎦 ❔ **FastAPI** 🍵 ↔ 📣 🎏 🎚. + +```Python hl_lines="4 139 168" +{!../../../docs_src/security/tutorial005.py!} +``` + +!!! info "📡 ℹ" + `Security` 🤙 🏿 `Depends`, & ⚫️ ✔️ 1️⃣ ➕ 🔢 👈 👥 🔜 👀 ⏪. + + ✋️ ⚙️ `Security` ↩️ `Depends`, **FastAPI** 🔜 💭 👈 ⚫️ 💪 📣 💂‍♂ ↔, ⚙️ 👫 🔘, & 📄 🛠️ ⏮️ 🗄. + + ✋️ 🕐❔ 👆 🗄 `Query`, `Path`, `Depends`, `Security` & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. + +## ⚙️ `SecurityScopes` + +🔜 ℹ 🔗 `get_current_user`. + +👉 1️⃣ ⚙️ 🔗 🔛. + +📥 👥 ⚙️ 🎏 Oauth2️⃣ ⚖ 👥 ✍ ⏭, 📣 ⚫️ 🔗: `oauth2_scheme`. + +↩️ 👉 🔗 🔢 🚫 ✔️ 🙆 ↔ 📄 ⚫️, 👥 💪 ⚙️ `Depends` ⏮️ `oauth2_scheme`, 👥 🚫 ✔️ ⚙️ `Security` 🕐❔ 👥 🚫 💪 ✔ 💂‍♂ ↔. + +👥 📣 🎁 🔢 🆎 `SecurityScopes`, 🗄 ⚪️➡️ `fastapi.security`. + +👉 `SecurityScopes` 🎓 🎏 `Request` (`Request` ⚙️ 🤚 📨 🎚 🔗). + +```Python hl_lines="8 105" +{!../../../docs_src/security/tutorial005.py!} +``` + +## ⚙️ `scopes` + +🔢 `security_scopes` 🔜 🆎 `SecurityScopes`. + +⚫️ 🔜 ✔️ 🏠 `scopes` ⏮️ 📇 ⚗ 🌐 ↔ ✔ ⚫️ & 🌐 🔗 👈 ⚙️ 👉 🎧-🔗. 👈 ⛓, 🌐 "⚓️"... 👉 💪 🔊 😨, ⚫️ 🔬 🔄 ⏪ 🔛. + +`security_scopes` 🎚 (🎓 `SecurityScopes`) 🚚 `scope_str` 🔢 ⏮️ 👁 🎻, 🔌 👈 ↔ 👽 🚀 (👥 🔜 ⚙️ ⚫️). + +👥 ✍ `HTTPException` 👈 👥 💪 🏤-⚙️ (`raise`) ⏪ 📚 ☝. + +👉 ⚠, 👥 🔌 ↔ 🚚 (🚥 🙆) 🎻 👽 🚀 (⚙️ `scope_str`). 👥 🚮 👈 🎻 ⚗ ↔ `WWW-Authenticate` 🎚 (👉 🍕 🔌). + +```Python hl_lines="105 107-115" +{!../../../docs_src/security/tutorial005.py!} +``` + +## ✔ `username` & 💽 💠 + +👥 ✔ 👈 👥 🤚 `username`, & ⚗ ↔. + +& ⤴️ 👥 ✔ 👈 📊 ⏮️ Pydantic 🏷 (✊ `ValidationError` ⚠), & 🚥 👥 🤚 ❌ 👂 🥙 🤝 ⚖️ ⚖ 📊 ⏮️ Pydantic, 👥 🤚 `HTTPException` 👥 ✍ ⏭. + +👈, 👥 ℹ Pydantic 🏷 `TokenData` ⏮️ 🆕 🏠 `scopes`. + +⚖ 📊 ⏮️ Pydantic 👥 💪 ⚒ 💭 👈 👥 ✔️, 🖼, ⚫️❔ `list` `str` ⏮️ ↔ & `str` ⏮️ `username`. + +↩️, 🖼, `dict`, ⚖️ 🕳 🙆, ⚫️ 💪 💔 🈸 ☝ ⏪, ⚒ ⚫️ 💂‍♂ ⚠. + +👥 ✔ 👈 👥 ✔️ 👩‍💻 ⏮️ 👈 🆔, & 🚥 🚫, 👥 🤚 👈 🎏 ⚠ 👥 ✍ ⏭. + +```Python hl_lines="46 116-127" +{!../../../docs_src/security/tutorial005.py!} +``` + +## ✔ `scopes` + +👥 🔜 ✔ 👈 🌐 ↔ ✔, 👉 🔗 & 🌐 ⚓️ (🔌 *➡ 🛠️*), 🔌 ↔ 🚚 🤝 📨, ⏪ 🤚 `HTTPException`. + +👉, 👥 ⚙️ `security_scopes.scopes`, 👈 🔌 `list` ⏮️ 🌐 👫 ↔ `str`. + +```Python hl_lines="128-134" +{!../../../docs_src/security/tutorial005.py!} +``` + +## 🔗 🌲 & ↔ + +➡️ 📄 🔄 👉 🔗 🌲 & ↔. + +`get_current_active_user` 🔗 ✔️ 🎧-🔗 🔛 `get_current_user`, ↔ `"me"` 📣 `get_current_active_user` 🔜 🔌 📇 ✔ ↔ `security_scopes.scopes` 🚶‍♀️ `get_current_user`. + +*➡ 🛠️* ⚫️ 📣 ↔, `"items"`, 👉 🔜 📇 `security_scopes.scopes` 🚶‍♀️ `get_current_user`. + +📥 ❔ 🔗 🔗 & ↔ 👀 💖: + +* *➡ 🛠️* `read_own_items` ✔️: + * ✔ ↔ `["items"]` ⏮️ 🔗: + * `get_current_active_user`: + * 🔗 🔢 `get_current_active_user` ✔️: + * ✔ ↔ `["me"]` ⏮️ 🔗: + * `get_current_user`: + * 🔗 🔢 `get_current_user` ✔️: + * 🙅‍♂ ↔ ✔ ⚫️. + * 🔗 ⚙️ `oauth2_scheme`. + * `security_scopes` 🔢 🆎 `SecurityScopes`: + * 👉 `security_scopes` 🔢 ✔️ 🏠 `scopes` ⏮️ `list` ⚗ 🌐 👫 ↔ 📣 🔛,: + * `security_scopes.scopes` 🔜 🔌 `["me", "items"]` *➡ 🛠️* `read_own_items`. + * `security_scopes.scopes` 🔜 🔌 `["me"]` *➡ 🛠️* `read_users_me`, ↩️ ⚫️ 📣 🔗 `get_current_active_user`. + * `security_scopes.scopes` 🔜 🔌 `[]` (🕳) *➡ 🛠️* `read_system_status`, ↩️ ⚫️ 🚫 📣 🙆 `Security` ⏮️ `scopes`, & 🚮 🔗, `get_current_user`, 🚫 📣 🙆 `scope` 👯‍♂️. + +!!! tip + ⚠ & "🎱" 👜 📥 👈 `get_current_user` 🔜 ✔️ 🎏 📇 `scopes` ✅ 🔠 *➡ 🛠️*. + + 🌐 ⚓️ 🔛 `scopes` 📣 🔠 *➡ 🛠️* & 🔠 🔗 🔗 🌲 👈 🎯 *➡ 🛠️*. + +## 🌖 ℹ 🔃 `SecurityScopes` + +👆 💪 ⚙️ `SecurityScopes` 🙆 ☝, & 💗 🥉, ⚫️ 🚫 ✔️ "🌱" 🔗. + +⚫️ 🔜 🕧 ✔️ 💂‍♂ ↔ 📣 ⏮️ `Security` 🔗 & 🌐 ⚓️ **👈 🎯** *➡ 🛠️* & **👈 🎯** 🔗 🌲. + +↩️ `SecurityScopes` 🔜 ✔️ 🌐 ↔ 📣 ⚓️, 👆 💪 ⚙️ ⚫️ ✔ 👈 🤝 ✔️ 🚚 ↔ 🇨🇫 🔗 🔢, & ⤴️ 📣 🎏 ↔ 📄 🎏 *➡ 🛠️*. + +👫 🔜 ✅ ➡ 🔠 *➡ 🛠️*. + +## ✅ ⚫️ + +🚥 👆 📂 🛠️ 🩺, 👆 💪 🔓 & ✔ ❔ ↔ 👆 💚 ✔. + + + +🚥 👆 🚫 🖊 🙆 ↔, 👆 🔜 "🔓", ✋️ 🕐❔ 👆 🔄 🔐 `/users/me/` ⚖️ `/users/me/items/` 👆 🔜 🤚 ❌ 💬 👈 👆 🚫 ✔️ 🥃 ✔. 👆 🔜 💪 🔐 `/status/`. + +& 🚥 👆 🖊 ↔ `me` ✋️ 🚫 ↔ `items`, 👆 🔜 💪 🔐 `/users/me/` ✋️ 🚫 `/users/me/items/`. + +👈 ⚫️❔ 🔜 🔨 🥉 🥳 🈸 👈 🔄 🔐 1️⃣ 👫 *➡ 🛠️* ⏮️ 🤝 🚚 👩‍💻, ⚓️ 🔛 ❔ 📚 ✔ 👩‍💻 🤝 🈸. + +## 🔃 🥉 🥳 🛠️ + +👉 🖼 👥 ⚙️ Oauth2️⃣ "🔐" 💧. + +👉 ☑ 🕐❔ 👥 🚨 👆 👍 🈸, 🎲 ⏮️ 👆 👍 🕸. + +↩️ 👥 💪 💙 ⚫️ 📨 `username` & `password`, 👥 🎛 ⚫️. + +✋️ 🚥 👆 🏗 Oauth2️⃣ 🈸 👈 🎏 🔜 🔗 (➡, 🚥 👆 🏗 🤝 🐕‍🦺 🌓 👱📔, 🇺🇸🔍, 📂, ♒️.) 👆 🔜 ⚙️ 1️⃣ 🎏 💧. + +🌅 ⚠ 🔑 💧. + +🏆 🔐 📟 💧, ✋️ 🌖 🏗 🛠️ ⚫️ 🚚 🌅 📶. ⚫️ 🌅 🏗, 📚 🐕‍🦺 🔚 🆙 ✔ 🔑 💧. + +!!! note + ⚫️ ⚠ 👈 🔠 🤝 🐕‍🦺 📛 👫 💧 🎏 🌌, ⚒ ⚫️ 🍕 👫 🏷. + + ✋️ 🔚, 👫 🛠️ 🎏 Oauth2️⃣ 🐩. + +**FastAPI** 🔌 🚙 🌐 👫 Oauth2️⃣ 🤝 💧 `fastapi.security.oauth2`. + +## `Security` 👨‍🎨 `dependencies` + +🎏 🌌 👆 💪 🔬 `list` `Depends` 👨‍🎨 `dependencies` 🔢 (🔬 [🔗 ➡ 🛠️ 👨‍🎨](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), 👆 💪 ⚙️ `Security` ⏮️ `scopes` 📤. diff --git a/docs/em/docs/advanced/settings.md b/docs/em/docs/advanced/settings.md new file mode 100644 index 0000000000..2ebe8ffcbe --- /dev/null +++ b/docs/em/docs/advanced/settings.md @@ -0,0 +1,382 @@ +# ⚒ & 🌐 🔢 + +📚 💼 👆 🈸 💪 💪 🔢 ⚒ ⚖️ 📳, 🖼 ㊙ 🔑, 💽 🎓, 🎓 📧 🐕‍🦺, ♒️. + +🏆 👫 ⚒ 🔢 (💪 🔀), 💖 💽 📛. & 📚 💪 🚿, 💖 ㊙. + +👉 🤔 ⚫️ ⚠ 🚚 👫 🌐 🔢 👈 ✍ 🈸. + +## 🌐 🔢 + +!!! tip + 🚥 👆 ⏪ 💭 ⚫️❔ "🌐 🔢" & ❔ ⚙️ 👫, 💭 🆓 🚶 ⏭ 📄 🔛. + +🌐 🔢 (💭 "🇨🇻 {") 🔢 👈 🖖 🏞 🐍 📟, 🏃‍♂ ⚙️, & 💪 ✍ 👆 🐍 📟 (⚖️ 🎏 📋 👍). + +👆 💪 ✍ & ⚙️ 🌐 🔢 🐚, 🍵 💆‍♂ 🐍: + +=== "💾, 🇸🇻, 🚪 🎉" + +
+ + ```console + // You could create an env var MY_NAME with + $ export MY_NAME="Wade Wilson" + + // Then you could use it with other programs, like + $ echo "Hello $MY_NAME" + + Hello Wade Wilson + ``` + +
+ +=== "🚪 📋" + +
+ + ```console + // Create an env var MY_NAME + $ $Env:MY_NAME = "Wade Wilson" + + // Use it with other programs, like + $ echo "Hello $Env:MY_NAME" + + Hello Wade Wilson + ``` + +
+ +### ✍ 🇨🇻 {🐍 + +👆 💪 ✍ 🌐 🔢 🏞 🐍, 📶 (⚖️ ⏮️ 🙆 🎏 👩‍🔬), & ⤴️ ✍ 👫 🐍. + +🖼 👆 💪 ✔️ 📁 `main.py` ⏮️: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +!!! tip + 🥈 ❌ `os.getenv()` 🔢 💲 📨. + + 🚥 🚫 🚚, ⚫️ `None` 🔢, 📥 👥 🚚 `"World"` 🔢 💲 ⚙️. + +⤴️ 👆 💪 🤙 👈 🐍 📋: + +
+ +```console +// Here we don't set the env var yet +$ python main.py + +// As we didn't set the env var, we get the default value + +Hello World from Python + +// But if we create an environment variable first +$ export MY_NAME="Wade Wilson" + +// And then call the program again +$ python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python +``` + +
+ +🌐 🔢 💪 ⚒ 🏞 📟, ✋️ 💪 ✍ 📟, & 🚫 ✔️ 🏪 (💕 `git`) ⏮️ 🎂 📁, ⚫️ ⚠ ⚙️ 👫 📳 ⚖️ ⚒. + +👆 💪 ✍ 🌐 🔢 🕴 🎯 📋 👼, 👈 🕴 💪 👈 📋, & 🕴 🚮 📐. + +👈, ✍ ⚫️ ▶️️ ⏭ 📋 ⚫️, 🔛 🎏 ⏸: + +
+ +```console +// Create an env var MY_NAME in line for this program call +$ MY_NAME="Wade Wilson" python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python + +// The env var no longer exists afterwards +$ python main.py + +Hello World from Python +``` + +
+ +!!! tip + 👆 💪 ✍ 🌅 🔃 ⚫️ 1️⃣2️⃣-⚖ 📱: 📁. + +### 🆎 & 🔬 + +👫 🌐 🔢 💪 🕴 🍵 ✍ 🎻, 👫 🔢 🐍 & ✔️ 🔗 ⏮️ 🎏 📋 & 🎂 ⚙️ (& ⏮️ 🎏 🏃‍♂ ⚙️, 💾, 🚪, 🇸🇻). + +👈 ⛓ 👈 🙆 💲 ✍ 🐍 ⚪️➡️ 🌐 🔢 🔜 `str`, & 🙆 🛠️ 🎏 🆎 ⚖️ 🔬 ✔️ 🔨 📟. + +## Pydantic `Settings` + +👐, Pydantic 🚚 👑 🚙 🍵 👫 ⚒ 👟 ⚪️➡️ 🌐 🔢 ⏮️ Pydantic: ⚒ 🧾. + +### ✍ `Settings` 🎚 + +🗄 `BaseSettings` ⚪️➡️ Pydantic & ✍ 🎧-🎓, 📶 🌅 💖 ⏮️ Pydantic 🏷. + +🎏 🌌 ⏮️ Pydantic 🏷, 👆 📣 🎓 🔢 ⏮️ 🆎 ✍, & 🎲 🔢 💲. + +👆 💪 ⚙️ 🌐 🎏 🔬 ⚒ & 🧰 👆 ⚙️ Pydantic 🏷, 💖 🎏 📊 🆎 & 🌖 🔬 ⏮️ `Field()`. + +```Python hl_lines="2 5-8 11" +{!../../../docs_src/settings/tutorial001.py!} +``` + +!!! tip + 🚥 👆 💚 🕳 ⏩ 📁 & 📋, 🚫 ⚙️ 👉 🖼, ⚙️ 🏁 1️⃣ 🔛. + +⤴️, 🕐❔ 👆 ✍ 👐 👈 `Settings` 🎓 (👉 💼, `settings` 🎚), Pydantic 🔜 ✍ 🌐 🔢 💼-😛 🌌,, ↖-💼 🔢 `APP_NAME` 🔜 ✍ 🔢 `app_name`. + +⏭ ⚫️ 🔜 🗜 & ✔ 💽. , 🕐❔ 👆 ⚙️ 👈 `settings` 🎚, 👆 🔜 ✔️ 📊 🆎 👆 📣 (✅ `items_per_user` 🔜 `int`). + +### ⚙️ `settings` + +⤴️ 👆 💪 ⚙️ 🆕 `settings` 🎚 👆 🈸: + +```Python hl_lines="18-20" +{!../../../docs_src/settings/tutorial001.py!} +``` + +### 🏃 💽 + +⏭, 👆 🔜 🏃 💽 🚶‍♀️ 📳 🌐 🔢, 🖼 👆 💪 ⚒ `ADMIN_EMAIL` & `APP_NAME` ⏮️: + +
+ +```console +$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" uvicorn main:app + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +!!! tip + ⚒ 💗 🇨🇻 {👁 📋 🎏 👫 ⏮️ 🚀, & 🚮 👫 🌐 ⏭ 📋. + +& ⤴️ `admin_email` ⚒ 🔜 ⚒ `"deadpool@example.com"`. + +`app_name` 🔜 `"ChimichangApp"`. + +& `items_per_user` 🔜 🚧 🚮 🔢 💲 `50`. + +## ⚒ ➕1️⃣ 🕹 + +👆 💪 🚮 👈 ⚒ ➕1️⃣ 🕹 📁 👆 👀 [🦏 🈸 - 💗 📁](../tutorial/bigger-applications.md){.internal-link target=_blank}. + +🖼, 👆 💪 ✔️ 📁 `config.py` ⏮️: + +```Python +{!../../../docs_src/settings/app01/config.py!} +``` + +& ⤴️ ⚙️ ⚫️ 📁 `main.py`: + +```Python hl_lines="3 11-13" +{!../../../docs_src/settings/app01/main.py!} +``` + +!!! tip + 👆 🔜 💪 📁 `__init__.py` 👆 👀 🔛 [🦏 🈸 - 💗 📁](../tutorial/bigger-applications.md){.internal-link target=_blank}. + +## ⚒ 🔗 + +🍾 ⚫️ 5️⃣📆 ⚠ 🚚 ⚒ ⚪️➡️ 🔗, ↩️ ✔️ 🌐 🎚 ⏮️ `settings` 👈 ⚙️ 🌐. + +👉 💪 ✴️ ⚠ ⏮️ 🔬, ⚫️ 📶 ⏩ 🔐 🔗 ⏮️ 👆 👍 🛃 ⚒. + +### 📁 📁 + +👟 ⚪️➡️ ⏮️ 🖼, 👆 `config.py` 📁 💪 👀 💖: + +```Python hl_lines="10" +{!../../../docs_src/settings/app02/config.py!} +``` + +👀 👈 🔜 👥 🚫 ✍ 🔢 👐 `settings = Settings()`. + +### 👑 📱 📁 + +🔜 👥 ✍ 🔗 👈 📨 🆕 `config.Settings()`. + +```Python hl_lines="5 11-12" +{!../../../docs_src/settings/app02/main.py!} +``` + +!!! tip + 👥 🔜 🔬 `@lru_cache` 🍖. + + 🔜 👆 💪 🤔 `get_settings()` 😐 🔢. + +& ⤴️ 👥 💪 🚚 ⚫️ ⚪️➡️ *➡ 🛠️ 🔢* 🔗 & ⚙️ ⚫️ 🙆 👥 💪 ⚫️. + +```Python hl_lines="16 18-20" +{!../../../docs_src/settings/app02/main.py!} +``` + +### ⚒ & 🔬 + +⤴️ ⚫️ 🔜 📶 ⏩ 🚚 🎏 ⚒ 🎚 ⏮️ 🔬 🏗 🔗 🔐 `get_settings`: + +```Python hl_lines="9-10 13 21" +{!../../../docs_src/settings/app02/test_main.py!} +``` + +🔗 🔐 👥 ⚒ 🆕 💲 `admin_email` 🕐❔ 🏗 🆕 `Settings` 🎚, & ⤴️ 👥 📨 👈 🆕 🎚. + +⤴️ 👥 💪 💯 👈 ⚫️ ⚙️. + +## 👂 `.env` 📁 + +🚥 👆 ✔️ 📚 ⚒ 👈 🎲 🔀 📚, 🎲 🎏 🌐, ⚫️ 5️⃣📆 ⚠ 🚮 👫 🔛 📁 & ⤴️ ✍ 👫 ⚪️➡️ ⚫️ 🚥 👫 🌐 🔢. + +👉 💡 ⚠ 🥃 👈 ⚫️ ✔️ 📛, 👫 🌐 🔢 🛎 🥉 📁 `.env`, & 📁 🤙 "🇨🇻". + +!!! tip + 📁 ▶️ ⏮️ ❣ (`.`) 🕵‍♂ 📁 🖥-💖 ⚙️, 💖 💾 & 🇸🇻. + + ✋️ 🇨🇻 📁 🚫 🤙 ✔️ ✔️ 👈 ☑ 📁. + +Pydantic ✔️ 🐕‍🦺 👂 ⚪️➡️ 👉 🆎 📁 ⚙️ 🔢 🗃. 👆 💪 ✍ 🌖 Pydantic ⚒: 🇨🇻 (.🇨🇻) 🐕‍🦺. + +!!! tip + 👉 👷, 👆 💪 `pip install python-dotenv`. + +### `.env` 📁 + +👆 💪 ✔️ `.env` 📁 ⏮️: + +```bash +ADMIN_EMAIL="deadpool@example.com" +APP_NAME="ChimichangApp" +``` + +### ✍ ⚒ ⚪️➡️ `.env` + +& ⤴️ ℹ 👆 `config.py` ⏮️: + +```Python hl_lines="9-10" +{!../../../docs_src/settings/app03/config.py!} +``` + +📥 👥 ✍ 🎓 `Config` 🔘 👆 Pydantic `Settings` 🎓, & ⚒ `env_file` 📁 ⏮️ 🇨🇻 📁 👥 💚 ⚙️. + +!!! tip + `Config` 🎓 ⚙️ Pydantic 📳. 👆 💪 ✍ 🌖 Pydantic 🏷 📁 + +### 🏗 `Settings` 🕴 🕐 ⏮️ `lru_cache` + +👂 📁 ⚪️➡️ 💾 🛎 ⚠ (🐌) 🛠️, 👆 🎲 💚 ⚫️ 🕴 🕐 & ⤴️ 🏤-⚙️ 🎏 ⚒ 🎚, ↩️ 👂 ⚫️ 🔠 📨. + +✋️ 🔠 🕰 👥: + +```Python +Settings() +``` + +🆕 `Settings` 🎚 🔜 ✍, & 🏗 ⚫️ 🔜 ✍ `.env` 📁 🔄. + +🚥 🔗 🔢 💖: + +```Python +def get_settings(): + return Settings() +``` + +👥 🔜 ✍ 👈 🎚 🔠 📨, & 👥 🔜 👂 `.env` 📁 🔠 📨. 👶 👶 + +✋️ 👥 ⚙️ `@lru_cache` 👨‍🎨 🔛 🔝, `Settings` 🎚 🔜 ✍ 🕴 🕐, 🥇 🕰 ⚫️ 🤙. 👶 👶 + +```Python hl_lines="1 10" +{!../../../docs_src/settings/app03/main.py!} +``` + +⤴️ 🙆 🏁 🤙 `get_settings()` 🔗 ⏭ 📨, ↩️ 🛠️ 🔗 📟 `get_settings()` & 🏗 🆕 `Settings` 🎚, ⚫️ 🔜 📨 🎏 🎚 👈 📨 🔛 🥇 🤙, 🔄 & 🔄. + +#### `lru_cache` 📡 ℹ + +`@lru_cache` 🔀 🔢 ⚫️ 🎀 📨 🎏 💲 👈 📨 🥇 🕰, ↩️ 💻 ⚫️ 🔄, 🛠️ 📟 🔢 🔠 🕰. + +, 🔢 🔛 ⚫️ 🔜 🛠️ 🕐 🔠 🌀 ❌. & ⤴️ 💲 📨 🔠 👈 🌀 ❌ 🔜 ⚙️ 🔄 & 🔄 🕐❔ 🔢 🤙 ⏮️ ⚫️❔ 🎏 🌀 ❌. + +🖼, 🚥 👆 ✔️ 🔢: + +```Python +@lru_cache +def say_hi(name: str, salutation: str = "Ms."): + return f"Hello {salutation} {name}" +``` + +👆 📋 💪 🛠️ 💖 👉: + +```mermaid +sequenceDiagram + +participant code as Code +participant function as say_hi() +participant execute as Execute function + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Camila") + function ->> execute: execute function code + execute ->> code: return the result + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Camila") + function ->> code: return stored result + end + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Rick") + function ->> execute: execute function code + execute ->> code: return the result + end + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Rick", salutation="Mr.") + function ->> execute: execute function code + execute ->> code: return the result + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Rick") + function ->> code: return stored result + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Camila") + function ->> code: return stored result + end +``` + +💼 👆 🔗 `get_settings()`, 🔢 🚫 ✊ 🙆 ❌, ⚫️ 🕧 📨 🎏 💲. + +👈 🌌, ⚫️ 🎭 🌖 🚥 ⚫️ 🌐 🔢. ✋️ ⚫️ ⚙️ 🔗 🔢, ⤴️ 👥 💪 🔐 ⚫️ 💪 🔬. + +`@lru_cache` 🍕 `functools` ❔ 🍕 🐍 🐩 🗃, 👆 💪 ✍ 🌅 🔃 ⚫️ 🐍 🩺 `@lru_cache`. + +## 🌃 + +👆 💪 ⚙️ Pydantic ⚒ 🍵 ⚒ ⚖️ 📳 👆 🈸, ⏮️ 🌐 🏋️ Pydantic 🏷. + +* ⚙️ 🔗 👆 💪 📉 🔬. +* 👆 💪 ⚙️ `.env` 📁 ⏮️ ⚫️. +* ⚙️ `@lru_cache` ➡️ 👆 ❎ 👂 🇨🇻 📁 🔄 & 🔄 🔠 📨, ⏪ 🤝 👆 🔐 ⚫️ ⏮️ 🔬. diff --git a/docs/em/docs/advanced/sub-applications.md b/docs/em/docs/advanced/sub-applications.md new file mode 100644 index 0000000000..e0391453be --- /dev/null +++ b/docs/em/docs/advanced/sub-applications.md @@ -0,0 +1,73 @@ +# 🎧 🈸 - 🗻 + +🚥 👆 💪 ✔️ 2️⃣ 🔬 FastAPI 🈸, ⏮️ 👫 👍 🔬 🗄 & 👫 👍 🩺 ⚜, 👆 💪 ✔️ 👑 📱 & "🗻" 1️⃣ (⚖️ 🌅) 🎧-🈸(Ⓜ). + +## 🗜 **FastAPI** 🈸 + +"🗜" ⛓ ❎ 🍕 "🔬" 🈸 🎯 ➡, 👈 ⤴️ ✊ 💅 🚚 🌐 🔽 👈 ➡, ⏮️ _➡ 🛠️_ 📣 👈 🎧-🈸. + +### 🔝-🎚 🈸 + +🥇, ✍ 👑, 🔝-🎚, **FastAPI** 🈸, & 🚮 *➡ 🛠️*: + +```Python hl_lines="3 6-8" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### 🎧-🈸 + +⤴️, ✍ 👆 🎧-🈸, & 🚮 *➡ 🛠️*. + +👉 🎧-🈸 ➕1️⃣ 🐩 FastAPI 🈸, ✋️ 👉 1️⃣ 👈 🔜 "🗻": + +```Python hl_lines="11 14-16" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### 🗻 🎧-🈸 + +👆 🔝-🎚 🈸, `app`, 🗻 🎧-🈸, `subapi`. + +👉 💼, ⚫️ 🔜 📌 ➡ `/subapi`: + +```Python hl_lines="11 19" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### ✅ 🏧 🛠️ 🩺 + +🔜, 🏃 `uvicorn` ⏮️ 👑 📱, 🚥 👆 📁 `main.py`, ⚫️ 🔜: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +& 📂 🩺 http://127.0.0.1:8000/docs. + +👆 🔜 👀 🏧 🛠️ 🩺 👑 📱, 🔌 🕴 🚮 👍 _➡ 🛠️_: + + + +& ⤴️, 📂 🩺 🎧-🈸, http://127.0.0.1:8000/subapi/docs. + +👆 🔜 👀 🏧 🛠️ 🩺 🎧-🈸, ✅ 🕴 🚮 👍 _➡ 🛠️_, 🌐 🔽 ☑ 🎧-➡ 🔡 `/subapi`: + + + +🚥 👆 🔄 🔗 ⏮️ 🙆 2️⃣ 👩‍💻 🔢, 👫 🔜 👷 ☑, ↩️ 🖥 🔜 💪 💬 🔠 🎯 📱 ⚖️ 🎧-📱. + +### 📡 ℹ: `root_path` + +🕐❔ 👆 🗻 🎧-🈸 🔬 🔛, FastAPI 🔜 ✊ 💅 🔗 🗻 ➡ 🎧-🈸 ⚙️ 🛠️ ⚪️➡️ 🔫 🔧 🤙 `root_path`. + +👈 🌌, 🎧-🈸 🔜 💭 ⚙️ 👈 ➡ 🔡 🩺 🎚. + +& 🎧-🈸 💪 ✔️ 🚮 👍 📌 🎧-🈸 & 🌐 🔜 👷 ☑, ↩️ FastAPI 🍵 🌐 👉 `root_path`Ⓜ 🔁. + +👆 🔜 💡 🌅 🔃 `root_path` & ❔ ⚙️ ⚫️ 🎯 📄 🔃 [⛅ 🗳](./behind-a-proxy.md){.internal-link target=_blank}. diff --git a/docs/em/docs/advanced/templates.md b/docs/em/docs/advanced/templates.md new file mode 100644 index 0000000000..0a73a4f47e --- /dev/null +++ b/docs/em/docs/advanced/templates.md @@ -0,0 +1,77 @@ +# 📄 + +👆 💪 ⚙️ 🙆 📄 🚒 👆 💚 ⏮️ **FastAPI**. + +⚠ ⚒ Jinja2️⃣, 🎏 1️⃣ ⚙️ 🏺 & 🎏 🧰. + +📤 🚙 🔗 ⚫️ 💪 👈 👆 💪 ⚙️ 🔗 👆 **FastAPI** 🈸 (🚚 💃). + +## ❎ 🔗 + +❎ `jinja2`: + +
+ +```console +$ pip install jinja2 + +---> 100% +``` + +
+ +## ⚙️ `Jinja2Templates` + +* 🗄 `Jinja2Templates`. +* ✍ `templates` 🎚 👈 👆 💪 🏤-⚙️ ⏪. +* 📣 `Request` 🔢 *➡ 🛠️* 👈 🔜 📨 📄. +* ⚙️ `templates` 👆 ✍ ✍ & 📨 `TemplateResponse`, 🚶‍♀️ `request` 1️⃣ 🔑-💲 👫 Jinja2️⃣ "🔑". + +```Python hl_lines="4 11 15-18" +{!../../../docs_src/templates/tutorial001.py!} +``` + +!!! note + 👀 👈 👆 ✔️ 🚶‍♀️ `request` 🍕 🔑-💲 👫 🔑 Jinja2️⃣. , 👆 ✔️ 📣 ⚫️ 👆 *➡ 🛠️*. + +!!! tip + 📣 `response_class=HTMLResponse` 🩺 🎚 🔜 💪 💭 👈 📨 🔜 🕸. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.templating import Jinja2Templates`. + + **FastAPI** 🚚 🎏 `starlette.templating` `fastapi.templating` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. 🎏 ⏮️ `Request` & `StaticFiles`. + +## ✍ 📄 + +⤴️ 👆 💪 ✍ 📄 `templates/item.html` ⏮️: + +```jinja hl_lines="7" +{!../../../docs_src/templates/templates/item.html!} +``` + +⚫️ 🔜 🎦 `id` ✊ ⚪️➡️ "🔑" `dict` 👆 🚶‍♀️: + +```Python +{"request": request, "id": id} +``` + +## 📄 & 🎻 📁 + +& 👆 💪 ⚙️ `url_for()` 🔘 📄, & ⚙️ ⚫️, 🖼, ⏮️ `StaticFiles` 👆 📌. + +```jinja hl_lines="4" +{!../../../docs_src/templates/templates/item.html!} +``` + +👉 🖼, ⚫️ 🔜 🔗 🎚 📁 `static/styles.css` ⏮️: + +```CSS hl_lines="4" +{!../../../docs_src/templates/static/styles.css!} +``` + +& ↩️ 👆 ⚙️ `StaticFiles`, 👈 🎚 📁 🔜 🍦 🔁 👆 **FastAPI** 🈸 📛 `/static/styles.css`. + +## 🌅 ℹ + +🌅 ℹ, 🔌 ❔ 💯 📄, ✅ 💃 🩺 🔛 📄. diff --git a/docs/em/docs/advanced/testing-database.md b/docs/em/docs/advanced/testing-database.md new file mode 100644 index 0000000000..93acd710e8 --- /dev/null +++ b/docs/em/docs/advanced/testing-database.md @@ -0,0 +1,95 @@ +# 🔬 💽 + +👆 💪 ⚙️ 🎏 🔗 🔐 ⚪️➡️ [🔬 🔗 ⏮️ 🔐](testing-dependencies.md){.internal-link target=_blank} 📉 💽 🔬. + +👆 💪 💚 ⚒ 🆙 🎏 💽 🔬, 💾 💽 ⏮️ 💯, 🏤-🥧 ⚫️ ⏮️ 🔬 💽, ♒️. + +👑 💭 ⚫️❔ 🎏 👆 👀 👈 ⏮️ 📃. + +## 🚮 💯 🗄 📱 + +➡️ ℹ 🖼 ⚪️➡️ [🗄 (🔗) 💽](../tutorial/sql-databases.md){.internal-link target=_blank} ⚙️ 🔬 💽. + +🌐 📱 📟 🎏, 👆 💪 🚶 🔙 👈 📃 ✅ ❔ ⚫️. + +🕴 🔀 📥 🆕 🔬 📁. + +👆 😐 🔗 `get_db()` 🔜 📨 💽 🎉. + +💯, 👆 💪 ⚙️ 🔗 🔐 📨 👆 *🛃* 💽 🎉 ↩️ 1️⃣ 👈 🔜 ⚙️ 🛎. + +👉 🖼 👥 🔜 ✍ 🍕 💽 🕴 💯. + +## 📁 📊 + +👥 ✍ 🆕 📁 `sql_app/tests/test_sql_app.py`. + +🆕 📁 📊 👀 💖: + +``` hl_lines="9-11" +. +└── sql_app + ├── __init__.py + ├── crud.py + ├── database.py + ├── main.py + ├── models.py + ├── schemas.py + └── tests + ├── __init__.py + └── test_sql_app.py +``` + +## ✍ 🆕 💽 🎉 + +🥇, 👥 ✍ 🆕 💽 🎉 ⏮️ 🆕 💽. + +💯 👥 🔜 ⚙️ 📁 `test.db` ↩️ `sql_app.db`. + +✋️ 🎂 🎉 📟 🌅 ⚖️ 🌘 🎏, 👥 📁 ⚫️. + +```Python hl_lines="8-13" +{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!} +``` + +!!! tip + 👆 💪 📉 ❎ 👈 📟 🚮 ⚫️ 🔢 & ⚙️ ⚫️ ⚪️➡️ 👯‍♂️ `database.py` & `tests/test_sql_app.py`. + + 🦁 & 🎯 🔛 🎯 🔬 📟, 👥 🖨 ⚫️. + +## ✍ 💽 + +↩️ 🔜 👥 🔜 ⚙️ 🆕 💽 🆕 📁, 👥 💪 ⚒ 💭 👥 ✍ 💽 ⏮️: + +```Python +Base.metadata.create_all(bind=engine) +``` + +👈 🛎 🤙 `main.py`, ✋️ ⏸ `main.py` ⚙️ 💽 📁 `sql_app.db`, & 👥 💪 ⚒ 💭 👥 ✍ `test.db` 💯. + +👥 🚮 👈 ⏸ 📥, ⏮️ 🆕 📁. + +```Python hl_lines="16" +{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!} +``` + +## 🔗 🔐 + +🔜 👥 ✍ 🔗 🔐 & 🚮 ⚫️ 🔐 👆 📱. + +```Python hl_lines="19-24 27" +{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!} +``` + +!!! tip + 📟 `override_get_db()` 🌖 ⚫️❔ 🎏 `get_db()`, ✋️ `override_get_db()` 👥 ⚙️ `TestingSessionLocal` 🔬 💽 ↩️. + +## 💯 📱 + +⤴️ 👥 💪 💯 📱 🛎. + +```Python hl_lines="32-47" +{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!} +``` + +& 🌐 🛠️ 👥 ⚒ 💽 ⏮️ 💯 🔜 `test.db` 💽 ↩️ 👑 `sql_app.db`. diff --git a/docs/em/docs/advanced/testing-dependencies.md b/docs/em/docs/advanced/testing-dependencies.md new file mode 100644 index 0000000000..104a6325ed --- /dev/null +++ b/docs/em/docs/advanced/testing-dependencies.md @@ -0,0 +1,49 @@ +# 🔬 🔗 ⏮️ 🔐 + +## 🔑 🔗 ⏮️ 🔬 + +📤 😐 🌐❔ 👆 💪 💚 🔐 🔗 ⏮️ 🔬. + +👆 🚫 💚 ⏮️ 🔗 🏃 (🚫 🙆 🎧-🔗 ⚫️ 💪 ✔️). + +↩️, 👆 💚 🚚 🎏 🔗 👈 🔜 ⚙️ 🕴 ⏮️ 💯 (🎲 🕴 🎯 💯), & 🔜 🚚 💲 👈 💪 ⚙️ 🌐❔ 💲 ⏮️ 🔗 ⚙️. + +### ⚙️ 💼: 🔢 🐕‍🦺 + +🖼 💪 👈 👆 ✔️ 🔢 🤝 🐕‍🦺 👈 👆 💪 🤙. + +👆 📨 ⚫️ 🤝 & ⚫️ 📨 🔓 👩‍💻. + +👉 🐕‍🦺 5️⃣📆 🔌 👆 📍 📨, & 🤙 ⚫️ 💪 ✊ ➕ 🕰 🌘 🚥 👆 ✔️ 🔧 🎁 👩‍💻 💯. + +👆 🎲 💚 💯 🔢 🐕‍🦺 🕐, ✋️ 🚫 🎯 🤙 ⚫️ 🔠 💯 👈 🏃. + +👉 💼, 👆 💪 🔐 🔗 👈 🤙 👈 🐕‍🦺, & ⚙️ 🛃 🔗 👈 📨 🎁 👩‍💻, 🕴 👆 💯. + +### ⚙️ `app.dependency_overrides` 🔢 + +👫 💼, 👆 **FastAPI** 🈸 ✔️ 🔢 `app.dependency_overrides`, ⚫️ 🙅 `dict`. + +🔐 🔗 🔬, 👆 🚮 🔑 ⏮️ 🔗 (🔢), & 💲, 👆 🔗 🔐 (➕1️⃣ 🔢). + +& ⤴️ **FastAPI** 🔜 🤙 👈 🔐 ↩️ ⏮️ 🔗. + +```Python hl_lines="28-29 32" +{!../../../docs_src/dependency_testing/tutorial001.py!} +``` + +!!! tip + 👆 💪 ⚒ 🔗 🔐 🔗 ⚙️ 🙆 👆 **FastAPI** 🈸. + + ⏮️ 🔗 💪 ⚙️ *➡ 🛠️ 🔢*, *➡ 🛠️ 👨‍🎨* (🕐❔ 👆 🚫 ⚙️ 📨 💲), `.include_router()` 🤙, ♒️. + + FastAPI 🔜 💪 🔐 ⚫️. + +⤴️ 👆 💪 ⏲ 👆 🔐 (❎ 👫) ⚒ `app.dependency_overrides` 🛁 `dict`: + +```Python +app.dependency_overrides = {} +``` + +!!! tip + 🚥 👆 💚 🔐 🔗 🕴 ⏮️ 💯, 👆 💪 ⚒ 🔐 ▶️ 💯 (🔘 💯 🔢) & ⏲ ⚫️ 🔚 (🔚 💯 🔢). diff --git a/docs/em/docs/advanced/testing-events.md b/docs/em/docs/advanced/testing-events.md new file mode 100644 index 0000000000..d64436eb9c --- /dev/null +++ b/docs/em/docs/advanced/testing-events.md @@ -0,0 +1,7 @@ +# 🔬 🎉: 🕴 - 🤫 + +🕐❔ 👆 💪 👆 🎉 🐕‍🦺 (`startup` & `shutdown`) 🏃 👆 💯, 👆 💪 ⚙️ `TestClient` ⏮️ `with` 📄: + +```Python hl_lines="9-12 20-24" +{!../../../docs_src/app_testing/tutorial003.py!} +``` diff --git a/docs/em/docs/advanced/testing-websockets.md b/docs/em/docs/advanced/testing-websockets.md new file mode 100644 index 0000000000..3b8e7e4208 --- /dev/null +++ b/docs/em/docs/advanced/testing-websockets.md @@ -0,0 +1,12 @@ +# 🔬 *️⃣ + +👆 💪 ⚙️ 🎏 `TestClient` 💯*️⃣. + +👉, 👆 ⚙️ `TestClient` `with` 📄, 🔗*️⃣: + +```Python hl_lines="27-31" +{!../../../docs_src/app_testing/tutorial002.py!} +``` + +!!! note + 🌅 ℹ, ✅ 💃 🧾 🔬 *️⃣ . diff --git a/docs/em/docs/advanced/using-request-directly.md b/docs/em/docs/advanced/using-request-directly.md new file mode 100644 index 0000000000..faeadb1aa2 --- /dev/null +++ b/docs/em/docs/advanced/using-request-directly.md @@ -0,0 +1,52 @@ +# ⚙️ 📨 🔗 + +🆙 🔜, 👆 ✔️ 📣 🍕 📨 👈 👆 💪 ⏮️ 👫 🆎. + +✊ 📊 ⚪️➡️: + +* ➡ 🔢. +* 🎚. +* 🍪. +* ♒️. + +& 🔨, **FastAPI** ⚖ 👈 💽, 🏭 ⚫️ & 🏭 🧾 👆 🛠️ 🔁. + +✋️ 📤 ⚠ 🌐❔ 👆 💪 💪 🔐 `Request` 🎚 🔗. + +## ℹ 🔃 `Request` 🎚 + +**FastAPI** 🤙 **💃** 🔘, ⏮️ 🧽 📚 🧰 🔛 🔝, 👆 💪 ⚙️ 💃 `Request` 🎚 🔗 🕐❔ 👆 💪. + +⚫️ 🔜 ⛓ 👈 🚥 👆 🤚 📊 ⚪️➡️ `Request` 🎚 🔗 (🖼, ✍ 💪) ⚫️ 🏆 🚫 ✔, 🗜 ⚖️ 📄 (⏮️ 🗄, 🏧 🛠️ 👩‍💻 🔢) FastAPI. + +👐 🙆 🎏 🔢 📣 🛎 (🖼, 💪 ⏮️ Pydantic 🏷) 🔜 ✔, 🗜, ✍, ♒️. + +✋️ 📤 🎯 💼 🌐❔ ⚫️ ⚠ 🤚 `Request` 🎚. + +## ⚙️ `Request` 🎚 🔗 + +➡️ 🌈 👆 💚 🤚 👩‍💻 📢 📢/🦠 🔘 👆 *➡ 🛠️ 🔢*. + +👈 👆 💪 🔐 📨 🔗. + +```Python hl_lines="1 7-8" +{!../../../docs_src/using_request_directly/tutorial001.py!} +``` + +📣 *➡ 🛠️ 🔢* 🔢 ⏮️ 🆎 ➖ `Request` **FastAPI** 🔜 💭 🚶‍♀️ `Request` 👈 🔢. + +!!! tip + 🗒 👈 👉 💼, 👥 📣 ➡ 🔢 ⤴️ 📨 🔢. + + , ➡ 🔢 🔜 ⚗, ✔, 🗜 ✔ 🆎 & ✍ ⏮️ 🗄. + + 🎏 🌌, 👆 💪 📣 🙆 🎏 🔢 🛎, & ➡, 🤚 `Request` 💁‍♂️. + +## `Request` 🧾 + +👆 💪 ✍ 🌅 ℹ 🔃 `Request` 🎚 🛂 💃 🧾 🕸. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.requests import Request`. + + **FastAPI** 🚚 ⚫️ 🔗 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. diff --git a/docs/em/docs/advanced/websockets.md b/docs/em/docs/advanced/websockets.md new file mode 100644 index 0000000000..6ba9b999dd --- /dev/null +++ b/docs/em/docs/advanced/websockets.md @@ -0,0 +1,184 @@ +# *️⃣ + +👆 💪 ⚙️ *️⃣ ⏮️ **FastAPI**. + +## ❎ `WebSockets` + +🥇 👆 💪 ❎ `WebSockets`: + +
+ +```console +$ pip install websockets + +---> 100% +``` + +
+ +## *️⃣ 👩‍💻 + +### 🏭 + +👆 🏭 ⚙️, 👆 🎲 ✔️ 🕸 ✍ ⏮️ 🏛 🛠️ 💖 😥, Vue.js ⚖️ 📐. + +& 🔗 ⚙️ *️⃣ ⏮️ 👆 👩‍💻 👆 🔜 🎲 ⚙️ 👆 🕸 🚙. + +⚖️ 👆 💪 ✔️ 🇦🇸 📱 🈸 👈 🔗 ⏮️ 👆 *️⃣ 👩‍💻 🔗, 🇦🇸 📟. + +⚖️ 👆 5️⃣📆 ✔️ 🙆 🎏 🌌 🔗 ⏮️ *️⃣ 🔗. + +--- + +✋️ 👉 🖼, 👥 🔜 ⚙️ 📶 🙅 🕸 📄 ⏮️ 🕸, 🌐 🔘 📏 🎻. + +👉, ↗️, 🚫 ⚖ & 👆 🚫🔜 ⚙️ ⚫️ 🏭. + +🏭 👆 🔜 ✔️ 1️⃣ 🎛 🔛. + +✋️ ⚫️ 🙅 🌌 🎯 🔛 💽-🚄 *️⃣ & ✔️ 👷 🖼: + +```Python hl_lines="2 6-38 41-43" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +## ✍ `websocket` + +👆 **FastAPI** 🈸, ✍ `websocket`: + +```Python hl_lines="1 46-47" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.websockets import WebSocket`. + + **FastAPI** 🚚 🎏 `WebSocket` 🔗 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. + +## ⌛ 📧 & 📨 📧 + +👆 *️⃣ 🛣 👆 💪 `await` 📧 & 📨 📧. + +```Python hl_lines="48-52" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +👆 💪 📨 & 📨 💱, ✍, & 🎻 💽. + +## 🔄 ⚫️ + +🚥 👆 📁 📛 `main.py`, 🏃 👆 🈸 ⏮️: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +📂 👆 🖥 http://127.0.0.1:8000. + +👆 🔜 👀 🙅 📃 💖: + + + +👆 💪 🆎 📧 🔢 📦, & 📨 👫: + + + +& 👆 **FastAPI** 🈸 ⏮️ *️⃣ 🔜 📨 🔙: + + + +👆 💪 📨 (& 📨) 📚 📧: + + + +& 🌐 👫 🔜 ⚙️ 🎏 *️⃣ 🔗. + +## ⚙️ `Depends` & 🎏 + +*️⃣ 🔗 👆 💪 🗄 ⚪️➡️ `fastapi` & ⚙️: + +* `Depends` +* `Security` +* `Cookie` +* `Header` +* `Path` +* `Query` + +👫 👷 🎏 🌌 🎏 FastAPI 🔗/*➡ 🛠️*: + +```Python hl_lines="66-77 76-91" +{!../../../docs_src/websockets/tutorial002.py!} +``` + +!!! info + 👉 *️⃣ ⚫️ 🚫 🤙 ⚒ 🔑 🤚 `HTTPException`, ↩️ 👥 🤚 `WebSocketException`. + + 👆 💪 ⚙️ 📪 📟 ⚪️➡️ ☑ 📟 🔬 🔧. + +### 🔄 *️⃣ ⏮️ 🔗 + +🚥 👆 📁 📛 `main.py`, 🏃 👆 🈸 ⏮️: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +📂 👆 🖥 http://127.0.0.1:8000. + +📤 👆 💪 ⚒: + +* "🏬 🆔", ⚙️ ➡. +* "🤝" ⚙️ 🔢 🔢. + +!!! tip + 👀 👈 🔢 `token` 🔜 🍵 🔗. + +⏮️ 👈 👆 💪 🔗 *️⃣ & ⤴️ 📨 & 📨 📧: + + + +## 🚚 🔀 & 💗 👩‍💻 + +🕐❔ *️⃣ 🔗 📪, `await websocket.receive_text()` 🔜 🤚 `WebSocketDisconnect` ⚠, ❔ 👆 💪 ⤴️ ✊ & 🍵 💖 👉 🖼. + +```Python hl_lines="81-83" +{!../../../docs_src/websockets/tutorial003.py!} +``` + +🔄 ⚫️ 👅: + +* 📂 📱 ⏮️ 📚 🖥 📑. +* ✍ 📧 ⚪️➡️ 👫. +* ⤴️ 🔐 1️⃣ 📑. + +👈 🔜 🤚 `WebSocketDisconnect` ⚠, & 🌐 🎏 👩‍💻 🔜 📨 📧 💖: + +``` +Client #1596980209979 left the chat +``` + +!!! tip + 📱 🔛 ⭐ & 🙅 🖼 🎦 ❔ 🍵 & 📻 📧 📚 *️⃣ 🔗. + + ✋️ ✔️ 🤯 👈, 🌐 🍵 💾, 👁 📇, ⚫️ 🔜 🕴 👷 ⏪ 🛠️ 🏃, & 🔜 🕴 👷 ⏮️ 👁 🛠️. + + 🚥 👆 💪 🕳 ⏩ 🛠️ ⏮️ FastAPI ✋️ 👈 🌖 🏋️, 🐕‍🦺 ✳, ✳ ⚖️ 🎏, ✅ 🗜/📻. + +## 🌅 ℹ + +💡 🌅 🔃 🎛, ✅ 💃 🧾: + +* `WebSocket` 🎓. +* 🎓-⚓️ *️⃣ 🚚. diff --git a/docs/em/docs/advanced/wsgi.md b/docs/em/docs/advanced/wsgi.md new file mode 100644 index 0000000000..4d051807fb --- /dev/null +++ b/docs/em/docs/advanced/wsgi.md @@ -0,0 +1,37 @@ +# ✅ 🇨🇻 - 🏺, ✳, 🎏 + +👆 💪 🗻 🇨🇻 🈸 👆 👀 ⏮️ [🎧 🈸 - 🗻](./sub-applications.md){.internal-link target=_blank}, [⛅ 🗳](./behind-a-proxy.md){.internal-link target=_blank}. + +👈, 👆 💪 ⚙️ `WSGIMiddleware` & ⚙️ ⚫️ 🎁 👆 🇨🇻 🈸, 🖼, 🏺, ✳, ♒️. + +## ⚙️ `WSGIMiddleware` + +👆 💪 🗄 `WSGIMiddleware`. + +⤴️ 🎁 🇨🇻 (✅ 🏺) 📱 ⏮️ 🛠️. + +& ⤴️ 🗻 👈 🔽 ➡. + +```Python hl_lines="2-3 22" +{!../../../docs_src/wsgi/tutorial001.py!} +``` + +## ✅ ⚫️ + +🔜, 🔠 📨 🔽 ➡ `/v1/` 🔜 🍵 🏺 🈸. + +& 🎂 🔜 🍵 **FastAPI**. + +🚥 👆 🏃 ⚫️ ⏮️ Uvicorn & 🚶 http://localhost:8000/v1/ 👆 🔜 👀 📨 ⚪️➡️ 🏺: + +```txt +Hello, World from Flask! +``` + +& 🚥 👆 🚶 http://localhost:8000/v2 👆 🔜 👀 📨 ⚪️➡️ FastAPI: + +```JSON +{ + "message": "Hello World" +} +``` diff --git a/docs/em/docs/alternatives.md b/docs/em/docs/alternatives.md new file mode 100644 index 0000000000..6169aa52d7 --- /dev/null +++ b/docs/em/docs/alternatives.md @@ -0,0 +1,414 @@ +# 🎛, 🌈 & 🔺 + +⚫️❔ 😮 **FastAPI**, ❔ ⚫️ 🔬 🎏 🎛 & ⚫️❔ ⚫️ 🇭🇲 ⚪️➡️ 👫. + +## 🎶 + +**FastAPI** 🚫🔜 🔀 🚥 🚫 ⏮️ 👷 🎏. + +📤 ✔️ 📚 🧰 ✍ ⏭ 👈 ✔️ ℹ 😮 🚮 🏗. + +👤 ✔️ ❎ 🏗 🆕 🛠️ 📚 1️⃣2️⃣🗓️. 🥇 👤 🔄 ❎ 🌐 ⚒ 📔 **FastAPI** ⚙️ 📚 🎏 🛠️, 🔌-🔌, & 🧰. + +✋️ ☝, 📤 🙅‍♂ 🎏 🎛 🌘 🏗 🕳 👈 🚚 🌐 👫 ⚒, ✊ 🏆 💭 ⚪️➡️ ⏮️ 🧰, & 🌀 👫 🏆 🌌 💪, ⚙️ 🇪🇸 ⚒ 👈 ➖🚫 💪 ⏭ (🐍 3️⃣.6️⃣ ➕ 🆎 🔑). + +## ⏮️ 🧰 + +### + +⚫️ 🌅 🌟 🐍 🛠️ & 🛎 🕴. ⚫️ ⚙️ 🏗 ⚙️ 💖 👱📔. + +⚫️ 📶 😆 🔗 ⏮️ 🔗 💽 (💖 ✳ ⚖️ ✳),, ✔️ ☁ 💽 (💖 🗄, ✳, 👸, ♒️) 👑 🏪 🚒 🚫 📶 ⏩. + +⚫️ ✍ 🏗 🕸 👩‍💻, 🚫 ✍ 🔗 ⚙️ 🏛 🕸 (💖 😥, Vue.js & 📐) ⚖️ 🎏 ⚙️ (💖 📳) 🔗 ⏮️ ⚫️. + +### ✳ 🎂 🛠️ + +✳ 🎂 🛠️ ✍ 🗜 🧰 🏗 🕸 🔗 ⚙️ ✳ 🔘, 📉 🚮 🛠️ 🛠️. + +⚫️ ⚙️ 📚 🏢 ✅ 🦎, 🟥 👒 & 🎟. + +⚫️ 🕐 🥇 🖼 **🏧 🛠️ 🧾**, & 👉 🎯 🕐 🥇 💭 👈 😮 "🔎" **FastAPI**. + +!!! note + ✳ 🎂 🛠️ ✍ ✡ 🇺🇸🏛. 🎏 👼 💃 & Uvicorn, 🔛 ❔ **FastAPI** ⚓️. + + +!!! check "😮 **FastAPI** " + ✔️ 🏧 🛠️ 🧾 🕸 👩‍💻 🔢. + +### 🏺 + +🏺 "🕸", ⚫️ 🚫 🔌 💽 🛠️ 🚫 📚 👜 👈 👟 🔢 ✳. + +👉 🦁 & 💪 ✔ 🔨 👜 💖 ⚙️ ☁ 💽 👑 💽 💾 ⚙️. + +⚫️ 📶 🙅, ⚫️ 📶 🏋️ 💡, 👐 🧾 🤚 🙁 📡 ☝. + +⚫️ 🛎 ⚙️ 🎏 🈸 👈 🚫 🎯 💪 💽, 👩‍💻 🧾, ⚖️ 🙆 📚 ⚒ 👈 👟 🏤-🏗 ✳. 👐 📚 👫 ⚒ 💪 🚮 ⏮️ 🔌-🔌. + +👉 ⚖ 🍕, & ➖ "🕸" 👈 💪 ↔ 📔 ⚫️❔ ⚫️❔ 💪 🔑 ⚒ 👈 👤 💚 🚧. + +👐 🦁 🏺, ⚫️ 😑 💖 👍 🏏 🏗 🔗. ⏭ 👜 🔎 "✳ 🎂 🛠️" 🏺. + +!!! check "😮 **FastAPI** " + ◾-🛠️. ⚒ ⚫️ ⏩ 🌀 & 🏏 🧰 & 🍕 💪. + + ✔️ 🙅 & ⏩ ⚙️ 🕹 ⚙️. + + +### 📨 + +**FastAPI** 🚫 🤙 🎛 **📨**. 👫 ↔ 📶 🎏. + +⚫️ 🔜 🤙 ⚠ ⚙️ 📨 *🔘* FastAPI 🈸. + +✋️, FastAPI 🤚 🌈 ⚪️➡️ 📨. + +**📨** 🗃 *🔗* ⏮️ 🔗 (👩‍💻), ⏪ **FastAPI** 🗃 *🏗* 🔗 (💽). + +👫, 🌖 ⚖️ 🌘, 🔄 🔚, 🔗 🔠 🎏. + +📨 ✔️ 📶 🙅 & 🏋️ 🔧, ⚫️ 📶 ⏩ ⚙️, ⏮️ 🤔 🔢. ✋️ 🎏 🕰, ⚫️ 📶 🏋️ & 🛃. + +👈 ⚫️❔, 💬 🛂 🕸: + +> 📨 1️⃣ 🏆 ⏬ 🐍 📦 🌐 🕰 + +🌌 👆 ⚙️ ⚫️ 📶 🙅. 🖼, `GET` 📨, 👆 🔜 ✍: + +```Python +response = requests.get("http://example.com/some/url") +``` + +FastAPI 😑 🛠️ *➡ 🛠️* 💪 👀 💖: + +```Python hl_lines="1" +@app.get("/some/url") +def read_url(): + return {"message": "Hello World"} +``` + +👀 🔀 `requests.get(...)` & `@app.get(...)`. + +!!! check "😮 **FastAPI** " + * ✔️ 🙅 & 🏋️ 🛠️. + * ⚙️ 🇺🇸🔍 👩‍🔬 📛 (🛠️) 🔗, 🎯 & 🏋️ 🌌. + * ✔️ 🤔 🔢, ✋️ 🏋️ 🛃. + + +### 🦁 / 🗄 + +👑 ⚒ 👤 💚 ⚪️➡️ ✳ 🎂 🛠️ 🏧 🛠️ 🧾. + +⤴️ 👤 🔎 👈 📤 🐩 📄 🔗, ⚙️ 🎻 (⚖️ 📁, ↔ 🎻) 🤙 🦁. + +& 📤 🕸 👩‍💻 🔢 🦁 🛠️ ⏪ ✍. , 💆‍♂ 💪 🏗 🦁 🧾 🛠️ 🔜 ✔ ⚙️ 👉 🕸 👩‍💻 🔢 🔁. + +☝, 🦁 👐 💾 🏛, 📁 🗄. + +👈 ⚫️❔ 🕐❔ 💬 🔃 ⏬ 2️⃣.0️⃣ ⚫️ ⚠ 💬 "🦁", & ⏬ 3️⃣ ➕ "🗄". + +!!! check "😮 **FastAPI** " + 🛠️ & ⚙️ 📂 🐩 🛠️ 🔧, ↩️ 🛃 🔗. + + & 🛠️ 🐩-⚓️ 👩‍💻 🔢 🧰: + + * 🦁 🎚 + * 📄 + + 👫 2️⃣ 👐 ➖ 📶 🌟 & ⚖, ✋️ 🔨 ⏩ 🔎, 👆 💪 🔎 💯 🌖 🎛 👩‍💻 🔢 🗄 (👈 👆 💪 ⚙️ ⏮️ **FastAPI**). + +### 🏺 🎂 🛠️ + +📤 📚 🏺 🎂 🛠️, ✋️ ⏮️ 💰 🕰 & 👷 🔘 🔬 👫, 👤 🔎 👈 📚 😞 ⚖️ 🚫, ⏮️ 📚 🧍 ❔ 👈 ⚒ 👫 🙃. + +### 🍭 + +1️⃣ 👑 ⚒ 💪 🛠️ ⚙️ 📊 "🛠️" ❔ ✊ 📊 ⚪️➡️ 📟 (🐍) & 🏭 ⚫️ 🔘 🕳 👈 💪 📨 🔘 🕸. 🖼, 🏭 🎚 ⚗ 📊 ⚪️➡️ 💽 🔘 🎻 🎚. 🏭 `datetime` 🎚 🔘 🎻, ♒️. + +➕1️⃣ 🦏 ⚒ 💚 🔗 💽 🔬, ⚒ 💭 👈 💽 ☑, 🤝 🎯 🔢. 🖼, 👈 🏑 `int`, & 🚫 🎲 🎻. 👉 ✴️ ⚠ 📨 💽. + +🍵 💽 🔬 ⚙️, 👆 🔜 ✔️ 🌐 ✅ ✋, 📟. + +👫 ⚒ ⚫️❔ 🍭 🏗 🚚. ⚫️ 👑 🗃, & 👤 ✔️ ⚙️ ⚫️ 📚 ⏭. + +✋️ ⚫️ ✍ ⏭ 📤 🔀 🐍 🆎 🔑. , 🔬 🔠 🔗 👆 💪 ⚙️ 🎯 🇨🇻 & 🎓 🚚 🍭. + +!!! check "😮 **FastAPI** " + ⚙️ 📟 🔬 "🔗" 👈 🚚 💽 🆎 & 🔬, 🔁. + +### Webarg + +➕1️⃣ 🦏 ⚒ ✔ 🔗 📊 ⚪️➡️ 📨 📨. + +Webarg 🧰 👈 ⚒ 🚚 👈 🔛 🔝 📚 🛠️, 🔌 🏺. + +⚫️ ⚙️ 🍭 🔘 💽 🔬. & ⚫️ ✍ 🎏 👩‍💻. + +⚫️ 👑 🧰 & 👤 ✔️ ⚙️ ⚫️ 📚 💁‍♂️, ⏭ ✔️ **FastAPI**. + +!!! info + Webarg ✍ 🎏 🍭 👩‍💻. + +!!! check "😮 **FastAPI** " + ✔️ 🏧 🔬 📨 📨 💽. + +### APISpec + +🍭 & Webarg 🚚 🔬, ✍ & 🛠️ 🔌-🔌. + +✋️ 🧾 ❌. ⤴️ APISpec ✍. + +⚫️ 🔌-📚 🛠️ (& 📤 🔌-💃 💁‍♂️). + +🌌 ⚫️ 👷 👈 👆 ✍ 🔑 🔗 ⚙️ 📁 📁 🔘 #️⃣ 🔠 🔢 🚚 🛣. + +& ⚫️ 🏗 🗄 🔗. + +👈 ❔ ⚫️ 👷 🏺, 💃, 🆘, ♒️. + +✋️ ⤴️, 👥 ✔️ 🔄 ⚠ ✔️ ◾-❕, 🔘 🐍 🎻 (🦏 📁). + +👨‍🎨 💪 🚫 ℹ 🌅 ⏮️ 👈. & 🚥 👥 🔀 🔢 ⚖️ 🍭 🔗 & 💭 🔀 👈 📁#️⃣, 🏗 🔗 🔜 ❌. + +!!! info + APISpec ✍ 🎏 🍭 👩‍💻. + + +!!! check "😮 **FastAPI** " + 🐕‍🦺 📂 🐩 🛠️, 🗄. + +### 🏺-Apispec + +⚫️ 🏺 🔌 -, 👈 👔 👯‍♂️ Webarg, 🍭 & APISpec. + +⚫️ ⚙️ ℹ ⚪️➡️ Webarg & 🍭 🔁 🏗 🗄 🔗, ⚙️ APISpec. + +⚫️ 👑 🧰, 📶 🔽-📈. ⚫️ 🔜 🌌 🌖 🌟 🌘 📚 🏺 🔌-🔌 👅 📤. ⚫️ 💪 ↩️ 🚮 🧾 ➖ 💁‍♂️ 🩲 & 📝. + +👉 ❎ ✔️ ✍ 📁 (➕1️⃣ ❕) 🔘 🐍 ✍. + +👉 🌀 🏺, 🏺-Apispec ⏮️ 🍭 & Webarg 👇 💕 👩‍💻 📚 ⏭ 🏗 **FastAPI**. + +⚙️ ⚫️ ↘️ 🏗 📚 🏺 🌕-📚 🚂. 👫 👑 📚 👤 (& 📚 🔢 🏉) ✔️ ⚙️ 🆙 🔜: + +* https://github.com/tiangolo/full-stack +* https://github.com/tiangolo/full-stack-flask-couchbase +* https://github.com/tiangolo/full-stack-flask-couchdb + +& 👫 🎏 🌕-📚 🚂 🧢 [**FastAPI** 🏗 🚂](project-generation.md){.internal-link target=_blank}. + +!!! info + 🏺-Apispec ✍ 🎏 🍭 👩‍💻. + +!!! check "😮 **FastAPI** " + 🏗 🗄 🔗 🔁, ⚪️➡️ 🎏 📟 👈 🔬 🛠️ & 🔬. + +### NestJS (& 📐) + +👉 ➖🚫 🚫 🐍, NestJS 🕸 (📕) ✳ 🛠️ 😮 📐. + +⚫️ 🏆 🕳 🙁 🎏 ⚫️❔ 💪 🔨 ⏮️ 🏺-Apispec. + +⚫️ ✔️ 🛠️ 🔗 💉 ⚙️, 😮 📐 2️⃣. ⚫️ 🚚 🏤-® "💉" (💖 🌐 🎏 🔗 💉 ⚙️ 👤 💭),, ⚫️ 🚮 🎭 & 📟 🔁. + +🔢 🔬 ⏮️ 📕 🆎 (🎏 🐍 🆎 🔑), 👨‍🎨 🐕‍🦺 👍. + +✋️ 📕 📊 🚫 🛡 ⏮️ 📹 🕸, ⚫️ 🚫🔜 ⚓️ 🔛 🆎 🔬 🔬, 🛠️ & 🧾 🎏 🕰. ↩️ 👉 & 🔧 🚫, 🤚 🔬, 🛠️ & 🏧 🔗 ⚡, ⚫️ 💪 🚮 👨‍🎨 📚 🥉. , ⚫️ ▶️️ 🔁. + +⚫️ 💪 🚫 🍵 🔁 🏷 📶 👍. , 🚥 🎻 💪 📨 🎻 🎚 👈 ✔️ 🔘 🏑 👈 🔄 🐦 🎻 🎚, ⚫️ 🚫🔜 ☑ 📄 & ✔. + +!!! check "😮 **FastAPI** " + ⚙️ 🐍 🆎 ✔️ 👑 👨‍🎨 🐕‍🦺. + + ✔️ 🏋️ 🔗 💉 ⚙️. 🔎 🌌 📉 📟 🔁. + +### 🤣 + +⚫️ 🕐 🥇 📶 ⏩ 🐍 🛠️ ⚓️ 🔛 `asyncio`. ⚫️ ⚒ 📶 🎏 🏺. + +!!! note "📡 ℹ" + ⚫️ ⚙️ `uvloop` ↩️ 🔢 🐍 `asyncio` ➰. 👈 ⚫️❔ ⚒ ⚫️ ⏩. + + ⚫️ 🎯 😮 Uvicorn & 💃, 👈 ⏳ ⏩ 🌘 🤣 📂 📇. + +!!! check "😮 **FastAPI** " + 🔎 🌌 ✔️ 😜 🎭. + + 👈 ⚫️❔ **FastAPI** ⚓️ 🔛 💃, ⚫️ ⏩ 🛠️ 💪 (💯 🥉-🥳 📇). + +### 🦅 + +🦅 ➕1️⃣ ↕ 🎭 🐍 🛠️, ⚫️ 🔧 ⭐, & 👷 🏛 🎏 🛠️ 💖 🤗. + +⚫️ 🏗 ✔️ 🔢 👈 📨 2️⃣ 🔢, 1️⃣ "📨" & 1️⃣ "📨". ⤴️ 👆 "✍" 🍕 ⚪️➡️ 📨, & "✍" 🍕 📨. ↩️ 👉 🔧, ⚫️ 🚫 💪 📣 📨 🔢 & 💪 ⏮️ 🐩 🐍 🆎 🔑 🔢 🔢. + +, 💽 🔬, 🛠️, & 🧾, ✔️ ⌛ 📟, 🚫 🔁. ⚖️ 👫 ✔️ 🛠️ 🛠️ 🔛 🔝 🦅, 💖 🤗. 👉 🎏 🔺 🔨 🎏 🛠️ 👈 😮 🦅 🔧, ✔️ 1️⃣ 📨 🎚 & 1️⃣ 📨 🎚 🔢. + +!!! check "😮 **FastAPI** " + 🔎 🌌 🤚 👑 🎭. + + ⤴️ ⏮️ 🤗 (🤗 ⚓️ 🔛 🦅) 😮 **FastAPI** 📣 `response` 🔢 🔢. + + 👐 FastAPI ⚫️ 📦, & ⚙️ ✴️ ⚒ 🎚, 🍪, & 🎛 👔 📟. + +### + +👤 🔎 ♨ 🥇 ▶️ 🏗 **FastAPI**. & ⚫️ ✔️ 🎏 💭: + +* ⚓️ 🔛 🐍 🆎 🔑. +* 🔬 & 🧾 ⚪️➡️ 👫 🆎. +* 🔗 💉 ⚙️. + +⚫️ 🚫 ⚙️ 💽 🔬, 🛠️ & 🧾 🥉-🥳 🗃 💖 Pydantic, ⚫️ ✔️ 🚮 👍. , 👫 💽 🆎 🔑 🔜 🚫 ♻ 💪. + +⚫️ 🚚 🐥 🍖 🌅 🔁 📳. & ⚫️ ⚓️ 🔛 🇨🇻 (↩️ 🔫), ⚫️ 🚫 🔧 ✊ 📈 ↕-🎭 🚚 🧰 💖 Uvicorn, 💃 & 🤣. + +🔗 💉 ⚙️ 🚚 🏤-® 🔗 & 🔗 ❎ 🧢 🔛 📣 🆎. , ⚫️ 🚫 💪 📣 🌅 🌘 1️⃣ "🦲" 👈 🚚 🎯 🆎. + +🛣 📣 👁 🥉, ⚙️ 🔢 📣 🎏 🥉 (↩️ ⚙️ 👨‍🎨 👈 💪 🥉 ▶️️ 🔛 🔝 🔢 👈 🍵 🔗). 👉 🔐 ❔ ✳ 🔨 ⚫️ 🌘 ❔ 🏺 (& 💃) 🔨 ⚫️. ⚫️ 🎏 📟 👜 👈 📶 😆 🔗. + +!!! check "😮 **FastAPI** " + 🔬 ➕ 🔬 💽 🆎 ⚙️ "🔢" 💲 🏷 🔢. 👉 📉 👨‍🎨 🐕‍🦺, & ⚫️ 🚫 💪 Pydantic ⏭. + + 👉 🤙 😮 🛠️ 🍕 Pydantic, 🐕‍🦺 🎏 🔬 📄 👗 (🌐 👉 🛠️ 🔜 ⏪ 💪 Pydantic). + +### 🤗 + +🤗 🕐 🥇 🛠️ 🛠️ 📄 🛠️ 🔢 🆎 ⚙️ 🐍 🆎 🔑. 👉 👑 💭 👈 😮 🎏 🧰 🎏. + +⚫️ ⚙️ 🛃 🆎 🚮 📄 ↩️ 🐩 🐍 🆎, ✋️ ⚫️ 🦏 🔁 ⏩. + +⚫️ 🕐 🥇 🛠️ 🏗 🛃 🔗 📣 🎂 🛠️ 🎻. + +⚫️ 🚫 ⚓️ 🔛 🐩 💖 🗄 & 🎻 🔗. ⚫️ 🚫🔜 🎯 🛠️ ⚫️ ⏮️ 🎏 🧰, 💖 🦁 🎚. ✋️ 🔄, ⚫️ 📶 💡 💭. + +⚫️ ✔️ 😌, ⭐ ⚒: ⚙️ 🎏 🛠️, ⚫️ 💪 ✍ 🔗 & 🇳🇨. + +⚫️ ⚓️ 🔛 ⏮️ 🐩 🔁 🐍 🕸 🛠️ (🇨🇻), ⚫️ 💪 🚫 🍵 *️⃣ & 🎏 👜, 👐 ⚫️ ✔️ ↕ 🎭 💁‍♂️. + +!!! info + 🤗 ✍ ✡ 🗄, 🎏 👼 `isort`, 👑 🧰 🔁 😇 🗄 🐍 📁. + +!!! check "💭 😮 **FastAPI**" + 🤗 😮 🍕 APIStar, & 1️⃣ 🧰 👤 🔎 🏆 👍, 🌟 APIStar. + + 🤗 ℹ 😍 **FastAPI** ⚙️ 🐍 🆎 🔑 📣 🔢, & 🏗 🔗 ⚖ 🛠️ 🔁. + + 🤗 😮 **FastAPI** 📣 `response` 🔢 🔢 ⚒ 🎚 & 🍪. + +### APIStar (<= 0️⃣.5️⃣) + +▶️️ ⏭ 🤔 🏗 **FastAPI** 👤 🔎 **APIStar** 💽. ⚫️ ✔️ 🌖 🌐 👤 👀 & ✔️ 👑 🔧. + +⚫️ 🕐 🥇 🛠️ 🛠️ ⚙️ 🐍 🆎 🔑 📣 🔢 & 📨 👈 👤 ⏱ 👀 (⏭ NestJS & ♨). 👤 🔎 ⚫️ 🌅 ⚖️ 🌘 🎏 🕰 🤗. ✋️ APIStar ⚙️ 🗄 🐩. + +⚫️ ✔️ 🏧 💽 🔬, 💽 🛠️ & 🗄 🔗 ⚡ ⚓️ 🔛 🎏 🆎 🔑 📚 🥉. + +💪 🔗 🔑 🚫 ⚙️ 🎏 🐍 🆎 🔑 💖 Pydantic, ⚫️ 🍖 🌅 🎏 🍭,, 👨‍🎨 🐕‍🦺 🚫🔜 👍, ✋️, APIStar 🏆 💪 🎛. + +⚫️ ✔️ 🏆 🎭 📇 🕰 (🕴 💥 💃). + +🥇, ⚫️ 🚫 ✔️ 🏧 🛠️ 🧾 🕸 🎚, ✋️ 👤 💭 👤 💪 🚮 🦁 🎚 ⚫️. + +⚫️ ✔️ 🔗 💉 ⚙️. ⚫️ ✔ 🏤-® 🦲, 🎏 🧰 🔬 🔛. ✋️, ⚫️ 👑 ⚒. + +👤 🙅 💪 ⚙️ ⚫️ 🌕 🏗, ⚫️ 🚫 ✔️ 💂‍♂ 🛠️,, 👤 🚫 🚫 ❎ 🌐 ⚒ 👤 ✔️ ⏮️ 🌕-📚 🚂 ⚓️ 🔛 🏺-Apispec. 👤 ✔️ 👇 📈 🏗 ✍ 🚲 📨 ❎ 👈 🛠️. + +✋️ ⤴️, 🏗 🎯 🔀. + +⚫️ 🙅‍♂ 📏 🛠️ 🕸 🛠️, 👼 💪 🎯 🔛 💃. + +🔜 APIStar ⚒ 🧰 ✔ 🗄 🔧, 🚫 🕸 🛠️. + +!!! info + APIStar ✍ ✡ 🇺🇸🏛. 🎏 👨 👈 ✍: + + * ✳ 🎂 🛠️ + * 💃 (❔ **FastAPI** ⚓️) + * Uvicorn (⚙️ 💃 & **FastAPI**) + +!!! check "😮 **FastAPI** " + 🔀. + + 💭 📣 💗 👜 (💽 🔬, 🛠️ & 🧾) ⏮️ 🎏 🐍 🆎, 👈 🎏 🕰 🚚 👑 👨‍🎨 🐕‍🦺, 🕳 👤 🤔 💎 💭. + + & ⏮️ 🔎 📏 🕰 🎏 🛠️ & 🔬 📚 🎏 🎛, APIStar 🏆 🎛 💪. + + ⤴️ APIStar ⛔️ 🔀 💽 & 💃 ✍, & 🆕 👻 🏛 ✅ ⚙️. 👈 🏁 🌈 🏗 **FastAPI**. + + 👤 🤔 **FastAPI** "🛐 👨‍💼" APIStar, ⏪ 📉 & 📈 ⚒, ⌨ ⚙️, & 🎏 🍕, ⚓️ 🔛 🏫 ⚪️➡️ 🌐 👉 ⏮️ 🧰. + +## ⚙️ **FastAPI** + +### Pydantic + +Pydantic 🗃 🔬 💽 🔬, 🛠️ & 🧾 (⚙️ 🎻 🔗) ⚓️ 🔛 🐍 🆎 🔑. + +👈 ⚒ ⚫️ 📶 🏋️. + +⚫️ ⭐ 🍭. 👐 ⚫️ ⏩ 🌘 🍭 📇. & ⚫️ ⚓️ 🔛 🎏 🐍 🆎 🔑, 👨‍🎨 🐕‍🦺 👑. + +!!! check "**FastAPI** ⚙️ ⚫️" + 🍵 🌐 💽 🔬, 💽 🛠️ & 🏧 🏷 🧾 (⚓️ 🔛 🎻 🔗). + + **FastAPI** ⤴️ ✊ 👈 🎻 🔗 💽 & 🚮 ⚫️ 🗄, ↖️ ⚪️➡️ 🌐 🎏 👜 ⚫️ 🔨. + +### 💃 + +💃 💿 🔫 🛠️/🧰, ❔ 💯 🏗 ↕-🎭 ✳ 🐕‍🦺. + +⚫️ 📶 🙅 & 🏋️. ⚫️ 🔧 💪 🏧, & ✔️ 🔧 🦲. + +⚫️ ✔️: + +* 🤙 🎆 🎭. +* *️⃣ 🐕‍🦺. +* -🛠️ 🖥 📋. +* 🕴 & 🤫 🎉. +* 💯 👩‍💻 🏗 🔛 🇸🇲. +* ⚜, 🗜, 🎻 📁, 🎏 📨. +* 🎉 & 🍪 🐕‍🦺. +* 1️⃣0️⃣0️⃣ 💯 💯 💰. +* 1️⃣0️⃣0️⃣ 💯 🆎 ✍ ✍. +* 👩‍❤‍👨 🏋️ 🔗. + +💃 ⏳ ⏩ 🐍 🛠️ 💯. 🕴 💥 Uvicorn, ❔ 🚫 🛠️, ✋️ 💽. + +💃 🚚 🌐 🔰 🕸 🕸 🛠️. + +✋️ ⚫️ 🚫 🚚 🏧 💽 🔬, 🛠️ ⚖️ 🧾. + +👈 1️⃣ 👑 👜 👈 **FastAPI** 🚮 🔛 🔝, 🌐 ⚓️ 🔛 🐍 🆎 🔑 (⚙️ Pydantic). 👈, ➕ 🔗 💉 ⚙️, 💂‍♂ 🚙, 🗄 🔗 ⚡, ♒️. + +!!! note "📡 ℹ" + 🔫 🆕 "🐩" ➖ 🛠️ ✳ 🐚 🏉 👨‍🎓. ⚫️ 🚫 "🐍 🐩" (🇩🇬), 👐 👫 🛠️ 🔨 👈. + + 👐, ⚫️ ⏪ ➖ ⚙️ "🐩" 📚 🧰. 👉 📉 📉 🛠️, 👆 💪 🎛 Uvicorn 🙆 🎏 🔫 💽 (💖 👸 ⚖️ Hypercorn), ⚖️ 👆 💪 🚮 🔫 🔗 🧰, 💖 `python-socketio`. + +!!! check "**FastAPI** ⚙️ ⚫️" + 🍵 🌐 🐚 🕸 🍕. ❎ ⚒ 🔛 🔝. + + 🎓 `FastAPI` ⚫️ 😖 🔗 ⚪️➡️ 🎓 `Starlette`. + + , 🕳 👈 👆 💪 ⏮️ 💃, 👆 💪 ⚫️ 🔗 ⏮️ **FastAPI**, ⚫️ 🌖 💃 🔛 💊. + +### Uvicorn + +Uvicorn 🌩-⏩ 🔫 💽, 🏗 🔛 uvloop & httptool. + +⚫️ 🚫 🕸 🛠️, ✋️ 💽. 🖼, ⚫️ 🚫 🚚 🧰 🕹 ➡. 👈 🕳 👈 🛠️ 💖 💃 (⚖️ **FastAPI**) 🔜 🚚 🔛 🔝. + +⚫️ 👍 💽 💃 & **FastAPI**. + +!!! check "**FastAPI** 👍 ⚫️" + 👑 🕸 💽 🏃 **FastAPI** 🈸. + + 👆 💪 🌀 ⚫️ ⏮️ 🐁, ✔️ 🔁 👁-🛠️ 💽. + + ✅ 🌅 ℹ [🛠️](deployment/index.md){.internal-link target=_blank} 📄. + +## 📇 & 🚅 + +🤔, 🔬, & 👀 🔺 🖖 Uvicorn, 💃 & FastAPI, ✅ 📄 🔃 [📇](benchmarks.md){.internal-link target=_blank}. diff --git a/docs/em/docs/async.md b/docs/em/docs/async.md new file mode 100644 index 0000000000..ddcae15739 --- /dev/null +++ b/docs/em/docs/async.md @@ -0,0 +1,430 @@ +# 🛠️ & 🔁 / ⌛ + +ℹ 🔃 `async def` ❕ *➡ 🛠️ 🔢* & 🖥 🔃 🔁 📟, 🛠️, & 🔁. + +## 🏃 ❓ + +🆑;👩‍⚕️: + +🚥 👆 ⚙️ 🥉 🥳 🗃 👈 💬 👆 🤙 👫 ⏮️ `await`, 💖: + +```Python +results = await some_library() +``` + +⤴️, 📣 👆 *➡ 🛠️ 🔢* ⏮️ `async def` 💖: + +```Python hl_lines="2" +@app.get('/') +async def read_results(): + results = await some_library() + return results +``` + +!!! note + 👆 💪 🕴 ⚙️ `await` 🔘 🔢 ✍ ⏮️ `async def`. + +--- + +🚥 👆 ⚙️ 🥉 🥳 🗃 👈 🔗 ⏮️ 🕳 (💽, 🛠️, 📁 ⚙️, ♒️.) & 🚫 ✔️ 🐕‍🦺 ⚙️ `await`, (👉 ⏳ 💼 🌅 💽 🗃), ⤴️ 📣 👆 *➡ 🛠️ 🔢* 🛎, ⏮️ `def`, 💖: + +```Python hl_lines="2" +@app.get('/') +def results(): + results = some_library() + return results +``` + +--- + +🚥 👆 🈸 (😫) 🚫 ✔️ 🔗 ⏮️ 🕳 🙆 & ⌛ ⚫️ 📨, ⚙️ `async def`. + +--- + +🚥 👆 🚫 💭, ⚙️ 😐 `def`. + +--- + +**🗒**: 👆 💪 🌀 `def` & `async def` 👆 *➡ 🛠️ 🔢* 🌅 👆 💪 & 🔬 🔠 1️⃣ ⚙️ 🏆 🎛 👆. FastAPI 🔜 ▶️️ 👜 ⏮️ 👫. + +😆, 🙆 💼 🔛, FastAPI 🔜 👷 🔁 & 📶 ⏩. + +✋️ 📄 📶 🔛, ⚫️ 🔜 💪 🎭 🛠️. + +## 📡 ℹ + +🏛 ⏬ 🐍 ✔️ 🐕‍🦺 **"🔁 📟"** ⚙️ 🕳 🤙 **"🔁"**, ⏮️ **`async` & `await`** ❕. + +➡️ 👀 👈 🔤 🍕 📄 🔛: + +* **🔁 📟** +* **`async` & `await`** +* **🔁** + +## 🔁 📟 + +🔁 📟 ⛓ 👈 🇪🇸 👶 ✔️ 🌌 💬 💻 / 📋 👶 👈 ☝ 📟, ⚫️ 👶 🔜 ✔️ ⌛ *🕳 🙆* 🏁 👱 🙆. ➡️ 💬 👈 *🕳 🙆* 🤙 "🐌-📁" 👶. + +, ⏮️ 👈 🕰, 💻 💪 🚶 & 🎏 👷, ⏪ "🐌-📁" 👶 🏁. + +⤴️ 💻 / 📋 👶 🔜 👟 🔙 🔠 🕰 ⚫️ ✔️ 🤞 ↩️ ⚫️ ⌛ 🔄, ⚖️ 🕐❔ ⚫️ 👶 🏁 🌐 👷 ⚫️ ✔️ 👈 ☝. & ⚫️ 👶 🔜 👀 🚥 🙆 📋 ⚫️ ⌛ ✔️ ⏪ 🏁, 🤸 ⚫️❔ ⚫️ ✔️. + +⏭, ⚫️ 👶 ✊ 🥇 📋 🏁 (➡️ 💬, 👆 "🐌-📁" 👶) & 😣 ⚫️❔ ⚫️ ✔️ ⏮️ ⚫️. + +👈 "⌛ 🕳 🙆" 🛎 🔗 👤/🅾 🛠️ 👈 📶 "🐌" (🔬 🚅 🕹 & 💾 💾), 💖 ⌛: + +* 📊 ⚪️➡️ 👩‍💻 📨 🔘 🕸 +* 📊 📨 👆 📋 📨 👩‍💻 🔘 🕸 +* 🎚 📁 💾 ✍ ⚙️ & 🤝 👆 📋 +* 🎚 👆 📋 🤝 ⚙️ ✍ 💾 +* 🛰 🛠️ 🛠️ +* 💽 🛠️ 🏁 +* 💽 🔢 📨 🏁 +* ♒️. + +🛠️ 🕰 🍴 ✴️ ⌛ 👤/🅾 🛠️, 👫 🤙 👫 "👤/🅾 🔗" 🛠️. + +⚫️ 🤙 "🔁" ↩️ 💻 / 📋 🚫 ✔️ "🔁" ⏮️ 🐌 📋, ⌛ ☑ 🙍 👈 📋 🏁, ⏪ 🔨 🕳, 💪 ✊ 📋 🏁 & 😣 👷. + +↩️ 👈, 💆‍♂ "🔁" ⚙️, 🕐 🏁, 📋 💪 ⌛ ⏸ 🐥 👄 (⏲) 💻 / 📋 🏁 ⚫️❔ ⚫️ 🚶, & ⤴️ 👟 🔙 ✊ 🏁 & 😣 👷 ⏮️ 👫. + +"🔁" (👽 "🔁") 👫 🛎 ⚙️ ⚖ "🔁", ↩️ 💻 / 📋 ⏩ 🌐 📶 🔁 ⏭ 🔀 🎏 📋, 🚥 👈 🔁 🔌 ⌛. + +### 🛠️ & 🍔 + +👉 💭 **🔁** 📟 🔬 🔛 🕣 🤙 **"🛠️"**. ⚫️ 🎏 ⚪️➡️ **"🔁"**. + +**🛠️** & **🔁** 👯‍♂️ 🔗 "🎏 👜 😥 🌅 ⚖️ 🌘 🎏 🕰". + +✋️ ℹ 🖖 *🛠️* & *🔁* 🎏. + +👀 🔺, 🌈 📄 📖 🔃 🍔: + +### 🛠️ 🍔 + +👆 🚶 ⏮️ 👆 🥰 🤚 ⏩ 🥕, 👆 🧍 ⏸ ⏪ 🏧 ✊ ✔ ⚪️➡️ 👫👫 🚪 👆. 👶 + + + +⤴️ ⚫️ 👆 🔄, 👆 🥉 👆 ✔ 2️⃣ 📶 🎀 🍔 👆 🥰 & 👆. 👶 👶 + + + +🏧 💬 🕳 🍳 👨‍🍳 👫 💭 👫 ✔️ 🏗 👆 🍔 (✋️ 👫 ⏳ 🏗 🕐 ⏮️ 👩‍💻). + + + +👆 💸. 👶 + +🏧 🤝 👆 🔢 👆 🔄. + + + +⏪ 👆 ⌛, 👆 🚶 ⏮️ 👆 🥰 & ⚒ 🏓, 👆 🧎 & 💬 ⏮️ 👆 🥰 📏 🕰 (👆 🍔 📶 🎀 & ✊ 🕰 🏗). + +👆 🏖 🏓 ⏮️ 👆 🥰, ⏪ 👆 ⌛ 🍔, 👆 💪 💸 👈 🕰 😮 ❔ 👌, 🐨 & 🙃 👆 🥰 👶 👶 👶. + + + +⏪ ⌛ & 💬 👆 🥰, ⚪️➡️ 🕰 🕰, 👆 ✅ 🔢 🖥 🔛 ⏲ 👀 🚥 ⚫️ 👆 🔄 ⏪. + +⤴️ ☝, ⚫️ 😒 👆 🔄. 👆 🚶 ⏲, 🤚 👆 🍔 & 👟 🔙 🏓. + + + +👆 & 👆 🥰 🍴 🍔 & ✔️ 👌 🕰. 👶 + + + +!!! info + 🌹 🖼 👯 🍏. 👶 + +--- + +🌈 👆 💻 / 📋 👶 👈 📖. + +⏪ 👆 ⏸, 👆 ⛽ 👶, ⌛ 👆 🔄, 🚫 🔨 🕳 📶 "😌". ✋️ ⏸ ⏩ ↩️ 🏧 🕴 ✊ ✔ (🚫 🏗 👫), 👈 👌. + +⤴️, 🕐❔ ⚫️ 👆 🔄, 👆 ☑ "😌" 👷, 👆 🛠️ 🍣, 💭 ⚫️❔ 👆 💚, 🤚 👆 🥰 ⚒, 💸, ✅ 👈 👆 🤝 ☑ 💵 ⚖️ 💳, ✅ 👈 👆 🈚 ☑, ✅ 👈 ✔ ✔️ ☑ 🏬, ♒️. + +✋️ ⤴️, ✋️ 👆 🚫 ✔️ 👆 🍔, 👆 👷 ⏮️ 🏧 "🔛 ⏸" ⏸, ↩️ 👆 ✔️ ⌛ 👶 👆 🍔 🔜. + +✋️ 👆 🚶 ↖️ ⚪️➡️ ⏲ & 🧎 🏓 ⏮️ 🔢 👆 🔄, 👆 💪 🎛 👶 👆 🙋 👆 🥰, & "👷" 👶 👶 🔛 👈. ⤴️ 👆 🔄 🔨 🕳 📶 "😌" 😏 ⏮️ 👆 🥰 👶. + +⤴️ 🏧 👶 💬 "👤 🏁 ⏮️ 🔨 🍔" 🚮 👆 🔢 🔛 ⏲ 🖥, ✋️ 👆 🚫 🦘 💖 😜 ⏪ 🕐❔ 🖥 🔢 🔀 👆 🔄 🔢. 👆 💭 🙅‍♂ 1️⃣ 🔜 📎 👆 🍔 ↩️ 👆 ✔️ 🔢 👆 🔄, & 👫 ✔️ 👫. + +👆 ⌛ 👆 🥰 🏁 📖 (🏁 ⏮️ 👷 👶 / 📋 ➖ 🛠️ 👶), 😀 🖐 & 💬 👈 👆 🔜 🍔 ⏸. + +⤴️ 👆 🚶 ⏲ 👶, ▶️ 📋 👈 🔜 🏁 👶, ⚒ 🍔, 💬 👏 & ✊ 👫 🏓. 👈 🏁 👈 🔁 / 📋 🔗 ⏮️ ⏲ ⏹. 👈 🔄, ✍ 🆕 📋, "🍴 🍔" 👶 👶, ✋️ ⏮️ 1️⃣ "🤚 🍔" 🏁 ⏹. + +### 🔗 🍔 + +🔜 ➡️ 🌈 👫 ➖🚫 🚫 "🛠️ 🍔", ✋️ "🔗 🍔". + +👆 🚶 ⏮️ 👆 🥰 🤚 🔗 ⏩ 🥕. + +👆 🧍 ⏸ ⏪ 📚 (➡️ 💬 8️⃣) 🏧 👈 🎏 🕰 🍳 ✊ ✔ ⚪️➡️ 👫👫 🚪 👆. + +👱 ⏭ 👆 ⌛ 👫 🍔 🔜 ⏭ 🍂 ⏲ ↩️ 🔠 8️⃣ 🏧 🚶 & 🏗 🍔 ▶️️ ↖️ ⏭ 💆‍♂ ⏭ ✔. + + + +⤴️ ⚫️ 😒 👆 🔄, 👆 🥉 👆 ✔ 2️⃣ 📶 🎀 🍔 👆 🥰 & 👆. + +👆 💸 👶. + + + +🏧 🚶 👨‍🍳. + +👆 ⌛, 🧍 🚪 ⏲ 👶, 👈 🙅‍♂ 1️⃣ 🙆 ✊ 👆 🍔 ⏭ 👆, 📤 🙅‍♂ 🔢 🔄. + + + +👆 & 👆 🥰 😩 🚫 ➡️ 🙆 🤚 🚪 👆 & ✊ 👆 🍔 🕐❔ 👫 🛬, 👆 🚫🔜 💸 🙋 👆 🥰. 👶 + +👉 "🔁" 👷, 👆 "🔁" ⏮️ 🏧/🍳 👶 👶. 👆 ✔️ ⌛ 👶 & 📤 ☑ 🙍 👈 🏧/🍳 👶 👶 🏁 🍔 & 🤝 👫 👆, ⚖️ ⏪, 👱 🙆 💪 ✊ 👫. + + + +⤴️ 👆 🏧/🍳 👶 👶 😒 👟 🔙 ⏮️ 👆 🍔, ⏮️ 📏 🕰 ⌛ 👶 📤 🚪 ⏲. + + + +👆 ✊ 👆 🍔 & 🚶 🏓 ⏮️ 👆 🥰. + +👆 🍴 👫, & 👆 🔨. ⏹ + + + +📤 🚫 🌅 💬 ⚖️ 😏 🌅 🕰 💸 ⌛ 👶 🚪 ⏲. 👶 + +!!! info + 🌹 🖼 👯 🍏. 👶 + +--- + +👉 😐 🔗 🍔, 👆 💻 / 📋 👶 ⏮️ 2️⃣ 🕹 (👆 & 👆 🥰), 👯‍♂️ ⌛ 👶 & 💡 👫 🙋 👶 "⌛ 🔛 ⏲" 👶 📏 🕰. + +⏩ 🥕 🏪 ✔️ 8️⃣ 🕹 (🏧/🍳). ⏪ 🛠️ 🍔 🏪 💪 ✔️ ✔️ 🕴 2️⃣ (1️⃣ 🏧 & 1️⃣ 🍳). + +✋️, 🏁 💡 🚫 🏆. 👶 + +--- + +👉 🔜 🔗 🌓 📖 🍔. 👶 + +🌅 "🎰 👨‍❤‍👨" 🖼 👉, 🌈 🏦. + +🆙 ⏳, 🏆 🏦 ✔️ 💗 🏧 👶 👶 👶 👶 👶 👶 👶 👶 & 🦏 ⏸ 👶 👶 👶 👶 👶 👶 👶 👶. + +🌐 🏧 🔨 🌐 👷 ⏮️ 1️⃣ 👩‍💻 ⏮️ 🎏 👶 👶 👶. + +& 👆 ✔️ ⌛ 👶 ⏸ 📏 🕰 ⚖️ 👆 💸 👆 🔄. + +👆 🎲 🚫🔜 💚 ✊ 👆 🥰 👶 ⏮️ 👆 👷 🏦 👶. + +### 🍔 🏁 + +👉 😐 "⏩ 🥕 🍔 ⏮️ 👆 🥰", 📤 📚 ⌛ 👶, ⚫️ ⚒ 📚 🌅 🔑 ✔️ 🛠️ ⚙️ ⏸ 👶 👶. + +👉 💼 🌅 🕸 🈸. + +📚, 📚 👩‍💻, ✋️ 👆 💽 ⌛ 👶 👫 🚫--👍 🔗 📨 👫 📨. + +& ⤴️ ⌛ 👶 🔄 📨 👟 🔙. + +👉 "⌛" 👶 ⚖ ⏲, ✋️, ⚖ ⚫️ 🌐, ⚫️ 📚 ⌛ 🔚. + +👈 ⚫️❔ ⚫️ ⚒ 📚 🔑 ⚙️ 🔁 ⏸ 👶 👶 📟 🕸 🔗. + +👉 😇 🔀 ⚫️❔ ⚒ ✳ 🌟 (✋️ ✳ 🚫 🔗) & 👈 💪 🚶 🛠️ 🇪🇸. + +& 👈 🎏 🎚 🎭 👆 🤚 ⏮️ **FastAPI**. + +& 👆 💪 ✔️ 🔁 & 🔀 🎏 🕰, 👆 🤚 ↕ 🎭 🌘 🌅 💯 ✳ 🛠️ & 🔛 🇷🇪 ⏮️ 🚶, ❔ ✍ 🇪🇸 🔐 🅱 (🌐 👏 💃). + +### 🛠️ 👍 🌘 🔁 ❓ + +😆 ❗ 👈 🚫 🛐 📖. + +🛠️ 🎏 🌘 🔁. & ⚫️ 👻 🔛 **🎯** 😐 👈 🔌 📚 ⌛. ↩️ 👈, ⚫️ 🛎 📚 👍 🌘 🔁 🕸 🈸 🛠️. ✋️ 🚫 🌐. + +, ⚖ 👈 👅, 🌈 📄 📏 📖: + +> 👆 ✔️ 🧹 🦏, 💩 🏠. + +*😆, 👈 🎂 📖*. + +--- + +📤 🙅‍♂ ⌛ 👶 🙆, 📚 👷 🔨, 🔛 💗 🥉 🏠. + +👆 💪 ✔️ 🔄 🍔 🖼, 🥇 🏠 🧖‍♂, ⤴️ 👨‍🍳, ✋️ 👆 🚫 ⌛ 👶 🕳, 🧹 & 🧹, 🔄 🚫🔜 📉 🕳. + +⚫️ 🔜 ✊ 🎏 💸 🕰 🏁 ⏮️ ⚖️ 🍵 🔄 (🛠️) & 👆 🔜 ✔️ ⌛ 🎏 💸 👷. + +✋️ 👉 💼, 🚥 👆 💪 ✊️ 8️⃣ 👰-🏧/🍳/🔜-🧹, & 🔠 1️⃣ 👫 (➕ 👆) 💪 ✊ 🏒 🏠 🧹 ⚫️, 👆 💪 🌐 👷 **🔗**, ⏮️ ➕ ℹ, & 🏁 🌅 🔜. + +👉 😐, 🔠 1️⃣ 🧹 (🔌 👆) 🔜 🕹, 🤸 👫 🍕 👨‍🏭. + +& 🏆 🛠️ 🕰 ✊ ☑ 👷 (↩️ ⌛), & 👷 💻 ⌛ 💽, 👫 🤙 👫 ⚠ "💽 🎁". + +--- + +⚠ 🖼 💽 🔗 🛠️ 👜 👈 🚚 🏗 🧪 🏭. + +🖼: + +* **🎧** ⚖️ **🖼 🏭**. +* **💻 👓**: 🖼 ✍ 💯 🔅, 🔠 🔅 ✔️ 3️⃣ 💲 / 🎨, 🏭 👈 🛎 🚚 💻 🕳 🔛 📚 🔅, 🌐 🎏 🕰. +* **🎰 🏫**: ⚫️ 🛎 🚚 📚 "✖" & "🖼" ✖. 💭 🦏 📋 ⏮️ 🔢 & ✖ 🌐 👫 👯‍♂️ 🎏 🕰. +* **⏬ 🏫**: 👉 🎧-🏑 🎰 🏫,, 🎏 ✔. ⚫️ 👈 📤 🚫 👁 📋 🔢 ✖, ✋️ 🦏 ⚒ 👫, & 📚 💼, 👆 ⚙️ 🎁 🕹 🏗 & / ⚖️ ⚙️ 👈 🏷. + +### 🛠️ ➕ 🔁: 🕸 ➕ 🎰 🏫 + +⏮️ **FastAPI** 👆 💪 ✊ 📈 🛠️ 👈 📶 ⚠ 🕸 🛠️ (🎏 👑 🧲 ✳). + +✋️ 👆 💪 🐄 💰 🔁 & 💾 (✔️ 💗 🛠️ 🏃‍♂ 🔗) **💽 🎁** ⚖ 💖 👈 🎰 🏫 ⚙️. + +👈, ➕ 🙅 👐 👈 🐍 👑 🇪🇸 **💽 🧪**, 🎰 🏫 & ✴️ ⏬ 🏫, ⚒ FastAPI 📶 👍 🏏 💽 🧪 / 🎰 🏫 🕸 🔗 & 🈸 (👪 📚 🎏). + +👀 ❔ 🏆 👉 🔁 🏭 👀 📄 🔃 [🛠️](deployment/index.md){.internal-link target=_blank}. + +## `async` & `await` + +🏛 ⏬ 🐍 ✔️ 📶 🏋️ 🌌 🔬 🔁 📟. 👉 ⚒ ⚫️ 👀 💖 😐 "🔁" 📟 & "⌛" 👆 ▶️️ 🙍. + +🕐❔ 📤 🛠️ 👈 🔜 🚚 ⌛ ⏭ 🤝 🏁 & ✔️ 🐕‍🦺 👉 🆕 🐍 ⚒, 👆 💪 📟 ⚫️ 💖: + +```Python +burgers = await get_burgers(2) +``` + +🔑 📥 `await`. ⚫️ 💬 🐍 👈 ⚫️ ✔️ ⌛ ⏸ `get_burgers(2)` 🏁 🔨 🚮 👜 👶 ⏭ ♻ 🏁 `burgers`. ⏮️ 👈, 🐍 🔜 💭 👈 ⚫️ 💪 🚶 & 🕳 🙆 👶 👶 👐 (💖 📨 ➕1️⃣ 📨). + +`await` 👷, ⚫️ ✔️ 🔘 🔢 👈 🐕‍🦺 👉 🔀. 👈, 👆 📣 ⚫️ ⏮️ `async def`: + +```Python hl_lines="1" +async def get_burgers(number: int): + # Do some asynchronous stuff to create the burgers + return burgers +``` + +...↩️ `def`: + +```Python hl_lines="2" +# This is not asynchronous +def get_sequential_burgers(number: int): + # Do some sequential stuff to create the burgers + return burgers +``` + +⏮️ `async def`, 🐍 💭 👈, 🔘 👈 🔢, ⚫️ ✔️ 🤔 `await` 🧬, & 👈 ⚫️ 💪 "⏸" ⏸ 🛠️ 👈 🔢 & 🚶 🕳 🙆 👶 ⏭ 👟 🔙. + +🕐❔ 👆 💚 🤙 `async def` 🔢, 👆 ✔️ "⌛" ⚫️. , 👉 🏆 🚫 👷: + +```Python +# This won't work, because get_burgers was defined with: async def +burgers = get_burgers(2) +``` + +--- + +, 🚥 👆 ⚙️ 🗃 👈 💬 👆 👈 👆 💪 🤙 ⚫️ ⏮️ `await`, 👆 💪 ✍ *➡ 🛠️ 🔢* 👈 ⚙️ ⚫️ ⏮️ `async def`, 💖: + +```Python hl_lines="2-3" +@app.get('/burgers') +async def read_burgers(): + burgers = await get_burgers(2) + return burgers +``` + +### 🌅 📡 ℹ + +👆 💪 ✔️ 👀 👈 `await` 💪 🕴 ⚙️ 🔘 🔢 🔬 ⏮️ `async def`. + +✋️ 🎏 🕰, 🔢 🔬 ⏮️ `async def` ✔️ "⌛". , 🔢 ⏮️ `async def` 💪 🕴 🤙 🔘 🔢 🔬 ⏮️ `async def` 💁‍♂️. + +, 🔃 🥚 & 🐔, ❔ 👆 🤙 🥇 `async` 🔢 ❓ + +🚥 👆 👷 ⏮️ **FastAPI** 👆 🚫 ✔️ 😟 🔃 👈, ↩️ 👈 "🥇" 🔢 🔜 👆 *➡ 🛠️ 🔢*, & FastAPI 🔜 💭 ❔ ▶️️ 👜. + +✋️ 🚥 👆 💚 ⚙️ `async` / `await` 🍵 FastAPI, 👆 💪 ⚫️ 👍. + +### ✍ 👆 👍 🔁 📟 + +💃 (& **FastAPI**) ⚓️ 🔛 AnyIO, ❔ ⚒ ⚫️ 🔗 ⏮️ 👯‍♂️ 🐍 🐩 🗃 & 🎻. + +🎯, 👆 💪 🔗 ⚙️ AnyIO 👆 🏧 🛠️ ⚙️ 💼 👈 🚚 🌅 🏧 ⚓ 👆 👍 📟. + +& 🚥 👆 🚫 ⚙️ FastAPI, 👆 💪 ✍ 👆 👍 🔁 🈸 ⏮️ AnyIO 🏆 🔗 & 🤚 🚮 💰 (✅ *📊 🛠️*). + +### 🎏 📨 🔁 📟 + +👉 👗 ⚙️ `async` & `await` 📶 🆕 🇪🇸. + +✋️ ⚫️ ⚒ 👷 ⏮️ 🔁 📟 📚 ⏩. + +👉 🎏 ❕ (⚖️ 🌖 🌓) 🔌 ⏳ 🏛 ⏬ 🕸 (🖥 & ✳). + +✋️ ⏭ 👈, 🚚 🔁 📟 🌖 🏗 & ⚠. + +⏮️ ⏬ 🐍, 👆 💪 ✔️ ⚙️ 🧵 ⚖️ 🐁. ✋️ 📟 🌌 🌖 🏗 🤔, ℹ, & 💭 🔃. + +⏮️ ⏬ ✳ / 🖥 🕸, 👆 🔜 ✔️ ⚙️ "⏲". ❔ ↘️ ⏲ 🔥😈. + +## 🔁 + +**🔁** 📶 🎀 ⚖ 👜 📨 `async def` 🔢. 🐍 💭 👈 ⚫️ 🕳 💖 🔢 👈 ⚫️ 💪 ▶️ & 👈 ⚫️ 🔜 🔚 ☝, ✋️ 👈 ⚫️ 5️⃣📆 ⏸ ⏸ 🔘 💁‍♂️, 🕐❔ 📤 `await` 🔘 ⚫️. + +✋️ 🌐 👉 🛠️ ⚙️ 🔁 📟 ⏮️ `async` & `await` 📚 🕰 🔬 ⚙️ "🔁". ⚫️ ⭐ 👑 🔑 ⚒ 🚶, "🔁". + +## 🏁 + +➡️ 👀 🎏 🔤 ⚪️➡️ 🔛: + +> 🏛 ⏬ 🐍 ✔️ 🐕‍🦺 **"🔁 📟"** ⚙️ 🕳 🤙 **"🔁"**, ⏮️ **`async` & `await`** ❕. + +👈 🔜 ⚒ 🌅 🔑 🔜. 👶 + +🌐 👈 ⚫️❔ 🏋️ FastAPI (🔘 💃) & ⚫️❔ ⚒ ⚫️ ✔️ ✅ 🎆 🎭. + +## 📶 📡 ℹ + +!!! warning + 👆 💪 🎲 🚶 👉. + + 👉 📶 📡 ℹ ❔ **FastAPI** 👷 🔘. + + 🚥 👆 ✔️ 📡 💡 (🈶-🏋, 🧵, 🍫, ♒️.) & 😟 🔃 ❔ FastAPI 🍵 `async def` 🆚 😐 `def`, 🚶 ⤴️. + +### ➡ 🛠️ 🔢 + +🕐❔ 👆 📣 *➡ 🛠️ 🔢* ⏮️ 😐 `def` ↩️ `async def`, ⚫️ 🏃 🔢 🧵 👈 ⤴️ ⌛, ↩️ ➖ 🤙 🔗 (⚫️ 🔜 🍫 💽). + +🚥 👆 👟 ⚪️➡️ ➕1️⃣ 🔁 🛠️ 👈 🔨 🚫 👷 🌌 🔬 🔛 & 👆 ⚙️ ⚖ 🙃 📊-🕴 *➡ 🛠️ 🔢* ⏮️ ✅ `def` 🤪 🎭 📈 (🔃 1️⃣0️⃣0️⃣ 💓), 🙏 🗒 👈 **FastAPI** ⭐ 🔜 🔄. 👫 💼, ⚫️ 👻 ⚙️ `async def` 🚥 👆 *➡ 🛠️ 🔢* ⚙️ 📟 👈 🎭 🚧 👤/🅾. + +, 👯‍♂️ ⚠, 🤞 👈 **FastAPI** 🔜 [⏩](/#performance){.internal-link target=_blank} 🌘 (⚖️ 🌘 ⭐) 👆 ⏮️ 🛠️. + +### 🔗 + +🎏 ✔ [🔗](./tutorial/dependencies/index.md){.internal-link target=_blank}. 🚥 🔗 🐩 `def` 🔢 ↩️ `async def`, ⚫️ 🏃 🔢 🧵. + +### 🎧-🔗 + +👆 💪 ✔️ 💗 🔗 & [🎧-🔗](./tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} 🚫 🔠 🎏 (🔢 🔢 🔑), 👫 💪 ✍ ⏮️ `async def` & ⏮️ 😐 `def`. ⚫️ 🔜 👷, & 🕐 ✍ ⏮️ 😐 `def` 🔜 🤙 🔛 🔢 🧵 (⚪️➡️ 🧵) ↩️ ➖ "⌛". + +### 🎏 🚙 🔢 + +🙆 🎏 🚙 🔢 👈 👆 🤙 🔗 💪 ✍ ⏮️ 😐 `def` ⚖️ `async def` & FastAPI 🏆 🚫 📉 🌌 👆 🤙 ⚫️. + +👉 🔅 🔢 👈 FastAPI 🤙 👆: *➡ 🛠️ 🔢* & 🔗. + +🚥 👆 🚙 🔢 😐 🔢 ⏮️ `def`, ⚫️ 🔜 🤙 🔗 (👆 ✍ ⚫️ 👆 📟), 🚫 🧵, 🚥 🔢 ✍ ⏮️ `async def` ⤴️ 👆 🔜 `await` 👈 🔢 🕐❔ 👆 🤙 ⚫️ 👆 📟. + +--- + +🔄, 👉 📶 📡 ℹ 👈 🔜 🎲 ⚠ 🚥 👆 👟 🔎 👫. + +⏪, 👆 🔜 👍 ⏮️ 📄 ⚪️➡️ 📄 🔛: 🏃 ❓. diff --git a/docs/em/docs/benchmarks.md b/docs/em/docs/benchmarks.md new file mode 100644 index 0000000000..003c3f62de --- /dev/null +++ b/docs/em/docs/benchmarks.md @@ -0,0 +1,34 @@ +# 📇 + +🔬 🇸🇲 📇 🎦 **FastAPI** 🈸 🏃‍♂ 🔽 Uvicorn 1️⃣ ⏩ 🐍 🛠️ 💪, 🕴 🔛 💃 & Uvicorn 👫 (⚙️ 🔘 FastAPI). (*) + +✋️ 🕐❔ ✅ 📇 & 🔺 👆 🔜 ✔️ 📄 🤯. + +## 📇 & 🚅 + +🕐❔ 👆 ✅ 📇, ⚫️ ⚠ 👀 📚 🧰 🎏 🆎 🔬 🌓. + +🎯, 👀 Uvicorn, 💃 & FastAPI 🔬 👯‍♂️ (👪 📚 🎏 🧰). + +🙅 ⚠ ❎ 🧰, 👍 🎭 ⚫️ 🔜 🤚. & 🏆 📇 🚫 💯 🌖 ⚒ 🚚 🧰. + +🔗 💖: + +* **Uvicorn**: 🔫 💽 + * **💃**: (⚙️ Uvicorn) 🕸 🕸 + * **FastAPI**: (⚙️ 💃) 🛠️ 🕸 ⏮️ 📚 🌖 ⚒ 🏗 🔗, ⏮️ 💽 🔬, ♒️. + +* **Uvicorn**: + * 🔜 ✔️ 🏆 🎭, ⚫️ 🚫 ✔️ 🌅 ➕ 📟 ↖️ ⚪️➡️ 💽 ⚫️. + * 👆 🚫🔜 ✍ 🈸 Uvicorn 🔗. 👈 🔜 ⛓ 👈 👆 📟 🔜 ✔️ 🔌 🌖 ⚖️ 🌘, 🌘, 🌐 📟 🚚 💃 (⚖️ **FastAPI**). & 🚥 👆 👈, 👆 🏁 🈸 🔜 ✔️ 🎏 🌥 ✔️ ⚙️ 🛠️ & 📉 👆 📱 📟 & 🐛. + * 🚥 👆 ⚖ Uvicorn, 🔬 ⚫️ 🛡 👸, Hypercorn, ✳, ♒️. 🈸 💽. +* **💃**: + * 🔜 ✔️ ⏭ 🏆 🎭, ⏮️ Uvicorn. 👐, 💃 ⚙️ Uvicorn 🏃. , ⚫️ 🎲 💪 🕴 🤚 "🐌" 🌘 Uvicorn ✔️ 🛠️ 🌅 📟. + * ✋️ ⚫️ 🚚 👆 🧰 🏗 🙅 🕸 🈸, ⏮️ 🕹 ⚓️ 🔛 ➡, ♒️. + * 🚥 👆 ⚖ 💃, 🔬 ⚫️ 🛡 🤣, 🏺, ✳, ♒️. 🕸 🛠️ (⚖️ 🕸). +* **FastAPI**: + * 🎏 🌌 👈 💃 ⚙️ Uvicorn & 🚫🔜 ⏩ 🌘 ⚫️, **FastAPI** ⚙️ 💃, ⚫️ 🚫🔜 ⏩ 🌘 ⚫️. + * FastAPI 🚚 🌅 ⚒ 🔛 🔝 💃. ⚒ 👈 👆 🌖 🕧 💪 🕐❔ 🏗 🔗, 💖 💽 🔬 & 🛠️. & ⚙️ ⚫️, 👆 🤚 🏧 🧾 🆓 (🏧 🧾 🚫 🚮 🌥 🏃‍♂ 🈸, ⚫️ 🏗 🔛 🕴). + * 🚥 👆 🚫 ⚙️ FastAPI & ⚙️ 💃 🔗 (⚖️ ➕1️⃣ 🧰, 💖 🤣, 🏺, 🆘, ♒️) 👆 🔜 ✔️ 🛠️ 🌐 💽 🔬 & 🛠️ 👆. , 👆 🏁 🈸 🔜 ✔️ 🎏 🌥 🚥 ⚫️ 🏗 ⚙️ FastAPI. & 📚 💼, 👉 💽 🔬 & 🛠️ 🦏 💸 📟 ✍ 🈸. + * , ⚙️ FastAPI 👆 ♻ 🛠️ 🕰, 🐛, ⏸ 📟, & 👆 🔜 🎲 🤚 🎏 🎭 (⚖️ 👍) 👆 🔜 🚥 👆 🚫 ⚙️ ⚫️ (👆 🔜 ✔️ 🛠️ ⚫️ 🌐 👆 📟). + * 🚥 👆 ⚖ FastAPI, 🔬 ⚫️ 🛡 🕸 🈸 🛠️ (⚖️ ⚒ 🧰) 👈 🚚 💽 🔬, 🛠️ & 🧾, 💖 🏺-apispec, NestJS, ♨, ♒️. 🛠️ ⏮️ 🛠️ 🏧 💽 🔬, 🛠️ & 🧾. diff --git a/docs/em/docs/contributing.md b/docs/em/docs/contributing.md new file mode 100644 index 0000000000..748928f88f --- /dev/null +++ b/docs/em/docs/contributing.md @@ -0,0 +1,465 @@ +# 🛠️ - 📉 + +🥇, 👆 💪 💚 👀 🔰 🌌 [ℹ FastAPI & 🤚 ℹ](help-fastapi.md){.internal-link target=_blank}. + +## 🛠️ + +🚥 👆 ⏪ 🖖 🗃 & 👆 💭 👈 👆 💪 ⏬ 🤿 📟, 📥 📄 ⚒ 🆙 👆 🌐. + +### 🕹 🌐 ⏮️ `venv` + +👆 💪 ✍ 🕹 🌐 📁 ⚙️ 🐍 `venv` 🕹: + +
+ +```console +$ python -m venv env +``` + +
+ +👈 🔜 ✍ 📁 `./env/` ⏮️ 🐍 💱 & ⤴️ 👆 🔜 💪 ❎ 📦 👈 ❎ 🌐. + +### 🔓 🌐 + +🔓 🆕 🌐 ⏮️: + +=== "💾, 🇸🇻" + +
+ + ```console + $ source ./env/bin/activate + ``` + +
+ +=== "🚪 📋" + +
+ + ```console + $ .\env\Scripts\Activate.ps1 + ``` + +
+ +=== "🚪 🎉" + + ⚖️ 🚥 👆 ⚙️ 🎉 🖥 (✅ 🐛 🎉): + +
+ + ```console + $ source ./env/Scripts/activate + ``` + +
+ +✅ ⚫️ 👷, ⚙️: + +=== "💾, 🇸🇻, 🚪 🎉" + +
+ + ```console + $ which pip + + some/directory/fastapi/env/bin/pip + ``` + +
+ +=== "🚪 📋" + +
+ + ```console + $ Get-Command pip + + some/directory/fastapi/env/bin/pip + ``` + +
+ +🚥 ⚫️ 🎦 `pip` 💱 `env/bin/pip` ⤴️ ⚫️ 👷. 👶 + +⚒ 💭 👆 ✔️ 📰 🐖 ⏬ 🔛 👆 🕹 🌐 ❎ ❌ 🔛 ⏭ 📶: + +
+ +```console +$ python -m pip install --upgrade pip + +---> 100% +``` + +
+ +!!! tip + 🔠 🕰 👆 ❎ 🆕 📦 ⏮️ `pip` 🔽 👈 🌐, 🔓 🌐 🔄. + + 👉 ⚒ 💭 👈 🚥 👆 ⚙️ 📶 📋 ❎ 👈 📦, 👆 ⚙️ 1️⃣ ⚪️➡️ 👆 🇧🇿 🌐 & 🚫 🙆 🎏 👈 💪 ❎ 🌐. + +### 🐖 + +⏮️ 🔓 🌐 🔬 🔛: + +
+ +```console +$ pip install -r requirements.txt + +---> 100% +``` + +
+ +⚫️ 🔜 ❎ 🌐 🔗 & 👆 🇧🇿 FastAPI 👆 🇧🇿 🌐. + +#### ⚙️ 👆 🇧🇿 FastAPI + +🚥 👆 ✍ 🐍 📁 👈 🗄 & ⚙️ FastAPI, & 🏃 ⚫️ ⏮️ 🐍 ⚪️➡️ 👆 🇧🇿 🌐, ⚫️ 🔜 ⚙️ 👆 🇧🇿 FastAPI ℹ 📟. + +& 🚥 👆 ℹ 👈 🇧🇿 FastAPI ℹ 📟, ⚫️ ❎ ⏮️ `-e`, 🕐❔ 👆 🏃 👈 🐍 📁 🔄, ⚫️ 🔜 ⚙️ 🍋 ⏬ FastAPI 👆 ✍. + +👈 🌌, 👆 🚫 ✔️ "❎" 👆 🇧🇿 ⏬ 💪 💯 🔠 🔀. + +### 📁 + +📤 ✍ 👈 👆 💪 🏃 👈 🔜 📁 & 🧹 🌐 👆 📟: + +
+ +```console +$ bash scripts/format.sh +``` + +
+ +⚫️ 🔜 🚘-😇 🌐 👆 🗄. + +⚫️ 😇 👫 ☑, 👆 💪 ✔️ FastAPI ❎ 🌐 👆 🌐, ⏮️ 📋 📄 🔛 ⚙️ `-e`. + +## 🩺 + +🥇, ⚒ 💭 👆 ⚒ 🆙 👆 🌐 🔬 🔛, 👈 🔜 ❎ 🌐 📄. + +🧾 ⚙️ . + +& 📤 ➕ 🧰/✍ 🥉 🍵 ✍ `./scripts/docs.py`. + +!!! tip + 👆 🚫 💪 👀 📟 `./scripts/docs.py`, 👆 ⚙️ ⚫️ 📋 ⏸. + +🌐 🧾 ✍ 📁 📁 `./docs/en/`. + +📚 🔰 ✔️ 🍫 📟. + +🌅 💼, 👫 🍫 📟 ☑ 🏁 🈸 👈 💪 🏃. + +👐, 👈 🍫 📟 🚫 ✍ 🔘 ✍, 👫 🐍 📁 `./docs_src/` 📁. + +& 👈 🐍 📁 🔌/💉 🧾 🕐❔ 🏭 🕸. + +### 🩺 💯 + +🏆 💯 🤙 🏃 🛡 🖼 ℹ 📁 🧾. + +👉 ℹ ⚒ 💭 👈: + +* 🧾 🆙 📅. +* 🧾 🖼 💪 🏃. +* 🌅 ⚒ 📔 🧾, 🚚 💯 💰. + +⏮️ 🇧🇿 🛠️, 📤 ✍ 👈 🏗 🕸 & ✅ 🙆 🔀, 🖖-🔫: + +
+ +```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`. + +👈 🌌, 👆 💪 ✍ 🧾/ℹ 📁 & 👀 🔀 🖖. + +#### 🏎 ✳ (📦) + +👩‍🌾 📥 🎦 👆 ❔ ⚙️ ✍ `./scripts/docs.py` ⏮️ `python` 📋 🔗. + +✋️ 👆 💪 ⚙️ 🏎 ✳, & 👆 🔜 🤚 ✍ 👆 📶 📋 ⏮️ ❎ 🛠️. + +🚥 👆 ❎ 🏎 ✳, 👆 💪 ❎ 🛠️ ⏮️: + +
+ +```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` 🏆 🚫 ⚔. + +### ✍ + +ℹ ⏮️ ✍ 📶 🌅 👍 ❗ & ⚫️ 💪 🚫 🔨 🍵 ℹ ⚪️➡️ 👪. 👶 👶 + +📥 📶 ℹ ⏮️ ✍. + +#### 💁‍♂ & 📄 + +* ✅ ⏳ ♻ 🚲 📨 👆 🇪🇸 & 🚮 📄 ✔ 🔀 ⚖️ ✔ 👫. + +!!! tip + 👆 💪 🚮 🏤 ⏮️ 🔀 🔑 ♻ 🚲 📨. + + ✅ 🩺 🔃 ❎ 🚲 📨 📄 ✔ ⚫️ ⚖️ 📨 🔀. + +* ✅ 👀 🚥 📤 1️⃣ 🛠️ ✍ 👆 🇪🇸. + +* 🚮 👁 🚲 📨 📍 📃 💬. 👈 🔜 ⚒ ⚫️ 🌅 ⏩ 🎏 📄 ⚫️. + +🇪🇸 👤 🚫 💬, 👤 🔜 ⌛ 📚 🎏 📄 ✍ ⏭ 🔗. + +* 👆 💪 ✅ 🚥 📤 ✍ 👆 🇪🇸 & 🚮 📄 👫, 👈 🔜 ℹ 👤 💭 👈 ✍ ☑ & 👤 💪 🔗 ⚫️. + +* ⚙️ 🎏 🐍 🖼 & 🕴 💬 ✍ 🩺. 👆 🚫 ✔️ 🔀 🕳 👉 👷. + +* ⚙️ 🎏 🖼, 📁 📛, & 🔗. 👆 🚫 ✔️ 🔀 🕳 ⚫️ 👷. + +* ✅ 2️⃣-🔤 📟 🇪🇸 👆 💚 💬 👆 💪 ⚙️ 🏓 📇 💾 6️⃣3️⃣9️⃣-1️⃣ 📟. + +#### ♻ 🇪🇸 + +➡️ 💬 👆 💚 💬 📃 🇪🇸 👈 ⏪ ✔️ ✍ 📃, 💖 🇪🇸. + +💼 🇪🇸, 2️⃣-🔤 📟 `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.md){.internal-link target=_blank}. + +* 📁 📁: + +``` +docs/en/docs/features.md +``` + +* 📋 ⚫️ ⚫️❔ 🎏 🗺 ✋️ 🇪🇸 👆 💚 💬, ✅: + +``` +docs/es/docs/features.md +``` + +!!! tip + 👀 👈 🕴 🔀 ➡ & 📁 📛 🇪🇸 📟, ⚪️➡️ `en` `es`. + +* 🔜 📂 ⬜ 📁 📁 🇪🇸: + +``` +docs/en/mkdocs.yml +``` + +* 🔎 🥉 🌐❔ 👈 `docs/features.md` 🔎 📁 📁. 👱 💖: + +```YAML hl_lines="8" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +``` + +* 📂 ⬜ 📁 📁 🇪🇸 👆 ✍, ✅: + +``` +docs/es/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 + ✍ 🥇 🚲 📨 ⏮️ 👉, ⚒ 🆙 📳 🆕 🇪🇸, ⏭ ❎ ✍. + + 👈 🌌 🎏 💪 ℹ ⏮️ 🎏 📃 ⏪ 👆 👷 🔛 🥇 🕐. 👶 + +▶️ ✍ 👑 📃, `docs/ht/index.md`. + +⤴️ 👆 💪 😣 ⏮️ ⏮️ 👩‍🌾, "♻ 🇪🇸". + +##### 🆕 🇪🇸 🚫 🐕‍🦺 + +🚥 🕐❔ 🏃‍♂ 🖖 💽 ✍ 👆 🤚 ❌ 🔃 🇪🇸 🚫 ➖ 🐕‍🦺, 🕳 💖: + +``` + raise TemplateNotFound(template) +jinja2.exceptions.TemplateNotFound: partials/language/xx.html +``` + +👈 ⛓ 👈 🎢 🚫 🐕‍🦺 👈 🇪🇸 (👉 💼, ⏮️ ❌ 2️⃣-🔤 📟 `xx`). + +✋️ 🚫 😟, 👆 💪 ⚒ 🎢 🇪🇸 🇪🇸 & ⤴️ 💬 🎚 🩺. + +🚥 👆 💪 👈, ✍ `mkdocs.yml` 👆 🆕 🇪🇸, ⚫️ 🔜 ✔️ 🕳 💖: + +```YAML hl_lines="5" +site_name: FastAPI +# More stuff +theme: + # More stuff + language: xx +``` + +🔀 👈 🇪🇸 ⚪️➡️ `xx` (⚪️➡️ 👆 🇪🇸 📟) `en`. + +⤴️ 👆 💪 ▶️ 🖖 💽 🔄. + +#### 🎮 🏁 + +🕐❔ 👆 ⚙️ ✍ `./scripts/docs.py` ⏮️ `live` 📋 ⚫️ 🕴 🎦 📁 & ✍ 💪 ⏮️ 🇪🇸. + +✋️ 🕐 👆 🔨, 👆 💪 💯 ⚫️ 🌐 ⚫️ 🔜 👀 💳. + +👈, 🥇 🏗 🌐 🩺: + +
+ +```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/` 🔠 🇪🇸. 👉 🔌 ❎ 🙆 📁 ⏮️ ❌ ✍, ⏮️ 🗒 💬 👈 "👉 📁 🚫 ✔️ ✍". ✋️ 👆 🚫 ✔️ 🕳 ⏮️ 👈 📁. + +⤴️ ⚫️ 🏗 🌐 👈 🔬 ⬜ 🕸 🔠 🇪🇸, 🌀 👫, & 🏗 🏁 🔢 `./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 +``` + +
+ +## 💯 + +📤 ✍ 👈 👆 💪 🏃 🌐 💯 🌐 📟 & 🏗 💰 📄 🕸: + +
+ +```console +$ bash scripts/test-cov-html.sh +``` + +
+ +👉 📋 🏗 📁 `./htmlcov/`, 🚥 👆 📂 📁 `./htmlcov/index.html` 👆 🖥, 👆 💪 🔬 🖥 🇹🇼 📟 👈 📔 💯, & 👀 🚥 📤 🙆 🇹🇼 ❌. diff --git a/docs/em/docs/deployment/concepts.md b/docs/em/docs/deployment/concepts.md new file mode 100644 index 0000000000..162b68615a --- /dev/null +++ b/docs/em/docs/deployment/concepts.md @@ -0,0 +1,311 @@ +# 🛠️ 🔧 + +🕐❔ 🛠️ **FastAPI** 🈸, ⚖️ 🤙, 🙆 🆎 🕸 🛠️, 📤 📚 🔧 👈 👆 🎲 💅 🔃, & ⚙️ 👫 👆 💪 🔎 **🏆 ☑** 🌌 **🛠️ 👆 🈸**. + +⚠ 🔧: + +* 💂‍♂ - 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* 🧬 (🔢 🛠️ 🏃) +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +👥 🔜 👀 ❔ 👫 🔜 📉 **🛠️**. + +🔚, 🏆 🎯 💪 **🍦 👆 🛠️ 👩‍💻** 🌌 👈 **🔐**, **❎ 📉**, & ⚙️ **📊 ℹ** (🖼 🛰 💽/🕹 🎰) ♻ 💪. 👶 + +👤 🔜 💬 👆 🍖 🌖 🔃 👫 **🔧** 📥, & 👈 🔜 🤞 🤝 👆 **🤔** 👆 🔜 💪 💭 ❔ 🛠️ 👆 🛠️ 📶 🎏 🌐, 🎲 **🔮** 🕐 👈 🚫 🔀. + +🤔 👫 🔧, 👆 🔜 💪 **🔬 & 🔧** 🏆 🌌 🛠️ **👆 👍 🔗**. + +⏭ 📃, 👤 🔜 🤝 👆 🌅 **🧱 🍮** 🛠️ FastAPI 🈸. + +✋️ 🔜, ➡️ ✅ 👉 ⚠ **⚛ 💭**. 👫 🔧 ✔ 🙆 🎏 🆎 🕸 🛠️. 👶 + +## 💂‍♂ - 🇺🇸🔍 + +[⏮️ 📃 🔃 🇺🇸🔍](./https.md){.internal-link target=_blank} 👥 🇭🇲 🔃 ❔ 🇺🇸🔍 🚚 🔐 👆 🛠️. + +👥 👀 👈 🇺🇸🔍 🛎 🚚 🦲 **🔢** 👆 🈸 💽, **🤝 ❎ 🗳**. + +& 📤 ✔️ 🕳 🈚 **♻ 🇺🇸🔍 📄**, ⚫️ 💪 🎏 🦲 ⚖️ ⚫️ 💪 🕳 🎏. + +### 🖼 🧰 🇺🇸🔍 + +🧰 👆 💪 ⚙️ 🤝 ❎ 🗳: + +* Traefik + * 🔁 🍵 📄 🔕 👶 +* 📥 + * 🔁 🍵 📄 🔕 👶 +* 👌 + * ⏮️ 🔢 🦲 💖 Certbot 📄 🔕 +* ✳ + * ⏮️ 🔢 🦲 💖 Certbot 📄 🔕 +* Kubernetes ⏮️ 🚧 🕹 💖 👌 + * ⏮️ 🔢 🦲 💖 🛂-👨‍💼 📄 🔕 +* 🍵 🔘 ☁ 🐕‍🦺 🍕 👫 🐕‍🦺 (✍ 🔛 👶) + +➕1️⃣ 🎛 👈 👆 💪 ⚙️ **☁ 🐕‍🦺** 👈 🔨 🌖 👷 ✅ ⚒ 🆙 🇺🇸🔍. ⚫️ 💪 ✔️ 🚫 ⚖️ 🈚 👆 🌅, ♒️. ✋️ 👈 💼, 👆 🚫🔜 ✔️ ⚒ 🆙 🤝 ❎ 🗳 👆. + +👤 🔜 🎦 👆 🧱 🖼 ⏭ 📃. + +--- + +⤴️ ⏭ 🔧 🤔 🌐 🔃 📋 🏃 👆 ☑ 🛠️ (✅ Uvicorn). + +## 📋 & 🛠️ + +👥 🔜 💬 📚 🔃 🏃 "**🛠️**", ⚫️ ⚠ ✔️ ☯ 🔃 ⚫️❔ ⚫️ ⛓, & ⚫️❔ 🔺 ⏮️ 🔤 "**📋**". + +### ⚫️❔ 📋 + +🔤 **📋** 🛎 ⚙️ 🔬 📚 👜: + +* **📟** 👈 👆 ✍, **🐍 📁**. +* **📁** 👈 💪 **🛠️** 🏃‍♂ ⚙️, 🖼: `python`, `python.exe` ⚖️ `uvicorn`. +* 🎯 📋 ⏪ ⚫️ **🏃‍♂** 🔛 🏗 ⚙️, ⚙️ 💽, & ♻ 👜 🔛 💾. 👉 🤙 **🛠️**. + +### ⚫️❔ 🛠️ + +🔤 **🛠️** 🛎 ⚙️ 🌖 🎯 🌌, 🕴 🔗 👜 👈 🏃 🏃‍♂ ⚙️ (💖 🏁 ☝ 🔛): + +* 🎯 📋 ⏪ ⚫️ **🏃‍♂** 🔛 🏃‍♂ ⚙️. + * 👉 🚫 🔗 📁, 🚫 📟, ⚫️ 🔗 **🎯** 👜 👈 ➖ **🛠️** & 🔄 🏃‍♂ ⚙️. +* 🙆 📋, 🙆 📟, **💪 🕴 👜** 🕐❔ ⚫️ ➖ **🛠️**. , 🕐❔ 📤 **🛠️ 🏃**. +* 🛠️ 💪 **❎** (⚖️ "💥") 👆, ⚖️ 🏃‍♂ ⚙️. 👈 ☝, ⚫️ ⛔️ 🏃/➖ 🛠️, & ⚫️ 💪 **🙅‍♂ 📏 👜**. +* 🔠 🈸 👈 👆 ✔️ 🏃 🔛 👆 💻 ✔️ 🛠️ ⛅ ⚫️, 🔠 🏃‍♂ 📋, 🔠 🚪, ♒️. & 📤 🛎 📚 🛠️ 🏃 **🎏 🕰** ⏪ 💻 🔛. +* 📤 💪 **💗 🛠️** **🎏 📋** 🏃 🎏 🕰. + +🚥 👆 ✅ 👅 "📋 👨‍💼" ⚖️ "⚙️ 🖥" (⚖️ 🎏 🧰) 👆 🏃‍♂ ⚙️, 👆 🔜 💪 👀 📚 👈 🛠️ 🏃‍♂. + +& , 🖼, 👆 🔜 🎲 👀 👈 📤 💗 🛠️ 🏃 🎏 🖥 📋 (🦎, 💄, 📐, ♒️). 👫 🛎 🏃 1️⃣ 🛠️ 📍 📑, ➕ 🎏 ➕ 🛠️. + + + +--- + +🔜 👈 👥 💭 🔺 🖖 ⚖ **🛠️** & **📋**, ➡️ 😣 💬 🔃 🛠️. + +## 🏃‍♂ 🔛 🕴 + +🌅 💼, 🕐❔ 👆 ✍ 🕸 🛠️, 👆 💚 ⚫️ **🕧 🏃‍♂**, ➡, 👈 👆 👩‍💻 💪 🕧 🔐 ⚫️. 👉 ↗️, 🚥 👆 ✔️ 🎯 🤔 ⚫️❔ 👆 💚 ⚫️ 🏃 🕴 🎯 ⚠, ✋️ 🌅 🕰 👆 💚 ⚫️ 🕧 🏃‍♂ & **💪**. + +### 🛰 💽 + +🕐❔ 👆 ⚒ 🆙 🛰 💽 (☁ 💽, 🕹 🎰, ♒️.) 🙅 👜 👆 💪 🏃 Uvicorn (⚖️ 🎏) ❎, 🎏 🌌 👆 🕐❔ 🛠️ 🌐. + +& ⚫️ 🔜 👷 & 🔜 ⚠ **⏮️ 🛠️**. + +✋️ 🚥 👆 🔗 💽 💸, **🏃‍♂ 🛠️** 🔜 🎲 ☠️. + +& 🚥 💽 ⏏ (🖼 ⏮️ ℹ, ⚖️ 🛠️ ⚪️➡️ ☁ 🐕‍🦺) 👆 🎲 **🏆 🚫 👀 ⚫️**. & ↩️ 👈, 👆 🏆 🚫 💭 👈 👆 ✔️ ⏏ 🛠️ ❎. , 👆 🛠️ 🔜 🚧 ☠️. 👶 + +### 🏃 🔁 🔛 🕴 + +🏢, 👆 🔜 🎲 💚 💽 📋 (✅ Uvicorn) ▶️ 🔁 🔛 💽 🕴, & 🍵 💪 🙆 **🗿 🏥**, ✔️ 🛠️ 🕧 🏃 ⏮️ 👆 🛠️ (✅ Uvicorn 🏃‍♂ 👆 FastAPI 📱). + +### 🎏 📋 + +🏆 👉, 👆 🔜 🛎 ✔️ **🎏 📋** 👈 🔜 ⚒ 💭 👆 🈸 🏃 🔛 🕴. & 📚 💼, ⚫️ 🔜 ⚒ 💭 🎏 🦲 ⚖️ 🈸 🏃, 🖼, 💽. + +### 🖼 🧰 🏃 🕴 + +🖼 🧰 👈 💪 👉 👨‍🏭: + +* ☁ +* Kubernetes +* ☁ ✍ +* ☁ 🐝 📳 +* ✳ +* 👨‍💻 +* 🍵 🔘 ☁ 🐕‍🦺 🍕 👫 🐕‍🦺 +* 🎏... + +👤 🔜 🤝 👆 🌅 🧱 🖼 ⏭ 📃. + +## ⏏ + +🎏 ⚒ 💭 👆 🈸 🏃 🔛 🕴, 👆 🎲 💚 ⚒ 💭 ⚫️ **⏏** ⏮️ ❌. + +### 👥 ⚒ ❌ + +👥, 🗿, ⚒ **❌**, 🌐 🕰. 🖥 🌖 *🕧* ✔️ **🐛** 🕵‍♂ 🎏 🥉. 👶 + +& 👥 👩‍💻 🚧 📉 📟 👥 🔎 👈 🐛 & 👥 🛠️ 🆕 ⚒ (🎲 ❎ 🆕 🐛 💁‍♂️ 👶). + +### 🤪 ❌ 🔁 🍵 + +🕐❔ 🏗 🕸 🔗 ⏮️ FastAPI, 🚥 📤 ❌ 👆 📟, FastAPI 🔜 🛎 🔌 ⚫️ 👁 📨 👈 ⏲ ❌. 🛡 + +👩‍💻 🔜 🤚 **5️⃣0️⃣0️⃣ 🔗 💽 ❌** 👈 📨, ✋️ 🈸 🔜 😣 👷 ⏭ 📨 ↩️ 💥 🍕. + +### 🦏 ❌ - 💥 + +👐, 📤 5️⃣📆 💼 🌐❔ 👥 ✍ 📟 👈 **💥 🎂 🈸** ⚒ Uvicorn & 🐍 💥. 👶 + +& , 👆 🔜 🎲 🚫 💚 🈸 🚧 ☠️ ↩️ 📤 ❌ 1️⃣ 🥉, 👆 🎲 💚 ⚫️ **😣 🏃** 🌘 *➡ 🛠️* 👈 🚫 💔. + +### ⏏ ⏮️ 💥 + +✋️ 👈 💼 ⏮️ 🤙 👎 ❌ 👈 💥 🏃‍♂ **🛠️**, 👆 🔜 💚 🔢 🦲 👈 🈚 **🔁** 🛠️, 🌘 👩‍❤‍👨 🕰... + +!!! tip + ...👐 🚥 🎂 🈸 **💥 ⏪** ⚫️ 🎲 🚫 ⚒ 🔑 🚧 🔁 ⚫️ ♾. ✋️ 📚 💼, 👆 🔜 🎲 👀 ⚫️ ⏮️ 🛠️, ⚖️ 🌘 ▶️️ ⏮️ 🛠️. + + ➡️ 🎯 🔛 👑 💼, 🌐❔ ⚫️ 💪 💥 🍕 🎯 💼 **🔮**, & ⚫️ ⚒ 🔑 ⏏ ⚫️. + +👆 🔜 🎲 💚 ✔️ 👜 🈚 🔁 👆 🈸 **🔢 🦲**, ↩️ 👈 ☝, 🎏 🈸 ⏮️ Uvicorn & 🐍 ⏪ 💥, 📤 🕳 🎏 📟 🎏 📱 👈 💪 🕳 🔃 ⚫️. + +### 🖼 🧰 ⏏ 🔁 + +🏆 💼, 🎏 🧰 👈 ⚙️ **🏃 📋 🔛 🕴** ⚙️ 🍵 🏧 **⏏**. + +🖼, 👉 💪 🍵: + +* ☁ +* Kubernetes +* ☁ ✍ +* ☁ 🐝 📳 +* ✳ +* 👨‍💻 +* 🍵 🔘 ☁ 🐕‍🦺 🍕 👫 🐕‍🦺 +* 🎏... + +## 🧬 - 🛠️ & 💾 + +⏮️ FastAPI 🈸, ⚙️ 💽 📋 💖 Uvicorn, 🏃‍♂ ⚫️ 🕐 **1️⃣ 🛠️** 💪 🍦 💗 👩‍💻 🔁. + +✋️ 📚 💼, 👆 🔜 💚 🏃 📚 👨‍🏭 🛠️ 🎏 🕰. + +### 💗 🛠️ - 👨‍🏭 + +🚥 👆 ✔️ 🌅 👩‍💻 🌘 ⚫️❔ 👁 🛠️ 💪 🍵 (🖼 🚥 🕹 🎰 🚫 💁‍♂️ 🦏) & 👆 ✔️ **💗 🐚** 💽 💽, ⤴️ 👆 💪 ✔️ **💗 🛠️** 🏃‍♂ ⏮️ 🎏 🈸 🎏 🕰, & 📎 🌐 📨 👪 👫. + +🕐❔ 👆 🏃 **💗 🛠️** 🎏 🛠️ 📋, 👫 🛎 🤙 **👨‍🏭**. + +### 👨‍🏭 🛠️ & ⛴ + +💭 ⚪️➡️ 🩺 [🔃 🇺🇸🔍](./https.md){.internal-link target=_blank} 👈 🕴 1️⃣ 🛠️ 💪 👂 🔛 1️⃣ 🌀 ⛴ & 📢 📢 💽 ❓ + +👉 ☑. + +, 💪 ✔️ **💗 🛠️** 🎏 🕰, 📤 ✔️ **👁 🛠️ 👂 🔛 ⛴** 👈 ⤴️ 📶 📻 🔠 👨‍🏭 🛠️ 🌌. + +### 💾 📍 🛠️ + +🔜, 🕐❔ 📋 📐 👜 💾, 🖼, 🎰 🏫 🏷 🔢, ⚖️ 🎚 ⭕ 📁 🔢, 🌐 👈 **🍴 👄 💾 (💾)** 💽. + +& 💗 🛠️ 🛎 **🚫 💰 🙆 💾**. 👉 ⛓ 👈 🔠 🏃 🛠️ ✔️ 🚮 👍 👜, 🔢, & 💾. & 🚥 👆 😩 ⭕ 💸 💾 👆 📟, **🔠 🛠️** 🔜 🍴 🌓 💸 💾. + +### 💽 💾 + +🖼, 🚥 👆 📟 📐 🎰 🏫 🏷 ⏮️ **1️⃣ 💾 📐**, 🕐❔ 👆 🏃 1️⃣ 🛠️ ⏮️ 👆 🛠️, ⚫️ 🔜 🍴 🌘 1️⃣ 💾 💾. & 🚥 👆 ▶️ **4️⃣ 🛠️** (4️⃣ 👨‍🏭), 🔠 🔜 🍴 1️⃣ 💾 💾. 🌐, 👆 🛠️ 🔜 🍴 **4️⃣ 💾 💾**. + +& 🚥 👆 🛰 💽 ⚖️ 🕹 🎰 🕴 ✔️ 3️⃣ 💾 💾, 🔄 📐 🌅 🌘 4️⃣ 💾 💾 🔜 🤕 ⚠. 👶 + +### 💗 🛠️ - 🖼 + +👉 🖼, 📤 **👨‍💼 🛠️** 👈 ▶️ & 🎛 2️⃣ **👨‍🏭 🛠️**. + +👉 👨‍💼 🛠️ 🔜 🎲 1️⃣ 👂 🔛 **⛴** 📢. & ⚫️ 🔜 📶 🌐 📻 👨‍🏭 🛠️. + +👈 👨‍🏭 🛠️ 🔜 🕐 🏃‍♂ 👆 🈸, 👫 🔜 🎭 👑 📊 📨 **📨** & 📨 **📨**, & 👫 🔜 📐 🕳 👆 🚮 🔢 💾. + + + +& ↗️, 🎏 🎰 🔜 🎲 ✔️ **🎏 🛠️** 🏃 👍, ↖️ ⚪️➡️ 👆 🈸. + +😌 ℹ 👈 🌐 **💽 ⚙️** 🔠 🛠️ 💪 **🪀** 📚 🤭 🕰, ✋️ **💾 (💾)** 🛎 🚧 🌖 ⚖️ 🌘 **⚖**. + +🚥 👆 ✔️ 🛠️ 👈 🔨 ⭐ 💸 📊 🔠 🕰 & 👆 ✔️ 📚 👩‍💻, ⤴️ **💽 🛠️** 🔜 🎲 *⚖* (↩️ 🕧 🔜 🆙 & 🔽 🔜). + +### 🖼 🧬 🧰 & 🎛 + +📤 💪 📚 🎯 🏆 👉, & 👤 🔜 💬 👆 🌅 🔃 🎯 🎛 ⏭ 📃, 🖼 🕐❔ 💬 🔃 ☁ & 📦. + +👑 ⚛ 🤔 👈 📤 ✔️ **👁** 🦲 🚚 **⛴** **📢 📢**. & ⤴️ ⚫️ ✔️ ✔️ 🌌 **📶** 📻 🔁 **🛠️/👨‍🏭**. + +📥 💪 🌀 & 🎛: + +* **🐁** 🛠️ **Uvicorn 👨‍🏭** + * 🐁 🔜 **🛠️ 👨‍💼** 👂 🔛 **📢** & **⛴**, 🧬 🔜 ✔️ **💗 Uvicorn 👨‍🏭 🛠️** +* **Uvicorn** 🛠️ **Uvicorn 👨‍🏭** + * 1️⃣ Uvicorn **🛠️ 👨‍💼** 🔜 👂 🔛 **📢** & **⛴**, & ⚫️ 🔜 ▶️ **💗 Uvicorn 👨‍🏭 🛠️** +* **Kubernetes** & 🎏 📎 **📦 ⚙️** + * 🕳 **☁** 🧽 🔜 👂 🔛 **📢** & **⛴**. 🧬 🔜 ✔️ **💗 📦**, 🔠 ⏮️ **1️⃣ Uvicorn 🛠️** 🏃‍♂ +* **☁ 🐕‍🦺** 👈 🍵 👉 👆 + * ☁ 🐕‍🦺 🔜 🎲 **🍵 🧬 👆**. ⚫️ 🔜 🎲 ➡️ 👆 🔬 **🛠️ 🏃**, ⚖️ **📦 🖼** ⚙️, 🙆 💼, ⚫️ 🔜 🌅 🎲 **👁 Uvicorn 🛠️**, & ☁ 🐕‍🦺 🔜 🈚 🔁 ⚫️. + +!!! tip + 🚫 😟 🚥 👫 🏬 🔃 **📦**, ☁, ⚖️ Kubernetes 🚫 ⚒ 📚 🔑. + + 👤 🔜 💬 👆 🌅 🔃 📦 🖼, ☁, Kubernetes, ♒️. 🔮 📃: [FastAPI 📦 - ☁](./docker.md){.internal-link target=_blank}. + +## ⏮️ 🔁 ⏭ ▶️ + +📤 📚 💼 🌐❔ 👆 💚 🎭 📶 **⏭ ▶️** 👆 🈸. + +🖼, 👆 💪 💚 🏃 **💽 🛠️**. + +✋️ 🌅 💼, 👆 🔜 💚 🎭 👉 🔁 🕴 **🕐**. + +, 👆 🔜 💚 ✔️ **👁 🛠️** 🎭 👈 **⏮️ 🔁**, ⏭ ▶️ 🈸. + +& 👆 🔜 ✔️ ⚒ 💭 👈 ⚫️ 👁 🛠️ 🏃 👈 ⏮️ 🔁 ** 🚥 ⏮️, 👆 ▶️ **💗 🛠️** (💗 👨‍🏭) 🈸 ⚫️. 🚥 👈 🔁 🏃 **💗 🛠️**, 👫 🔜 **❎** 👷 🏃‍♂ ⚫️ 🔛 **🔗**, & 🚥 📶 🕳 💎 💖 💽 🛠️, 👫 💪 🤕 ⚔ ⏮️ 🔠 🎏. + +↗️, 📤 💼 🌐❔ 📤 🙅‍♂ ⚠ 🏃 ⏮️ 🔁 💗 🕰, 👈 💼, ⚫️ 📚 ⏩ 🍵. + +!!! tip + , ✔️ 🤯 👈 ⚓️ 🔛 👆 🖥, 💼 👆 **5️⃣📆 🚫 💪 🙆 ⏮️ 🔁** ⏭ ▶️ 👆 🈸. + + 👈 💼, 👆 🚫🔜 ✔️ 😟 🔃 🙆 👉. 🤷 + +### 🖼 ⏮️ 🔁 🎛 + +👉 🔜 **🪀 🙇** 🔛 🌌 👆 **🛠️ 👆 ⚙️**, & ⚫️ 🔜 🎲 🔗 🌌 👆 ▶️ 📋, 🚚 ⏏, ♒️. + +📥 💪 💭: + +* "🕑 📦" Kubernetes 👈 🏃 ⏭ 👆 📱 📦 +* 🎉 ✍ 👈 🏃 ⏮️ 🔁 & ⤴️ ▶️ 👆 🈸 + * 👆 🔜 💪 🌌 ▶️/⏏ *👈* 🎉 ✍, 🔍 ❌, ♒️. + +!!! tip + 👤 🔜 🤝 👆 🌅 🧱 🖼 🔨 👉 ⏮️ 📦 🔮 📃: [FastAPI 📦 - ☁](./docker.md){.internal-link target=_blank}. + +## ℹ 🛠️ + +👆 💽(Ⓜ) () **ℹ**, 👆 💪 🍴 ⚖️ **⚙️**, ⏮️ 👆 📋, 📊 🕰 🔛 💽, & 💾 💾 💪. + +❔ 🌅 ⚙️ ℹ 👆 💚 😩/♻ ❓ ⚫️ 💪 ⏩ 💭 "🚫 🌅", ✋️ 🌌, 👆 🔜 🎲 💚 🍴 **🌅 💪 🍵 💥**. + +🚥 👆 💸 3️⃣ 💽 ✋️ 👆 ⚙️ 🕴 🐥 🍖 👫 💾 & 💽, 👆 🎲 **🗑 💸** 👶, & 🎲 **🗑 💽 🔦 🏋️** 👶, ♒️. + +👈 💼, ⚫️ 💪 👻 ✔️ 🕴 2️⃣ 💽 & ⚙️ ↕ 🌐 👫 ℹ (💽, 💾, 💾, 🕸 💿, ♒️). + +🔛 🎏 ✋, 🚥 👆 ✔️ 2️⃣ 💽 & 👆 ⚙️ **1️⃣0️⃣0️⃣ 💯 👫 💽 & 💾**, ☝ 1️⃣ 🛠️ 🔜 💭 🌅 💾, & 💽 🔜 ✔️ ⚙️ 💾 "💾" (❔ 💪 💯 🕰 🐌), ⚖️ **💥**. ⚖️ 1️⃣ 🛠️ 💪 💪 📊 & 🔜 ✔️ ⌛ ⏭ 💽 🆓 🔄. + +👉 💼, ⚫️ 🔜 👍 🤚 **1️⃣ ➕ 💽** & 🏃 🛠️ 🔛 ⚫️ 👈 👫 🌐 ✔️ **🥃 💾 & 💽 🕰**. + +📤 🤞 👈 🤔 👆 ✔️ **🌵** ⚙️ 👆 🛠️. 🎲 ⚫️ 🚶 🦠, ⚖️ 🎲 🎏 🐕‍🦺 ⚖️ 🤖 ▶️ ⚙️ ⚫️. & 👆 💪 💚 ✔️ ➕ ℹ 🔒 👈 💼. + +👆 💪 🚮 **❌ 🔢** 🎯, 🖼, 🕳 **🖖 5️⃣0️⃣ 💯 9️⃣0️⃣ 💯** ℹ 🛠️. ☝ 👈 📚 🎲 👑 👜 👆 🔜 💚 ⚖ & ⚙️ ⚒ 👆 🛠️. + +👆 💪 ⚙️ 🙅 🧰 💖 `htop` 👀 💽 & 💾 ⚙️ 👆 💽 ⚖️ 💸 ⚙️ 🔠 🛠️. ⚖️ 👆 💪 ⚙️ 🌖 🏗 ⚖ 🧰, ❔ 5️⃣📆 📎 🤭 💽, ♒️. + +## 🌃 + +👆 ✔️ 👂 📥 👑 🔧 👈 👆 🔜 🎲 💪 ✔️ 🤯 🕐❔ 🤔 ❔ 🛠️ 👆 🈸: + +* 💂‍♂ - 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* 🧬 (🔢 🛠️ 🏃) +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +🤔 👉 💭 & ❔ ✔ 👫 🔜 🤝 👆 🤔 💪 ✊ 🙆 🚫 🕐❔ 🛠️ & 🛠️ 👆 🛠️. 👶 + +⏭ 📄, 👤 🔜 🤝 👆 🌅 🧱 🖼 💪 🎛 👆 💪 ⏩. 👶 diff --git a/docs/em/docs/deployment/docker.md b/docs/em/docs/deployment/docker.md new file mode 100644 index 0000000000..f28735ed71 --- /dev/null +++ b/docs/em/docs/deployment/docker.md @@ -0,0 +1,698 @@ +# FastAPI 📦 - ☁ + +🕐❔ 🛠️ FastAPI 🈸 ⚠ 🎯 🏗 **💾 📦 🖼**. ⚫️ 🛎 🔨 ⚙️ **☁**. 👆 💪 ⤴️ 🛠️ 👈 📦 🖼 1️⃣ 👩‍❤‍👨 💪 🌌. + +⚙️ 💾 📦 ✔️ 📚 📈 ✅ **💂‍♂**, **🔬**, **🦁**, & 🎏. + +!!! tip + 🏃 & ⏪ 💭 👉 💩 ❓ 🦘 [`Dockerfile` 🔛 👶](#build-a-docker-image-for-fastapi). + +
+📁 🎮 👶 + +```Dockerfile +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] + +# If running behind a proxy like Nginx or Traefik add --proxy-headers +# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"] +``` + +
+ +## ⚫️❔ 📦 + +📦 (✴️ 💾 📦) 📶 **💿** 🌌 📦 🈸 ✅ 🌐 👫 🔗 & 💪 📁 ⏪ 🚧 👫 ❎ ⚪️➡️ 🎏 📦 (🎏 🈸 ⚖️ 🦲) 🎏 ⚙️. + +💾 📦 🏃 ⚙️ 🎏 💾 💾 🦠 (🎰, 🕹 🎰, ☁ 💽, ♒️). 👉 ⛓ 👈 👫 📶 💿 (🔬 🌕 🕹 🎰 👍 🎂 🏃‍♂ ⚙️). + +👉 🌌, 📦 🍴 **🐥 ℹ**, 💸 ⭐ 🏃‍♂ 🛠️ 🔗 (🕹 🎰 🔜 🍴 🌅 🌅). + +📦 ✔️ 👫 👍 **❎** 🏃‍♂ 🛠️ (🛎 1️⃣ 🛠️), 📁 ⚙️, & 🕸, 🔬 🛠️, 💂‍♂, 🛠️, ♒️. + +## ⚫️❔ 📦 🖼 + +**📦** 🏃 ⚪️➡️ **📦 🖼**. + +📦 🖼 **🎻** ⏬ 🌐 📁, 🌐 🔢, & 🔢 📋/📋 👈 🔜 🎁 📦. **🎻** 📥 ⛓ 👈 📦 **🖼** 🚫 🏃, ⚫️ 🚫 ➖ 🛠️, ⚫️ 🕴 📦 📁 & 🗃. + +🔅 "**📦 🖼**" 👈 🏪 🎻 🎚,"**📦**" 🛎 🔗 🏃‍♂ 👐, 👜 👈 ➖ **🛠️**. + +🕐❔ **📦** ▶️ & 🏃‍♂ (▶️ ⚪️➡️ **📦 🖼**) ⚫️ 💪 ✍ ⚖️ 🔀 📁, 🌐 🔢, ♒️. 👈 🔀 🔜 🔀 🕴 👈 📦, ✋️ 🔜 🚫 😣 👽 📦 🖼 (🔜 🚫 🖊 💾). + +📦 🖼 ⭐ **📋** 📁 & 🎚, ✅ `python` & 📁 `main.py`. + +& **📦** ⚫️ (🔅 **📦 🖼**) ☑ 🏃 👐 🖼, ⭐ **🛠️**. 👐, 📦 🏃 🕴 🕐❔ ⚫️ ✔️ **🛠️ 🏃** (& 🛎 ⚫️ 🕴 👁 🛠️). 📦 ⛔️ 🕐❔ 📤 🙅‍♂ 🛠️ 🏃 ⚫️. + +## 📦 🖼 + +☁ ✔️ 1️⃣ 👑 🧰 ✍ & 🛠️ **📦 🖼** & **📦**. + +& 📤 📢 ☁ 🎡 ⏮️ 🏤-⚒ **🛂 📦 🖼** 📚 🧰, 🌐, 💽, & 🈸. + +🖼, 📤 🛂 🐍 🖼. + +& 📤 📚 🎏 🖼 🎏 👜 💖 💽, 🖼: + +* +* +* +* , ♒️. + +⚙️ 🏤-⚒ 📦 🖼 ⚫️ 📶 ⏩ **🌀** & ⚙️ 🎏 🧰. 🖼, 🔄 👅 🆕 💽. 🌅 💼, 👆 💪 ⚙️ **🛂 🖼**, & 🔗 👫 ⏮️ 🌐 🔢. + +👈 🌌, 📚 💼 👆 💪 💡 🔃 📦 & ☁ & 🏤-⚙️ 👈 💡 ⏮️ 📚 🎏 🧰 & 🦲. + +, 👆 🔜 🏃 **💗 📦** ⏮️ 🎏 👜, 💖 💽, 🐍 🈸, 🕸 💽 ⏮️ 😥 🕸 🈸, & 🔗 👫 👯‍♂️ 📨 👫 🔗 🕸. + +🌐 📦 🧾 ⚙️ (💖 ☁ ⚖️ Kubernetes) ✔️ 👫 🕸 ⚒ 🛠️ 🔘 👫. + +## 📦 & 🛠️ + +**📦 🖼** 🛎 🔌 🚮 🗃 🔢 📋 ⚖️ 📋 👈 🔜 🏃 🕐❔ **📦** ▶️ & 🔢 🚶‍♀️ 👈 📋. 📶 🎏 ⚫️❔ 🔜 🚥 ⚫️ 📋 ⏸. + +🕐❔ **📦** ▶️, ⚫️ 🔜 🏃 👈 📋/📋 (👐 👆 💪 🔐 ⚫️ & ⚒ ⚫️ 🏃 🎏 📋/📋). + +📦 🏃 📏 **👑 🛠️** (📋 ⚖️ 📋) 🏃. + +📦 🛎 ✔️ **👁 🛠️**, ✋️ ⚫️ 💪 ▶️ ✳ ⚪️➡️ 👑 🛠️, & 👈 🌌 👆 🔜 ✔️ **💗 🛠️** 🎏 📦. + +✋️ ⚫️ 🚫 💪 ✔️ 🏃‍♂ 📦 🍵 **🌘 1️⃣ 🏃‍♂ 🛠️**. 🚥 👑 🛠️ ⛔️, 📦 ⛔️. + +## 🏗 ☁ 🖼 FastAPI + +🆗, ➡️ 🏗 🕳 🔜 ❗ 👶 + +👤 🔜 🎦 👆 ❔ 🏗 **☁ 🖼** FastAPI **⚪️➡️ 🖌**, ⚓️ 🔛 **🛂 🐍** 🖼. + +👉 ⚫️❔ 👆 🔜 💚 **🏆 💼**, 🖼: + +* ⚙️ **Kubernetes** ⚖️ 🎏 🧰 +* 🕐❔ 🏃‍♂ 🔛 **🍓 👲** +* ⚙️ ☁ 🐕‍🦺 👈 🔜 🏃 📦 🖼 👆, ♒️. + +### 📦 📄 + +👆 🔜 🛎 ✔️ **📦 📄** 👆 🈸 📁. + +⚫️ 🔜 🪀 ✴️ 🔛 🧰 👆 ⚙️ **❎** 👈 📄. + +🌅 ⚠ 🌌 ⚫️ ✔️ 📁 `requirements.txt` ⏮️ 📦 📛 & 👫 ⏬, 1️⃣ 📍 ⏸. + +👆 🔜 ↗️ ⚙️ 🎏 💭 👆 ✍ [🔃 FastAPI ⏬](./versions.md){.internal-link target=_blank} ⚒ ↔ ⏬. + +🖼, 👆 `requirements.txt` 💪 👀 💖: + +``` +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +``` + +& 👆 🔜 🛎 ❎ 👈 📦 🔗 ⏮️ `pip`, 🖼: + +
+ +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic uvicorn +``` + +
+ +!!! info + 📤 🎏 📁 & 🧰 🔬 & ❎ 📦 🔗. + + 👤 🔜 🎦 👆 🖼 ⚙️ 🎶 ⏪ 📄 🔛. 👶 + +### ✍ **FastAPI** 📟 + +* ✍ `app` 📁 & ⛔ ⚫️. +* ✍ 🛁 📁 `__init__.py`. +* ✍ `main.py` 📁 ⏮️: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +### 📁 + +🔜 🎏 🏗 📁 ✍ 📁 `Dockerfile` ⏮️: + +```{ .dockerfile .annotate } +# (1) +FROM python:3.9 + +# (2) +WORKDIR /code + +# (3) +COPY ./requirements.txt /code/requirements.txt + +# (4) +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (5) +COPY ./app /code/app + +# (6) +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +``` + +1️⃣. ▶️ ⚪️➡️ 🛂 🐍 🧢 🖼. + +2️⃣. ⚒ ⏮️ 👷 📁 `/code`. + + 👉 🌐❔ 👥 🔜 🚮 `requirements.txt` 📁 & `app` 📁. + +3️⃣. 📁 📁 ⏮️ 📄 `/code` 📁. + + 📁 **🕴** 📁 ⏮️ 📄 🥇, 🚫 🎂 📟. + + 👉 📁 **🚫 🔀 🛎**, ☁ 🔜 🔍 ⚫️ & ⚙️ **💾** 👉 🔁, 🛠️ 💾 ⏭ 🔁 💁‍♂️. + +4️⃣. ❎ 📦 🔗 📄 📁. + + `--no-cache-dir` 🎛 💬 `pip` 🚫 🖊 ⏬ 📦 🌐, 👈 🕴 🚥 `pip` 🔜 🏃 🔄 ❎ 🎏 📦, ✋️ 👈 🚫 💼 🕐❔ 👷 ⏮️ 📦. + + !!! note + `--no-cache-dir` 🕴 🔗 `pip`, ⚫️ ✔️ 🕳 ⏮️ ☁ ⚖️ 📦. + + `--upgrade` 🎛 💬 `pip` ♻ 📦 🚥 👫 ⏪ ❎. + + ↩️ ⏮️ 🔁 🖨 📁 💪 🔍 **☁ 💾**, 👉 🔁 🔜 **⚙️ ☁ 💾** 🕐❔ 💪. + + ⚙️ 💾 👉 🔁 🔜 **🖊** 👆 📚 **🕰** 🕐❔ 🏗 🖼 🔄 & 🔄 ⏮️ 🛠️, ↩️ **⏬ & ❎** 🌐 🔗 **🔠 🕰**. + +5️⃣. 📁 `./app` 📁 🔘 `/code` 📁. + + 👉 ✔️ 🌐 📟 ❔ ⚫️❔ **🔀 🌅 🛎** ☁ **💾** 🏆 🚫 ⚙️ 👉 ⚖️ 🙆 **📄 🔁** 💪. + + , ⚫️ ⚠ 🚮 👉 **🏘 🔚** `Dockerfile`, 🔬 📦 🖼 🏗 🕰. + +6️⃣. ⚒ **📋** 🏃 `uvicorn` 💽. + + `CMD` ✊ 📇 🎻, 🔠 👫 🎻 ⚫️❔ 👆 🔜 🆎 📋 ⏸ 👽 🚀. + + 👉 📋 🔜 🏃 ⚪️➡️ **⏮️ 👷 📁**, 🎏 `/code` 📁 👆 ⚒ 🔛 ⏮️ `WORKDIR /code`. + + ↩️ 📋 🔜 ▶️ `/code` & 🔘 ⚫️ 📁 `./app` ⏮️ 👆 📟, **Uvicorn** 🔜 💪 👀 & **🗄** `app` ⚪️➡️ `app.main`. + +!!! tip + 📄 ⚫️❔ 🔠 ⏸ 🔨 🖊 🔠 🔢 💭 📟. 👶 + +👆 🔜 🔜 ✔️ 📁 📊 💖: + +``` +. +├── app +│   ├── __init__.py +│ └── main.py +├── Dockerfile +└── requirements.txt +``` + +#### ⛅ 🤝 ❎ 🗳 + +🚥 👆 🏃‍♂ 👆 📦 ⛅ 🤝 ❎ 🗳 (📐 ⚙) 💖 👌 ⚖️ Traefik, 🚮 🎛 `--proxy-headers`, 👉 🔜 💬 Uvicorn 💙 🎚 📨 👈 🗳 💬 ⚫️ 👈 🈸 🏃 ⛅ 🇺🇸🔍, ♒️. + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +#### ☁ 💾 + +📤 ⚠ 🎱 👉 `Dockerfile`, 👥 🥇 📁 **📁 ⏮️ 🔗 😞**, 🚫 🎂 📟. ➡️ 👤 💬 👆 ⚫️❔ 👈. + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +☁ & 🎏 🧰 **🏗** 👉 📦 🖼 **🔁**, 🚮 **1️⃣ 🧽 🔛 🔝 🎏**, ▶️ ⚪️➡️ 🔝 `Dockerfile` & ❎ 🙆 📁 ✍ 🔠 👩‍🌾 `Dockerfile`. + +☁ & 🎏 🧰 ⚙️ **🔗 💾** 🕐❔ 🏗 🖼, 🚥 📁 🚫 🔀 ↩️ 🏁 🕰 🏗 📦 🖼, ⤴️ ⚫️ 🔜 **🏤-⚙️ 🎏 🧽** ✍ 🏁 🕰, ↩️ 🖨 📁 🔄 & 🏗 🆕 🧽 ⚪️➡️ 🖌. + +❎ 📁 📁 🚫 🎯 📉 👜 💁‍♂️ 🌅, ✋️ ↩️ ⚫️ ⚙️ 💾 👈 🔁, ⚫️ 💪 **⚙️ 💾 ⏭ 🔁**. 🖼, ⚫️ 💪 ⚙️ 💾 👩‍🌾 👈 ❎ 🔗 ⏮️: + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +📁 ⏮️ 📦 📄 **🏆 🚫 🔀 🛎**. , 🖨 🕴 👈 📁, ☁ 🔜 💪 **⚙️ 💾** 👈 🔁. + +& ⤴️, ☁ 🔜 💪 **⚙️ 💾 ⏭ 🔁** 👈 ⏬ & ❎ 👈 🔗. & 📥 🌐❔ 👥 **🖊 📚 🕰**. 👶 ...& ❎ 😩 ⌛. 👶 👶 + +⏬ & ❎ 📦 🔗 **💪 ✊ ⏲**, ✋️ ⚙️ **💾** 🔜 **✊ 🥈** 🌅. + +& 👆 🔜 🏗 📦 🖼 🔄 & 🔄 ⏮️ 🛠️ ✅ 👈 👆 📟 🔀 👷, 📤 📚 📈 🕰 👉 🔜 🖊. + +⤴️, 🏘 🔚 `Dockerfile`, 👥 📁 🌐 📟. 👉 ⚫️❔ **🔀 🏆 🛎**, 👥 🚮 ⚫️ 🏘 🔚, ↩️ 🌖 🕧, 🕳 ⏮️ 👉 🔁 🔜 🚫 💪 ⚙️ 💾. + +```Dockerfile +COPY ./app /code/app +``` + +### 🏗 ☁ 🖼 + +🔜 👈 🌐 📁 🥉, ➡️ 🏗 📦 🖼. + +* 🚶 🏗 📁 (🌐❔ 👆 `Dockerfile` , ⚗ 👆 `app` 📁). +* 🏗 👆 FastAPI 🖼: + +
+ +```console +$ docker build -t myimage . + +---> 100% +``` + +
+ +!!! tip + 👀 `.` 🔚, ⚫️ 🌓 `./`, ⚫️ 💬 ☁ 📁 ⚙️ 🏗 📦 🖼. + + 👉 💼, ⚫️ 🎏 ⏮️ 📁 (`.`). + +### ▶️ ☁ 📦 + +* 🏃 📦 ⚓️ 🔛 👆 🖼: + +
+ +```console +$ docker run -d --name mycontainer -p 80:80 myimage +``` + +
+ +## ✅ ⚫️ + +👆 🔜 💪 ✅ ⚫️ 👆 ☁ 📦 📛, 🖼: http://192.168.99.100/items/5?q=somequery ⚖️ http://127.0.0.1/items/5?q=somequery (⚖️ 🌓, ⚙️ 👆 ☁ 🦠). + +👆 🔜 👀 🕳 💖: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +## 🎓 🛠️ 🩺 + +🔜 👆 💪 🚶 http://192.168.99.100/docs ⚖️ http://127.0.0.1/docs (⚖️ 🌓, ⚙️ 👆 ☁ 🦠). + +👆 🔜 👀 🏧 🎓 🛠️ 🧾 (🚚 🦁 🎚): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +## 🎛 🛠️ 🩺 + +& 👆 💪 🚶 http://192.168.99.100/redoc ⚖️ http://127.0.0.1/redoc (⚖️ 🌓, ⚙️ 👆 ☁ 🦠). + +👆 🔜 👀 🎛 🏧 🧾 (🚚 📄): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## 🏗 ☁ 🖼 ⏮️ 👁-📁 FastAPI + +🚥 👆 FastAPI 👁 📁, 🖼, `main.py` 🍵 `./app` 📁, 👆 📁 📊 💪 👀 💖 👉: + +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` + +⤴️ 👆 🔜 ✔️ 🔀 🔗 ➡ 📁 📁 🔘 `Dockerfile`: + +```{ .dockerfile .annotate hl_lines="10 13" } +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (1) +COPY ./main.py /code/ + +# (2) +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] +``` + +1️⃣. 📁 `main.py` 📁 `/code` 📁 🔗 (🍵 🙆 `./app` 📁). + +2️⃣. 🏃 Uvicorn & 💬 ⚫️ 🗄 `app` 🎚 ⚪️➡️ `main` (↩️ 🏭 ⚪️➡️ `app.main`). + +⤴️ 🔆 Uvicorn 📋 ⚙️ 🆕 🕹 `main` ↩️ `app.main` 🗄 FastAPI 🎚 `app`. + +## 🛠️ 🔧 + +➡️ 💬 🔄 🔃 🎏 [🛠️ 🔧](./concepts.md){.internal-link target=_blank} ⚖ 📦. + +📦 ✴️ 🧰 📉 🛠️ **🏗 & 🛠️** 🈸, ✋️ 👫 🚫 🛠️ 🎯 🎯 🍵 👉 **🛠️ 🔧**, & 📤 📚 💪 🎛. + +**👍 📰** 👈 ⏮️ 🔠 🎏 🎛 📤 🌌 📔 🌐 🛠️ 🔧. 👶 + +➡️ 📄 👉 **🛠️ 🔧** ⚖ 📦: + +* 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* 🧬 (🔢 🛠️ 🏃) +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +## 🇺🇸🔍 + +🚥 👥 🎯 🔛 **📦 🖼** FastAPI 🈸 (& ⏪ 🏃‍♂ **📦**), 🇺🇸🔍 🛎 🔜 🍵 **🗜** ➕1️⃣ 🧰. + +⚫️ 💪 ➕1️⃣ 📦, 🖼 ⏮️ Traefik, 🚚 **🇺🇸🔍** & **🏧** 🛠️ **📄**. + +!!! tip + Traefik ✔️ 🛠️ ⏮️ ☁, Kubernetes, & 🎏, ⚫️ 📶 ⏩ ⚒ 🆙 & 🔗 🇺🇸🔍 👆 📦 ⏮️ ⚫️. + +👐, 🇺🇸🔍 💪 🍵 ☁ 🐕‍🦺 1️⃣ 👫 🐕‍🦺 (⏪ 🏃 🈸 📦). + +## 🏃‍♂ 🔛 🕴 & ⏏ + +📤 🛎 ➕1️⃣ 🧰 🈚 **▶️ & 🏃‍♂** 👆 📦. + +⚫️ 💪 **☁** 🔗, **☁ ✍**, **Kubernetes**, **☁ 🐕‍🦺**, ♒️. + +🌅 (⚖️ 🌐) 💼, 📤 🙅 🎛 🛠️ 🏃 📦 🔛 🕴 & 🛠️ ⏏ 🔛 ❌. 🖼, ☁, ⚫️ 📋 ⏸ 🎛 `--restart`. + +🍵 ⚙️ 📦, ⚒ 🈸 🏃 🔛 🕴 & ⏮️ ⏏ 💪 ⚠ & ⚠. ✋️ 🕐❔ **👷 ⏮️ 📦** 🌅 💼 👈 🛠️ 🔌 🔢. 👶 + +## 🧬 - 🔢 🛠️ + +🚥 👆 ✔️ 🌑 🎰 ⏮️ **☁**, ☁ 🐝 📳, 🖖, ⚖️ ➕1️⃣ 🎏 🏗 ⚙️ 🛠️ 📎 📦 🔛 💗 🎰, ⤴️ 👆 🔜 🎲 💚 **🍵 🧬** **🌑 🎚** ↩️ ⚙️ **🛠️ 👨‍💼** (💖 🐁 ⏮️ 👨‍🏭) 🔠 📦. + +1️⃣ 📚 📎 📦 🧾 ⚙️ 💖 Kubernetes 🛎 ✔️ 🛠️ 🌌 🚚 **🧬 📦** ⏪ 🔗 **📐 ⚖** 📨 📨. 🌐 **🌑 🎚**. + +📚 💼, 👆 🔜 🎲 💚 🏗 **☁ 🖼 ⚪️➡️ 🖌** [🔬 🔛](#dockerfile), ❎ 👆 🔗, & 🏃‍♂ **👁 Uvicorn 🛠️** ↩️ 🏃‍♂ 🕳 💖 🐁 ⏮️ Uvicorn 👨‍🏭. + +### 📐 ⚙ + +🕐❔ ⚙️ 📦, 👆 🔜 🛎 ✔️ 🦲 **👂 🔛 👑 ⛴**. ⚫️ 💪 🎲 ➕1️⃣ 📦 👈 **🤝 ❎ 🗳** 🍵 **🇺🇸🔍** ⚖️ 🎏 🧰. + +👉 🦲 🔜 ✊ **📐** 📨 & 📎 👈 👪 👨‍🏭 (🤞) **⚖** 🌌, ⚫️ 🛎 🤙 **📐 ⚙**. + +!!! tip + 🎏 **🤝 ❎ 🗳** 🦲 ⚙️ 🇺🇸🔍 🔜 🎲 **📐 ⚙**. + +& 🕐❔ 👷 ⏮️ 📦, 🎏 ⚙️ 👆 ⚙️ ▶️ & 🛠️ 👫 🔜 ⏪ ✔️ 🔗 🧰 📶 **🕸 📻** (✅ 🇺🇸🔍 📨) ⚪️➡️ 👈 **📐 ⚙** (👈 💪 **🤝 ❎ 🗳**) 📦(Ⓜ) ⏮️ 👆 📱. + +### 1️⃣ 📐 ⚙ - 💗 👨‍🏭 📦 + +🕐❔ 👷 ⏮️ **Kubernetes** ⚖️ 🎏 📎 📦 🧾 ⚙️, ⚙️ 👫 🔗 🕸 🛠️ 🔜 ✔ 👁 **📐 ⚙** 👈 👂 🔛 👑 **⛴** 📶 📻 (📨) 🎲 **💗 📦** 🏃 👆 📱. + +🔠 👫 📦 🏃‍♂ 👆 📱 🔜 🛎 ✔️ **1️⃣ 🛠️** (✅ Uvicorn 🛠️ 🏃 👆 FastAPI 🈸). 👫 🔜 🌐 **🌓 📦**, 🏃‍♂ 🎏 👜, ✋️ 🔠 ⏮️ 🚮 👍 🛠️, 💾, ♒️. 👈 🌌 👆 🔜 ✊ 📈 **🛠️** **🎏 🐚** 💽, ⚖️ **🎏 🎰**. + +& 📎 📦 ⚙️ ⏮️ **📐 ⚙** 🔜 **📎 📨** 🔠 1️⃣ 📦 ⏮️ 👆 📱 **🔄**. , 🔠 📨 💪 🍵 1️⃣ 💗 **🔁 📦** 🏃 👆 📱. + +& 🛎 👉 **📐 ⚙** 🔜 💪 🍵 📨 👈 🚶 *🎏* 📱 👆 🌑 (✅ 🎏 🆔, ⚖️ 🔽 🎏 📛 ➡ 🔡), & 🔜 📶 👈 📻 ▶️️ 📦 *👈 🎏* 🈸 🏃‍♂ 👆 🌑. + +### 1️⃣ 🛠️ 📍 📦 + +👉 🆎 😐, 👆 🎲 🔜 💚 ✔️ **👁 (Uvicorn) 🛠️ 📍 📦**, 👆 🔜 ⏪ 🚚 🧬 🌑 🎚. + +, 👉 💼, 👆 **🔜 🚫** 💚 ✔️ 🛠️ 👨‍💼 💖 🐁 ⏮️ Uvicorn 👨‍🏭, ⚖️ Uvicorn ⚙️ 🚮 👍 Uvicorn 👨‍🏭. 👆 🔜 💚 ✔️ **👁 Uvicorn 🛠️** 📍 📦 (✋️ 🎲 💗 📦). + +✔️ ➕1️⃣ 🛠️ 👨‍💼 🔘 📦 (🔜 ⏮️ 🐁 ⚖️ Uvicorn 🛠️ Uvicorn 👨‍🏭) 🔜 🕴 🚮 **🙃 🔀** 👈 👆 🌅 🎲 ⏪ ✊ 💅 ⏮️ 👆 🌑 ⚙️. + +### 📦 ⏮️ 💗 🛠️ & 🎁 💼 + +↗️, 📤 **🎁 💼** 🌐❔ 👆 💪 💚 ✔️ **📦** ⏮️ **🐁 🛠️ 👨‍💼** ▶️ 📚 **Uvicorn 👨‍🏭 🛠️** 🔘. + +📚 💼, 👆 💪 ⚙️ **🛂 ☁ 🖼** 👈 🔌 **🐁** 🛠️ 👨‍💼 🏃‍♂ 💗 **Uvicorn 👨‍🏭 🛠️**, & 🔢 ⚒ 🔆 🔢 👨‍🏭 ⚓️ 🔛 ⏮️ 💽 🐚 🔁. 👤 🔜 💬 👆 🌅 🔃 ⚫️ 🔛 [🛂 ☁ 🖼 ⏮️ 🐁 - Uvicorn](#official-docker-image-with-gunicorn-uvicorn). + +📥 🖼 🕐❔ 👈 💪 ⚒ 🔑: + +#### 🙅 📱 + +👆 💪 💚 🛠️ 👨‍💼 📦 🚥 👆 🈸 **🙅 🥃** 👈 👆 🚫 💪 (🐥 🚫) 👌-🎶 🔢 🛠️ 💁‍♂️ 🌅, & 👆 💪 ⚙️ 🏧 🔢 (⏮️ 🛂 ☁ 🖼), & 👆 🏃‍♂ ⚫️ 🔛 **👁 💽**, 🚫 🌑. + +#### ☁ ✍ + +👆 💪 🛠️ **👁 💽** (🚫 🌑) ⏮️ **☁ ✍**, 👆 🚫🔜 ✔️ ⏩ 🌌 🛠️ 🧬 📦 (⏮️ ☁ ✍) ⏪ 🛡 🔗 🕸 & **📐 ⚖**. + +⤴️ 👆 💪 💚 ✔️ **👁 📦** ⏮️ **🛠️ 👨‍💼** ▶️ **📚 👨‍🏭 🛠️** 🔘. + +#### 🤴 & 🎏 🤔 + +👆 💪 ✔️ **🎏 🤔** 👈 🔜 ⚒ ⚫️ ⏩ ✔️ **👁 📦** ⏮️ **💗 🛠️** ↩️ ✔️ **💗 📦** ⏮️ **👁 🛠️** 🔠 👫. + +🖼 (🪀 🔛 👆 🖥) 👆 💪 ✔️ 🧰 💖 🤴 🏭 🎏 📦 👈 🔜 ✔️ 🔐 **🔠 📨** 👈 👟. + +👉 💼, 🚥 👆 ✔️ **💗 📦**, 🔢, 🕐❔ 🤴 👟 **✍ ⚖**, ⚫️ 🔜 🤚 🕐 **👁 📦 🔠 🕰** (📦 👈 🍵 👈 🎯 📨), ↩️ 🤚 **📈 ⚖** 🌐 🔁 📦. + +⤴️, 👈 💼, ⚫️ 💪 🙅 ✔️ **1️⃣ 📦** ⏮️ **💗 🛠️**, & 🇧🇿 🧰 (✅ 🤴 🏭) 🔛 🎏 📦 📈 🤴 ⚖ 🌐 🔗 🛠️ & 🎦 👈 ⚖ 🔛 👈 👁 📦. + +--- + +👑 ☝, **👌** 👉 **🚫 ✍ 🗿** 👈 👆 ✔️ 😄 ⏩. 👆 💪 ⚙️ 👫 💭 **🔬 👆 👍 ⚙️ 💼** & 💭 ⚫️❔ 👍 🎯 👆 ⚙️, ✅ 👅 ❔ 🛠️ 🔧: + +* 💂‍♂ - 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* 🧬 (🔢 🛠️ 🏃) +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +## 💾 + +🚥 👆 🏃 **👁 🛠️ 📍 📦** 👆 🔜 ✔️ 🌅 ⚖️ 🌘 👍-🔬, ⚖, & 📉 💸 💾 🍴 🔠 👈 📦 (🌅 🌘 1️⃣ 🚥 👫 🔁). + +& ⤴️ 👆 💪 ⚒ 👈 🎏 💾 📉 & 📄 👆 📳 👆 📦 🧾 ⚙️ (🖼 **Kubernetes**). 👈 🌌 ⚫️ 🔜 💪 **🔁 📦** **💪 🎰** ✊ 🔘 🏧 💸 💾 💪 👫, & 💸 💪 🎰 🌑. + +🚥 👆 🈸 **🙅**, 👉 🔜 🎲 **🚫 ⚠**, & 👆 💪 🚫 💪 ✔ 🏋️ 💾 📉. ✋️ 🚥 👆 **⚙️ 📚 💾** (🖼 ⏮️ **🎰 🏫** 🏷), 👆 🔜 ✅ ❔ 🌅 💾 👆 😩 & 🔆 **🔢 📦** 👈 🏃 **🔠 🎰** (& 🎲 🚮 🌖 🎰 👆 🌑). + +🚥 👆 🏃 **💗 🛠️ 📍 📦** (🖼 ⏮️ 🛂 ☁ 🖼) 👆 🔜 ✔️ ⚒ 💭 👈 🔢 🛠️ ▶️ 🚫 **🍴 🌖 💾** 🌘 ⚫️❔ 💪. + +## ⏮️ 🔁 ⏭ ▶️ & 📦 + +🚥 👆 ⚙️ 📦 (✅ ☁, Kubernetes), ⤴️ 📤 2️⃣ 👑 🎯 👆 💪 ⚙️. + +### 💗 📦 + +🚥 👆 ✔️ **💗 📦**, 🎲 🔠 1️⃣ 🏃 **👁 🛠️** (🖼, **Kubernetes** 🌑), ⤴️ 👆 🔜 🎲 💚 ✔️ **🎏 📦** 🔨 👷 **⏮️ 📶** 👁 📦, 🏃 👁 🛠️, **⏭** 🏃 🔁 👨‍🏭 📦. + +!!! info + 🚥 👆 ⚙️ Kubernetes, 👉 🔜 🎲 🕑 📦. + +🚥 👆 ⚙️ 💼 📤 🙅‍♂ ⚠ 🏃‍♂ 👈 ⏮️ 📶 **💗 🕰 🔗** (🖼 🚥 👆 🚫 🏃 💽 🛠️, ✋️ ✅ 🚥 💽 🔜), ⤴️ 👆 💪 🚮 👫 🔠 📦 ▶️️ ⏭ ▶️ 👑 🛠️. + +### 👁 📦 + +🚥 👆 ✔️ 🙅 🖥, ⏮️ **👁 📦** 👈 ⤴️ ▶️ 💗 **👨‍🏭 🛠️** (⚖️ 1️⃣ 🛠️), ⤴️ 👆 💪 🏃 👈 ⏮️ 🔁 🎏 📦, ▶️️ ⏭ ▶️ 🛠️ ⏮️ 📱. 🛂 ☁ 🖼 🐕‍🦺 👉 🔘. + +## 🛂 ☁ 🖼 ⏮️ 🐁 - Uvicorn + +📤 🛂 ☁ 🖼 👈 🔌 🐁 🏃‍♂ ⏮️ Uvicorn 👨‍🏭, ℹ ⏮️ 📃: [💽 👨‍🏭 - 🐁 ⏮️ Uvicorn](./server-workers.md){.internal-link target=_blank}. + +👉 🖼 🔜 ⚠ ✴️ ⚠ 🔬 🔛: [📦 ⏮️ 💗 🛠️ & 🎁 💼](#containers-with-multiple-processes-and-special-cases). + +* tiangolo/uvicorn-🐁-fastapi. + +!!! warning + 📤 ↕ 🤞 👈 👆 **🚫** 💪 👉 🧢 🖼 ⚖️ 🙆 🎏 🎏 1️⃣, & 🔜 👻 📆 🏗 🖼 ⚪️➡️ 🖌 [🔬 🔛: 🏗 ☁ 🖼 FastAPI](#build-a-docker-image-for-fastapi). + +👉 🖼 ✔️ **🚘-📳** 🛠️ 🔌 ⚒ **🔢 👨‍🏭 🛠️** ⚓️ 🔛 💽 🐚 💪. + +⚫️ ✔️ **🤔 🔢**, ✋️ 👆 💪 🔀 & ℹ 🌐 📳 ⏮️ **🌐 🔢** ⚖️ 📳 📁. + +⚫️ 🐕‍🦺 🏃 **⏮️ 🔁 ⏭ ▶️** ⏮️ ✍. + +!!! tip + 👀 🌐 📳 & 🎛, 🚶 ☁ 🖼 📃: Tiangolo/uvicorn-🐁-fastapi. + +### 🔢 🛠️ 🔛 🛂 ☁ 🖼 + +**🔢 🛠️** 🔛 👉 🖼 **📊 🔁** ⚪️➡️ 💽 **🐚** 💪. + +👉 ⛓ 👈 ⚫️ 🔜 🔄 **🗜** 🌅 **🎭** ⚪️➡️ 💽 💪. + +👆 💪 🔆 ⚫️ ⏮️ 📳 ⚙️ **🌐 🔢**, ♒️. + +✋️ ⚫️ ⛓ 👈 🔢 🛠️ 🪀 🔛 💽 📦 🏃, **💸 💾 🍴** 🔜 🪀 🔛 👈. + +, 🚥 👆 🈸 🍴 📚 💾 (🖼 ⏮️ 🎰 🏫 🏷), & 👆 💽 ✔️ 📚 💽 🐚 **✋️ 🐥 💾**, ⤴️ 👆 📦 💪 🔚 🆙 🔄 ⚙️ 🌅 💾 🌘 ⚫️❔ 💪, & 🤕 🎭 📚 (⚖️ 💥). 👶 + +### ✍ `Dockerfile` + +📥 ❔ 👆 🔜 ✍ `Dockerfile` ⚓️ 🔛 👉 🖼: + +```Dockerfile +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 + +COPY ./requirements.txt /app/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +COPY ./app /app +``` + +### 🦏 🈸 + +🚥 👆 ⏩ 📄 🔃 🏗 [🦏 🈸 ⏮️ 💗 📁](../tutorial/bigger-applications.md){.internal-link target=_blank}, 👆 `Dockerfile` 💪 ↩️ 👀 💖: + +```Dockerfile hl_lines="7" +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 + +COPY ./requirements.txt /app/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +COPY ./app /app/app +``` + +### 🕐❔ ⚙️ + +👆 🔜 🎲 **🚫** ⚙️ 👉 🛂 🧢 🖼 (⚖️ 🙆 🎏 🎏 1️⃣) 🚥 👆 ⚙️ **Kubernetes** (⚖️ 🎏) & 👆 ⏪ ⚒ **🧬** 🌑 🎚, ⏮️ 💗 **📦**. 📚 💼, 👆 👍 📆 **🏗 🖼 ⚪️➡️ 🖌** 🔬 🔛: [🏗 ☁ 🖼 FastAPI](#build-a-docker-image-for-fastapi). + +👉 🖼 🔜 ⚠ ✴️ 🎁 💼 🔬 🔛 [📦 ⏮️ 💗 🛠️ & 🎁 💼](#containers-with-multiple-processes-and-special-cases). 🖼, 🚥 👆 🈸 **🙅 🥃** 👈 ⚒ 🔢 🔢 🛠️ ⚓️ 🔛 💽 👷 👍, 👆 🚫 💚 😥 ⏮️ ❎ 🛠️ 🧬 🌑 🎚, & 👆 🚫 🏃 🌅 🌘 1️⃣ 📦 ⏮️ 👆 📱. ⚖️ 🚥 👆 🛠️ ⏮️ **☁ ✍**, 🏃 🔛 👁 💽, ♒️. + +## 🛠️ 📦 🖼 + +⏮️ ✔️ 📦 (☁) 🖼 📤 📚 🌌 🛠️ ⚫️. + +🖼: + +* ⏮️ **☁ ✍** 👁 💽 +* ⏮️ **Kubernetes** 🌑 +* ⏮️ ☁ 🐝 📳 🌑 +* ⏮️ ➕1️⃣ 🧰 💖 🖖 +* ⏮️ ☁ 🐕‍🦺 👈 ✊ 👆 📦 🖼 & 🛠️ ⚫️ + +## ☁ 🖼 ⏮️ 🎶 + +🚥 👆 ⚙️ 🎶 🛠️ 👆 🏗 🔗, 👆 💪 ⚙️ ☁ 👁-▶️ 🏗: + +```{ .dockerfile .annotate } +# (1) +FROM python:3.9 as requirements-stage + +# (2) +WORKDIR /tmp + +# (3) +RUN pip install poetry + +# (4) +COPY ./pyproject.toml ./poetry.lock* /tmp/ + +# (5) +RUN poetry export -f requirements.txt --output requirements.txt --without-hashes + +# (6) +FROM python:3.9 + +# (7) +WORKDIR /code + +# (8) +COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt + +# (9) +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (10) +COPY ./app /code/app + +# (11) +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +``` + +1️⃣. 👉 🥇 ▶️, ⚫️ 🌟 `requirements-stage`. + +2️⃣. ⚒ `/tmp` ⏮️ 👷 📁. + + 📥 🌐❔ 👥 🔜 🏗 📁 `requirements.txt` + +3️⃣. ❎ 🎶 👉 ☁ ▶️. + +4️⃣. 📁 `pyproject.toml` & `poetry.lock` 📁 `/tmp` 📁. + + ↩️ ⚫️ ⚙️ `./poetry.lock*` (▶️ ⏮️ `*`), ⚫️ 🏆 🚫 💥 🚥 👈 📁 🚫 💪. + +5️⃣. 🏗 `requirements.txt` 📁. + +6️⃣. 👉 🏁 ▶️, 🕳 📥 🔜 🛡 🏁 📦 🖼. + +7️⃣. ⚒ ⏮️ 👷 📁 `/code`. + +8️⃣. 📁 `requirements.txt` 📁 `/code` 📁. + + 👉 📁 🕴 🖖 ⏮️ ☁ ▶️, 👈 ⚫️❔ 👥 ⚙️ `--from-requirements-stage` 📁 ⚫️. + +9️⃣. ❎ 📦 🔗 🏗 `requirements.txt` 📁. + +1️⃣0️⃣. 📁 `app` 📁 `/code` 📁. + +1️⃣1️⃣. 🏃 `uvicorn` 📋, 💬 ⚫️ ⚙️ `app` 🎚 🗄 ⚪️➡️ `app.main`. + +!!! tip + 🖊 💭 🔢 👀 ⚫️❔ 🔠 ⏸ 🔨. + +**☁ ▶️** 🍕 `Dockerfile` 👈 👷 **🍕 📦 🖼** 👈 🕴 ⚙️ 🏗 📁 ⚙️ ⏪. + +🥇 ▶️ 🔜 🕴 ⚙️ **❎ 🎶** & **🏗 `requirements.txt`** ⏮️ 👆 🏗 🔗 ⚪️➡️ 🎶 `pyproject.toml` 📁. + +👉 `requirements.txt` 📁 🔜 ⚙️ ⏮️ `pip` ⏪ **⏭ ▶️**. + +🏁 📦 🖼 **🕴 🏁 ▶️** 🛡. ⏮️ ▶️(Ⓜ) 🔜 ❎. + +🕐❔ ⚙️ 🎶, ⚫️ 🔜 ⚒ 🔑 ⚙️ **☁ 👁-▶️ 🏗** ↩️ 👆 🚫 🤙 💪 ✔️ 🎶 & 🚮 🔗 ❎ 🏁 📦 🖼, 👆 **🕴 💪** ✔️ 🏗 `requirements.txt` 📁 ❎ 👆 🏗 🔗. + +⤴️ ⏭ (& 🏁) ▶️ 👆 🔜 🏗 🖼 🌅 ⚖️ 🌘 🎏 🌌 🔬 ⏭. + +### ⛅ 🤝 ❎ 🗳 - 🎶 + +🔄, 🚥 👆 🏃‍♂ 👆 📦 ⛅ 🤝 ❎ 🗳 (📐 ⚙) 💖 👌 ⚖️ Traefik, 🚮 🎛 `--proxy-headers` 📋: + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +## 🌃 + +⚙️ 📦 ⚙️ (✅ ⏮️ **☁** & **Kubernetes**) ⚫️ ▶️️ 📶 🎯 🍵 🌐 **🛠️ 🔧**: + +* 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* 🧬 (🔢 🛠️ 🏃) +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +🌅 💼, 👆 🎲 🏆 🚫 💚 ⚙️ 🙆 🧢 🖼, & ↩️ **🏗 📦 🖼 ⚪️➡️ 🖌** 1️⃣ ⚓️ 🔛 🛂 🐍 ☁ 🖼. + +✊ 💅 **✔** 👩‍🌾 `Dockerfile` & **☁ 💾** 👆 💪 **📉 🏗 🕰**, 📉 👆 📈 (& ❎ 😩). 👶 + +🎯 🎁 💼, 👆 💪 💚 ⚙️ 🛂 ☁ 🖼 FastAPI. 👶 diff --git a/docs/em/docs/deployment/https.md b/docs/em/docs/deployment/https.md new file mode 100644 index 0000000000..3feb3a2c2c --- /dev/null +++ b/docs/em/docs/deployment/https.md @@ -0,0 +1,190 @@ +# 🔃 🇺🇸🔍 + +⚫️ ⏩ 🤔 👈 🇺🇸🔍 🕳 👈 "🛠️" ⚖️ 🚫. + +✋️ ⚫️ 🌌 🌖 🏗 🌘 👈. + +!!! tip + 🚥 👆 🏃 ⚖️ 🚫 💅, 😣 ⏮️ ⏭ 📄 🔁 🔁 👩‍🌾 ⚒ 🌐 🆙 ⏮️ 🎏 ⚒. + +**💡 🔰 🇺🇸🔍**, ⚪️➡️ 🏬 🤔, ✅ https://howhttps.works/. + +🔜, ⚪️➡️ **👩‍💻 🤔**, 📥 📚 👜 ✔️ 🤯 ⏪ 💭 🔃 🇺🇸🔍: + +* 🇺🇸🔍, **💽** 💪 **✔️ "📄"** 🏗 **🥉 🥳**. + * 📚 📄 🤙 **🏆** ⚪️➡️ 🥉 🥳, 🚫 "🏗". +* 📄 ✔️ **1️⃣2️⃣🗓️**. + * 👫 **🕛**. + * & ⤴️ 👫 💪 **♻**, **🏆 🔄** ⚪️➡️ 🥉 🥳. +* 🔐 🔗 🔨 **🕸 🎚**. + * 👈 1️⃣ 🧽 **🔛 🇺🇸🔍**. + * , **📄 & 🔐** 🍵 🔨 **⏭ 🇺🇸🔍**. +* **🕸 🚫 💭 🔃 "🆔"**. 🕴 🔃 📢 📢. + * ℹ 🔃 **🎯 🆔** 📨 🚶 **🇺🇸🔍 💽**. +* **🇺🇸🔍 📄** "✔" **🎯 🆔**, ✋️ 🛠️ & 🔐 🔨 🕸 🎚, **⏭ 💭** ❔ 🆔 ➖ 🙅 ⏮️. +* **🔢**, 👈 🔜 ⛓ 👈 👆 💪 🕴 ✔️ **1️⃣ 🇺🇸🔍 📄 📍 📢 📢**. + * 🙅‍♂ 🤔 ❔ 🦏 👆 💽 ⚖️ ❔ 🤪 🔠 🈸 👆 ✔️ 🔛 ⚫️ 💪. + * 📤 **⚗** 👉, 👐. +* 📤 **↔** **🤝** 🛠️ (1️⃣ 🚚 🔐 🕸 🎚, ⏭ 🇺🇸🔍) 🤙 **👲**. + * 👉 👲 ↔ ✔ 1️⃣ 👁 💽 (⏮️ **👁 📢 📢**) ✔️ **📚 🇺🇸🔍 📄** & 🍦 **💗 🇺🇸🔍 🆔/🈸**. + * 👉 👷, **👁** 🦲 (📋) 🏃 🔛 💽, 👂 🔛 **📢 📢 📢**, 🔜 ✔️ **🌐 🇺🇸🔍 📄** 💽. +* **⏮️** 🏆 🔐 🔗, 📻 🛠️ **🇺🇸🔍**. + * 🎚 **🗜**, ✋️ 👫 ➖ 📨 ⏮️ **🇺🇸🔍 🛠️**. + +⚫️ ⚠ 💡 ✔️ **1️⃣ 📋/🇺🇸🔍 💽** 🏃 🔛 💽 (🎰, 🦠, ♒️.) & **🛠️ 🌐 🇺🇸🔍 🍕**: 📨 **🗜 🇺🇸🔍 📨**, 📨 **🗜 🇺🇸🔍 📨** ☑ 🇺🇸🔍 🈸 🏃 🎏 💽 ( **FastAPI** 🈸, 👉 💼), ✊ **🇺🇸🔍 📨** ⚪️➡️ 🈸, **🗜 ⚫️** ⚙️ ☑ **🇺🇸🔍 📄** & 📨 ⚫️ 🔙 👩‍💻 ⚙️ **🇺🇸🔍**. 👉 💽 🛎 🤙 **🤝 ❎ 🗳**. + +🎛 👆 💪 ⚙️ 🤝 ❎ 🗳: + +* Traefik (👈 💪 🍵 📄 🔕) +* 📥 (👈 💪 🍵 📄 🔕) +* 👌 +* ✳ + +## ➡️ 🗜 + +⏭ ➡️ 🗜, 👫 **🇺🇸🔍 📄** 💲 💙 🥉 🥳. + +🛠️ 📎 1️⃣ 👫 📄 ⚙️ ⚠, 🚚 📠 & 📄 😥. + +✋️ ⤴️ **➡️ 🗜** ✍. + +⚫️ 🏗 ⚪️➡️ 💾 🏛. ⚫️ 🚚 **🇺🇸🔍 📄 🆓**, 🏧 🌌. 👫 📄 ⚙️ 🌐 🐩 🔐 💂‍♂, & 📏-🖖 (🔃 3️⃣ 🗓️), **💂‍♂ 🤙 👍** ↩️ 👫 📉 🔆. + +🆔 🔐 ✔ & 📄 🏗 🔁. 👉 ✔ 🏧 🔕 👫 📄. + +💭 🏧 🛠️ & 🔕 👫 📄 👈 👆 💪 ✔️ **🔐 🇺🇸🔍, 🆓, ♾**. + +## 🇺🇸🔍 👩‍💻 + +📥 🖼 ❔ 🇺🇸🔍 🛠️ 💪 👀 💖, 🔁 🔁, 💸 🙋 ✴️ 💭 ⚠ 👩‍💻. + +### 🆔 📛 + +⚫️ 🔜 🎲 🌐 ▶️ 👆 **🏗** **🆔 📛**. ⤴️, 👆 🔜 🔗 ⚫️ 🏓 💽 (🎲 👆 🎏 ☁ 🐕‍🦺). + +👆 🔜 🎲 🤚 ☁ 💽 (🕹 🎰) ⚖️ 🕳 🎏, & ⚫️ 🔜 ✔️ 🔧 **📢 📢 📢**. + +🏓 💽(Ⓜ) 👆 🔜 🔗 ⏺ ("`A record`") ☝ **👆 🆔** 📢 **📢 📢 👆 💽**. + +👆 🔜 🎲 👉 🕐, 🥇 🕰, 🕐❔ ⚒ 🌐 🆙. + +!!! tip + 👉 🆔 📛 🍕 🌌 ⏭ 🇺🇸🔍, ✋️ 🌐 🪀 🔛 🆔 & 📢 📢, ⚫️ 💸 💬 ⚫️ 📥. + +### 🏓 + +🔜 ➡️ 🎯 🔛 🌐 ☑ 🇺🇸🔍 🍕. + +🥇, 🖥 🔜 ✅ ⏮️ **🏓 💽** ⚫️❔ **📢 🆔**, 👉 💼, `someapp.example.com`. + +🏓 💽 🔜 💬 🖥 ⚙️ 🎯 **📢 📢**. 👈 🔜 📢 📢 📢 ⚙️ 👆 💽, 👈 👆 🔗 🏓 💽. + + + +### 🤝 🤝 ▶️ + +🖥 🔜 ⤴️ 🔗 ⏮️ 👈 📢 📢 🔛 **⛴ 4️⃣4️⃣3️⃣** (🇺🇸🔍 ⛴). + +🥇 🍕 📻 🛠️ 🔗 🖖 👩‍💻 & 💽 & 💭 🔐 🔑 👫 🔜 ⚙️, ♒️. + + + +👉 🔗 🖖 👩‍💻 & 💽 🛠️ 🤝 🔗 🤙 **🤝 🤝**. + +### 🤝 ⏮️ 👲 ↔ + +**🕴 1️⃣ 🛠️** 💽 💪 👂 🔛 🎯 **⛴** 🎯 **📢 📢**. 📤 💪 🎏 🛠️ 👂 🔛 🎏 ⛴ 🎏 📢 📢, ✋️ 🕴 1️⃣ 🔠 🌀 📢 📢 & ⛴. + +🤝 (🇺🇸🔍) ⚙️ 🎯 ⛴ `443` 🔢. 👈 ⛴ 👥 🔜 💪. + +🕴 1️⃣ 🛠️ 💪 👂 🔛 👉 ⛴, 🛠️ 👈 🔜 ⚫️ 🔜 **🤝 ❎ 🗳**. + +🤝 ❎ 🗳 🔜 ✔️ 🔐 1️⃣ ⚖️ 🌅 **🤝 📄** (🇺🇸🔍 📄). + +⚙️ **👲 ↔** 🔬 🔛, 🤝 ❎ 🗳 🔜 ✅ ❔ 🤝 (🇺🇸🔍) 📄 💪 ⚫️ 🔜 ⚙️ 👉 🔗, ⚙️ 1️⃣ 👈 🏏 🆔 📈 👩‍💻. + +👉 💼, ⚫️ 🔜 ⚙️ 📄 `someapp.example.com`. + + + +👩‍💻 ⏪ **💙** 👨‍💼 👈 🏗 👈 🤝 📄 (👉 💼 ➡️ 🗜, ✋️ 👥 🔜 👀 🔃 👈 ⏪), ⚫️ 💪 **✔** 👈 📄 ☑. + +⤴️, ⚙️ 📄, 👩‍💻 & 🤝 ❎ 🗳 **💭 ❔ 🗜** 🎂 **🕸 📻**. 👉 🏁 **🤝 🤝** 🍕. + +⏮️ 👉, 👩‍💻 & 💽 ✔️ **🗜 🕸 🔗**, 👉 ⚫️❔ 🤝 🚚. & ⤴️ 👫 💪 ⚙️ 👈 🔗 ▶️ ☑ **🇺🇸🔍 📻**. + +& 👈 ⚫️❔ **🇺🇸🔍** , ⚫️ ✅ **🇺🇸🔍** 🔘 **🔐 🤝 🔗** ↩️ 😁 (💽) 🕸 🔗. + +!!! tip + 👀 👈 🔐 📻 🔨 **🕸 🎚**, 🚫 🇺🇸🔍 🎚. + +### 🇺🇸🔍 📨 + +🔜 👈 👩‍💻 & 💽 (🎯 🖥 & 🤝 ❎ 🗳) ✔️ **🗜 🕸 🔗**, 👫 💪 ▶️ **🇺🇸🔍 📻**. + +, 👩‍💻 📨 **🇺🇸🔍 📨**. 👉 🇺🇸🔍 📨 🔘 🗜 🤝 🔗. + + + +### 🗜 📨 + +🤝 ❎ 🗳 🔜 ⚙️ 🔐 ✔ **🗜 📨**, & 🔜 📶 **✅ (🗜) 🇺🇸🔍 📨** 🛠️ 🏃 🈸 (🖼 🛠️ ⏮️ Uvicorn 🏃‍♂ FastAPI 🈸). + + + +### 🇺🇸🔍 📨 + +🈸 🔜 🛠️ 📨 & 📨 **✅ (💽) 🇺🇸🔍 📨** 🤝 ❎ 🗳. + + + +### 🇺🇸🔍 📨 + +🤝 ❎ 🗳 🔜 ⤴️ **🗜 📨** ⚙️ ⚛ ✔ ⏭ (👈 ▶️ ⏮️ 📄 `someapp.example.com`), & 📨 ⚫️ 🔙 🖥. + +⏭, 🖥 🔜 ✔ 👈 📨 ☑ & 🗜 ⏮️ ▶️️ 🔐 🔑, ♒️. ⚫️ 🔜 ⤴️ **🗜 📨** & 🛠️ ⚫️. + + + +👩‍💻 (🖥) 🔜 💭 👈 📨 👟 ⚪️➡️ ☑ 💽 ↩️ ⚫️ ⚙️ ⚛ 👫 ✔ ⚙️ **🇺🇸🔍 📄** ⏭. + +### 💗 🈸 + +🎏 💽 (⚖️ 💽), 📤 💪 **💗 🈸**, 🖼, 🎏 🛠️ 📋 ⚖️ 💽. + +🕴 1️⃣ 🛠️ 💪 🚚 🎯 📢 & ⛴ (🤝 ❎ 🗳 👆 🖼) ✋️ 🎏 🈸/🛠️ 💪 🏃 🔛 💽(Ⓜ) 💁‍♂️, 📏 👫 🚫 🔄 ⚙️ 🎏 **🌀 📢 📢 & ⛴**. + + + +👈 🌌, 🤝 ❎ 🗳 💪 🍵 🇺🇸🔍 & 📄 **💗 🆔**, 💗 🈸, & ⤴️ 📶 📨 ▶️️ 🈸 🔠 💼. + +### 📄 🔕 + +☝ 🔮, 🔠 📄 🔜 **🕛** (🔃 3️⃣ 🗓️ ⏮️ 🏗 ⚫️). + +& ⤴️, 📤 🔜 ➕1️⃣ 📋 (💼 ⚫️ ➕1️⃣ 📋, 💼 ⚫️ 💪 🎏 🤝 ❎ 🗳) 👈 🔜 💬 ➡️ 🗜, & ♻ 📄(Ⓜ). + + + +**🤝 📄** **🔗 ⏮️ 🆔 📛**, 🚫 ⏮️ 📢 📢. + +, ♻ 📄, 🔕 📋 💪 **🎦** 🛃 (➡️ 🗜) 👈 ⚫️ 👐 **"👍" & 🎛 👈 🆔**. + +👈, & 🏗 🎏 🈸 💪, 📤 📚 🌌 ⚫️ 💪 ⚫️. 🌟 🌌: + +* **🔀 🏓 ⏺**. + * 👉, 🔕 📋 💪 🐕‍🦺 🔗 🏓 🐕‍🦺,, ⚓️ 🔛 🏓 🐕‍🦺 👆 ⚙️, 👉 5️⃣📆 ⚖️ 💪 🚫 🎛. +* **🏃 💽** (🌘 ⏮️ 📄 🛠️ 🛠️) 🔛 📢 📢 📢 🔗 ⏮️ 🆔. + * 👥 💬 🔛, 🕴 1️⃣ 🛠️ 💪 👂 🔛 🎯 📢 & ⛴. + * 👉 1️⃣ 🤔 ⚫️❔ ⚫️ 📶 ⚠ 🕐❔ 🎏 🤝 ❎ 🗳 ✊ 💅 📄 🔕 🛠️. + * ⏪, 👆 💪 ✔️ ⛔️ 🤝 ❎ 🗳 😖, ▶️ 🔕 📋 📎 📄, ⤴️ 🔗 👫 ⏮️ 🤝 ❎ 🗳, & ⤴️ ⏏ 🤝 ❎ 🗳. 👉 🚫 💯, 👆 📱(Ⓜ) 🔜 🚫 💪 ⏮️ 🕰 👈 🤝 ❎ 🗳 📆. + +🌐 👉 🔕 🛠️, ⏪ 🍦 📱, 1️⃣ 👑 🤔 ⚫️❔ 👆 🔜 💚 ✔️ **🎏 ⚙️ 🍵 🇺🇸🔍** ⏮️ 🤝 ❎ 🗳 ↩️ ⚙️ 🤝 📄 ⏮️ 🈸 💽 🔗 (✅ Uvicorn). + +## 🌃 + +✔️ **🇺🇸🔍** 📶 ⚠, & **🎯** 🏆 💼. 🌅 🎯 👆 👩‍💻 ✔️ 🚮 🤭 🇺🇸🔍 🔃 **🤔 👉 🔧** & ❔ 👫 👷. + +✋️ 🕐 👆 💭 🔰 ℹ **🇺🇸🔍 👩‍💻** 👆 💪 💪 🌀 & 🔗 🎏 🧰 ℹ 👆 🛠️ 🌐 🙅 🌌. + +⏭ 📃, 👤 🔜 🎦 👆 📚 🧱 🖼 ❔ ⚒ 🆙 **🇺🇸🔍** **FastAPI** 🈸. 👶 diff --git a/docs/em/docs/deployment/index.md b/docs/em/docs/deployment/index.md new file mode 100644 index 0000000000..9bcf427b69 --- /dev/null +++ b/docs/em/docs/deployment/index.md @@ -0,0 +1,21 @@ +# 🛠️ + +🛠️ **FastAPI** 🈸 📶 ⏩. + +## ⚫️❔ 🔨 🛠️ ⛓ + +**🛠️** 🈸 ⛓ 🎭 💪 📶 ⚒ ⚫️ **💪 👩‍💻**. + +**🕸 🛠️**, ⚫️ 🛎 🔌 🚮 ⚫️ **🛰 🎰**, ⏮️ **💽 📋** 👈 🚚 👍 🎭, ⚖, ♒️, 👈 👆 **👩‍💻** 💪 **🔐** 🈸 ♻ & 🍵 🔁 ⚖️ ⚠. + +👉 🔅 **🛠️** ▶️, 🌐❔ 👆 🕧 🔀 📟, 💔 ⚫️ & ♻ ⚫️, ⛔️ & 🔁 🛠️ 💽, ♒️. + +## 🛠️ 🎛 + +📤 📚 🌌 ⚫️ ⚓️ 🔛 👆 🎯 ⚙️ 💼 & 🧰 👈 👆 ⚙️. + +👆 💪 **🛠️ 💽** 👆 ⚙️ 🌀 🧰, 👆 💪 ⚙️ **☁ 🐕‍🦺** 👈 🔨 🍕 👷 👆, ⚖️ 🎏 💪 🎛. + +👤 🔜 🎦 👆 👑 🔧 👆 🔜 🎲 ✔️ 🤯 🕐❔ 🛠️ **FastAPI** 🈸 (👐 🌅 ⚫️ ✔ 🙆 🎏 🆎 🕸 🈸). + +👆 🔜 👀 🌖 ℹ ✔️ 🤯 & ⚒ ⚫️ ⏭ 📄. 👶 diff --git a/docs/em/docs/deployment/manually.md b/docs/em/docs/deployment/manually.md new file mode 100644 index 0000000000..f27b423e24 --- /dev/null +++ b/docs/em/docs/deployment/manually.md @@ -0,0 +1,145 @@ +# 🏃 💽 ❎ - Uvicorn + +👑 👜 👆 💪 🏃 **FastAPI** 🈸 🛰 💽 🎰 🔫 💽 📋 💖 **Uvicorn**. + +📤 3️⃣ 👑 🎛: + +* Uvicorn: ↕ 🎭 🔫 💽. +* Hypercorn: 🔫 💽 🔗 ⏮️ 🇺🇸🔍/2️⃣ & 🎻 👪 🎏 ⚒. +* 👸: 🔫 💽 🏗 ✳ 📻. + +## 💽 🎰 & 💽 📋 + +📤 🤪 ℹ 🔃 📛 ✔️ 🤯. 👶 + +🔤 "**💽**" 🛎 ⚙️ 🔗 👯‍♂️ 🛰/☁ 💻 (⚛ ⚖️ 🕹 🎰) & 📋 👈 🏃‍♂ 🔛 👈 🎰 (✅ Uvicorn). + +✔️ 👈 🤯 🕐❔ 👆 ✍ "💽" 🏢, ⚫️ 💪 🔗 1️⃣ 📚 2️⃣ 👜. + +🕐❔ 🔗 🛰 🎰, ⚫️ ⚠ 🤙 ⚫️ **💽**, ✋️ **🎰**, **💾** (🕹 🎰), **🕸**. 👈 🌐 🔗 🆎 🛰 🎰, 🛎 🏃‍♂ 💾, 🌐❔ 👆 🏃 📋. + +## ❎ 💽 📋 + +👆 💪 ❎ 🔫 🔗 💽 ⏮️: + +=== "Uvicorn" + + * Uvicorn, 🌩-⏩ 🔫 💽, 🏗 🔛 uvloop & httptool. + +
+ + ```console + $ pip install "uvicorn[standard]" + + ---> 100% + ``` + +
+ + !!! tip + ❎ `standard`, Uvicorn 🔜 ❎ & ⚙️ 👍 ➕ 🔗. + + 👈 ✅ `uvloop`, ↕-🎭 💧-♻ `asyncio`, 👈 🚚 🦏 🛠️ 🎭 📈. + +=== "Hypercorn" + + * Hypercorn, 🔫 💽 🔗 ⏮️ 🇺🇸🔍/2️⃣. + +
+ + ```console + $ pip install hypercorn + + ---> 100% + ``` + +
+ + ...⚖️ 🙆 🎏 🔫 💽. + +## 🏃 💽 📋 + +👆 💪 ⤴️ 🏃 👆 🈸 🎏 🌌 👆 ✔️ ⌛ 🔰, ✋️ 🍵 `--reload` 🎛, ✅: + +=== "Uvicorn" + +
+ + ```console + $ uvicorn main:app --host 0.0.0.0 --port 80 + + INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) + ``` + +
+ +=== "Hypercorn" + +
+ + ```console + $ hypercorn main:app --bind 0.0.0.0:80 + + Running on 0.0.0.0:8080 over http (CTRL + C to quit) + ``` + +
+ +!!! warning + 💭 ❎ `--reload` 🎛 🚥 👆 ⚙️ ⚫️. + + `--reload` 🎛 🍴 🌅 🌅 ℹ, 🌅 ⚠, ♒️. + + ⚫️ ℹ 📚 ⏮️ **🛠️**, ✋️ 👆 **🚫🔜 🚫** ⚙️ ⚫️ **🏭**. + +## Hypercorn ⏮️ 🎻 + +💃 & **FastAPI** ⚓️ 🔛 AnyIO, ❔ ⚒ 👫 🔗 ⏮️ 👯‍♂️ 🐍 🐩 🗃 & 🎻. + +👐, Uvicorn ⏳ 🕴 🔗 ⏮️ ✳, & ⚫️ 🛎 ⚙️ `uvloop`, ↕-🎭 💧-♻ `asyncio`. + +✋️ 🚥 👆 💚 🔗 ⚙️ **🎻**, ⤴️ 👆 💪 ⚙️ **Hypercorn** ⚫️ 🐕‍🦺 ⚫️. 👶 + +### ❎ Hypercorn ⏮️ 🎻 + +🥇 👆 💪 ❎ Hypercorn ⏮️ 🎻 🐕‍🦺: + +
+ +```console +$ pip install "hypercorn[trio]" +---> 100% +``` + +
+ +### 🏃 ⏮️ 🎻 + +⤴️ 👆 💪 🚶‍♀️ 📋 ⏸ 🎛 `--worker-class` ⏮️ 💲 `trio`: + +
+ +```console +$ hypercorn main:app --worker-class trio +``` + +
+ +& 👈 🔜 ▶️ Hypercorn ⏮️ 👆 📱 ⚙️ 🎻 👩‍💻. + +🔜 👆 💪 ⚙️ 🎻 🔘 👆 📱. ⚖️ 👍, 👆 💪 ⚙️ AnyIO, 🚧 👆 📟 🔗 ⏮️ 👯‍♂️ 🎻 & ✳. 👶 + +## 🛠️ 🔧 + +👫 🖼 🏃 💽 📋 (📧.Ⓜ Uvicorn), ▶️ **👁 🛠️**, 👂 🔛 🌐 📢 (`0.0.0.0`) 🔛 🔁 ⛴ (✅ `80`). + +👉 🔰 💭. ✋️ 👆 🔜 🎲 💚 ✊ 💅 🌖 👜, 💖: + +* 💂‍♂ - 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* 🧬 (🔢 🛠️ 🏃) +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +👤 🔜 💬 👆 🌅 🔃 🔠 👫 🔧, ❔ 💭 🔃 👫, & 🧱 🖼 ⏮️ 🎛 🍵 👫 ⏭ 📃. 👶 diff --git a/docs/em/docs/deployment/server-workers.md b/docs/em/docs/deployment/server-workers.md new file mode 100644 index 0000000000..b7e58c4f47 --- /dev/null +++ b/docs/em/docs/deployment/server-workers.md @@ -0,0 +1,178 @@ +# 💽 👨‍🏭 - 🐁 ⏮️ Uvicorn + +➡️ ✅ 🔙 👈 🛠️ 🔧 ⚪️➡️ ⏭: + +* 💂‍♂ - 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* **🧬 (🔢 🛠️ 🏃)** +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +🆙 👉 ☝, ⏮️ 🌐 🔰 🩺, 👆 ✔️ 🎲 🏃‍♂ **💽 📋** 💖 Uvicorn, 🏃‍♂ **👁 🛠️**. + +🕐❔ 🛠️ 🈸 👆 🔜 🎲 💚 ✔️ **🧬 🛠️** ✊ 📈 **💗 🐚** & 💪 🍵 🌅 📨. + +👆 👀 ⏮️ 📃 🔃 [🛠️ 🔧](./concepts.md){.internal-link target=_blank}, 📤 💗 🎛 👆 💪 ⚙️. + +📥 👤 🔜 🎦 👆 ❔ ⚙️ **🐁** ⏮️ **Uvicorn 👨‍🏭 🛠️**. + +!!! info + 🚥 👆 ⚙️ 📦, 🖼 ⏮️ ☁ ⚖️ Kubernetes, 👤 🔜 💬 👆 🌅 🔃 👈 ⏭ 📃: [FastAPI 📦 - ☁](./docker.md){.internal-link target=_blank}. + + 🎯, 🕐❔ 🏃 🔛 **Kubernetes** 👆 🔜 🎲 **🚫** 💚 ⚙️ 🐁 & ↩️ 🏃 **👁 Uvicorn 🛠️ 📍 📦**, ✋️ 👤 🔜 💬 👆 🔃 ⚫️ ⏪ 👈 📃. + +## 🐁 ⏮️ Uvicorn 👨‍🏭 + +**🐁** ✴️ 🈸 💽 ⚙️ **🇨🇻 🐩**. 👈 ⛓ 👈 🐁 💪 🍦 🈸 💖 🏺 & ✳. 🐁 ⚫️ 🚫 🔗 ⏮️ **FastAPI**, FastAPI ⚙️ 🆕 **🔫 🐩**. + +✋️ 🐁 🐕‍🦺 👷 **🛠️ 👨‍💼** & 🤝 👩‍💻 💬 ⚫️ ❔ 🎯 **👨‍🏭 🛠️ 🎓** ⚙️. ⤴️ 🐁 🔜 ▶️ 1️⃣ ⚖️ 🌖 **👨‍🏭 🛠️** ⚙️ 👈 🎓. + +& **Uvicorn** ✔️ **🐁-🔗 👨‍🏭 🎓**. + +⚙️ 👈 🌀, 🐁 🔜 🚫 **🛠️ 👨‍💼**, 👂 🔛 **⛴** & **📢**. & ⚫️ 🔜 **📶** 📻 👨‍🏭 🛠️ 🏃 **Uvicorn 🎓**. + +& ⤴️ 🐁-🔗 **Uvicorn 👨‍🏭** 🎓 🔜 🈚 🏭 📊 📨 🐁 🔫 🐩 FastAPI ⚙️ ⚫️. + +## ❎ 🐁 & Uvicorn + +
+ +```console +$ pip install "uvicorn[standard]" gunicorn + +---> 100% +``` + +
+ +👈 🔜 ❎ 👯‍♂️ Uvicorn ⏮️ `standard` ➕ 📦 (🤚 ↕ 🎭) & 🐁. + +## 🏃 🐁 ⏮️ Uvicorn 👨‍🏭 + +⤴️ 👆 💪 🏃 🐁 ⏮️: + +
+ +```console +$ gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 + +[19499] [INFO] Starting gunicorn 20.1.0 +[19499] [INFO] Listening at: http://0.0.0.0:80 (19499) +[19499] [INFO] Using worker: uvicorn.workers.UvicornWorker +[19511] [INFO] Booting worker with pid: 19511 +[19513] [INFO] Booting worker with pid: 19513 +[19514] [INFO] Booting worker with pid: 19514 +[19515] [INFO] Booting worker with pid: 19515 +[19511] [INFO] Started server process [19511] +[19511] [INFO] Waiting for application startup. +[19511] [INFO] Application startup complete. +[19513] [INFO] Started server process [19513] +[19513] [INFO] Waiting for application startup. +[19513] [INFO] Application startup complete. +[19514] [INFO] Started server process [19514] +[19514] [INFO] Waiting for application startup. +[19514] [INFO] Application startup complete. +[19515] [INFO] Started server process [19515] +[19515] [INFO] Waiting for application startup. +[19515] [INFO] Application startup complete. +``` + +
+ +➡️ 👀 ⚫️❔ 🔠 👈 🎛 ⛓: + +* `main:app`: 👉 🎏 ❕ ⚙️ Uvicorn, `main` ⛓ 🐍 🕹 📛 "`main`",, 📁 `main.py`. & `app` 📛 🔢 👈 **FastAPI** 🈸. + * 👆 💪 🌈 👈 `main:app` 🌓 🐍 `import` 📄 💖: + + ```Python + from main import app + ``` + + * , ❤ `main:app` 🔜 🌓 🐍 `import` 🍕 `from main import app`. +* `--workers`: 🔢 👨‍🏭 🛠️ ⚙️, 🔠 🔜 🏃 Uvicorn 👨‍🏭, 👉 💼, 4️⃣ 👨‍🏭. +* `--worker-class`: 🐁-🔗 👨‍🏭 🎓 ⚙️ 👨‍🏭 🛠️. + * 📥 👥 🚶‍♀️ 🎓 👈 🐁 💪 🗄 & ⚙️ ⏮️: + + ```Python + import uvicorn.workers.UvicornWorker + ``` + +* `--bind`: 👉 💬 🐁 📢 & ⛴ 👂, ⚙️ ❤ (`:`) 🎏 📢 & ⛴. + * 🚥 👆 🏃‍♂ Uvicorn 🔗, ↩️ `--bind 0.0.0.0:80` (🐁 🎛) 👆 🔜 ⚙️ `--host 0.0.0.0` & `--port 80`. + +🔢, 👆 💪 👀 👈 ⚫️ 🎦 **🕹** (🛠️ 🆔) 🔠 🛠️ (⚫️ 🔢). + +👆 💪 👀 👈: + +* 🐁 **🛠️ 👨‍💼** ▶️ ⏮️ 🕹 `19499` (👆 💼 ⚫️ 🔜 🎏 🔢). +* ⤴️ ⚫️ ▶️ `Listening at: http://0.0.0.0:80`. +* ⤴️ ⚫️ 🔍 👈 ⚫️ ✔️ ⚙️ 👨‍🏭 🎓 `uvicorn.workers.UvicornWorker`. +* & ⤴️ ⚫️ ▶️ **4️⃣ 👨‍🏭**, 🔠 ⏮️ 🚮 👍 🕹: `19511`, `19513`, `19514`, & `19515`. + +🐁 🔜 ✊ 💅 🛠️ **☠️ 🛠️** & **🔁** 🆕 🕐 🚥 💚 🚧 🔢 👨‍🏭. 👈 ℹ 🍕 ⏮️ **⏏** 🔧 ⚪️➡️ 📇 🔛. + +👐, 👆 🔜 🎲 💚 ✔️ 🕳 🏞 ⚒ 💭 **⏏ 🐁** 🚥 💪, & **🏃 ⚫️ 🔛 🕴**, ♒️. + +## Uvicorn ⏮️ 👨‍🏭 + +Uvicorn ✔️ 🎛 ▶️ & 🏃 📚 **👨‍🏭 🛠️**. + +👐, 🔜, Uvicorn 🛠️ 🚚 👨‍🏭 🛠️ 🌅 📉 🌘 🐁. , 🚥 👆 💚 ✔️ 🛠️ 👨‍💼 👉 🎚 (🐍 🎚), ⤴️ ⚫️ 💪 👍 🔄 ⏮️ 🐁 🛠️ 👨‍💼. + +🙆 💼, 👆 🔜 🏃 ⚫️ 💖 👉: + +
+ +```console +$ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 +INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit) +INFO: Started parent process [27365] +INFO: Started server process [27368] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27369] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27370] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27367] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +🕴 🆕 🎛 📥 `--workers` 💬 Uvicorn ▶️ 4️⃣ 👨‍🏭 🛠️. + +👆 💪 👀 👈 ⚫️ 🎦 **🕹** 🔠 🛠️, `27365` 👪 🛠️ (👉 **🛠️ 👨‍💼**) & 1️⃣ 🔠 👨‍🏭 🛠️: `27368`, `27369`, `27370`, & `27367`. + +## 🛠️ 🔧 + +📥 👆 👀 ❔ ⚙️ **🐁** (⚖️ Uvicorn) 🛠️ **Uvicorn 👨‍🏭 🛠️** **🔁** 🛠️ 🈸, ✊ 📈 **💗 🐚** 💽, & 💪 🍦 **🌅 📨**. + +⚪️➡️ 📇 🛠️ 🔧 ⚪️➡️ 🔛, ⚙️ 👨‍🏭 🔜 ✴️ ℹ ⏮️ **🧬** 🍕, & 🐥 🍖 ⏮️ **⏏**, ✋️ 👆 💪 ✊ 💅 🎏: + +* **💂‍♂ - 🇺🇸🔍** +* **🏃‍♂ 🔛 🕴** +* ***⏏*** +* 🧬 (🔢 🛠️ 🏃) +* **💾** +* **⏮️ 🔁 ⏭ ▶️** + +## 📦 & ☁ + +⏭ 📃 🔃 [FastAPI 📦 - ☁](./docker.md){.internal-link target=_blank} 👤 🔜 💬 🎛 👆 💪 ⚙️ 🍵 🎏 **🛠️ 🔧**. + +👤 🔜 🎦 👆 **🛂 ☁ 🖼** 👈 🔌 **🐁 ⏮️ Uvicorn 👨‍🏭** & 🔢 📳 👈 💪 ⚠ 🙅 💼. + +📤 👤 🔜 🎦 👆 ❔ **🏗 👆 👍 🖼 ⚪️➡️ 🖌** 🏃 👁 Uvicorn 🛠️ (🍵 🐁). ⚫️ 🙅 🛠️ & 🎲 ⚫️❔ 👆 🔜 💚 🕐❔ ⚙️ 📎 📦 🧾 ⚙️ 💖 **Kubernetes**. + +## 🌃 + +👆 💪 ⚙️ **🐁** (⚖️ Uvicorn) 🛠️ 👨‍💼 ⏮️ Uvicorn 👨‍🏭 ✊ 📈 **👁-🐚 💽**, 🏃 **💗 🛠️ 🔗**. + +👆 💪 ⚙️ 👉 🧰 & 💭 🚥 👆 ⚒ 🆙 **👆 👍 🛠️ ⚙️** ⏪ ✊ 💅 🎏 🛠️ 🔧 👆. + +✅ 👅 ⏭ 📃 💡 🔃 **FastAPI** ⏮️ 📦 (✅ ☁ & Kubernetes). 👆 🔜 👀 👈 👈 🧰 ✔️ 🙅 🌌 ❎ 🎏 **🛠️ 🔧** 👍. 👶 diff --git a/docs/em/docs/deployment/versions.md b/docs/em/docs/deployment/versions.md new file mode 100644 index 0000000000..8bfdf97318 --- /dev/null +++ b/docs/em/docs/deployment/versions.md @@ -0,0 +1,87 @@ +# 🔃 FastAPI ⏬ + +**FastAPI** ⏪ ➖ ⚙️ 🏭 📚 🈸 & ⚙️. & 💯 💰 🚧 1️⃣0️⃣0️⃣ 💯. ✋️ 🚮 🛠️ 🚚 🔜. + +🆕 ⚒ 🚮 🛎, 🐛 🔧 🛎, & 📟 🔁 📉. + +👈 ⚫️❔ ⏮️ ⏬ `0.x.x`, 👉 🎨 👈 🔠 ⏬ 💪 ⚠ ✔️ 💔 🔀. 👉 ⏩ ⚛ 🛠️ 🏛. + +👆 💪 ✍ 🏭 🈸 ⏮️ **FastAPI** ▶️️ 🔜 (& 👆 ✔️ 🎲 🔨 ⚫️ 🕰), 👆 ✔️ ⚒ 💭 👈 👆 ⚙️ ⏬ 👈 👷 ☑ ⏮️ 🎂 👆 📟. + +## 📌 👆 `fastapi` ⏬ + +🥇 👜 👆 🔜 "📌" ⏬ **FastAPI** 👆 ⚙️ 🎯 📰 ⏬ 👈 👆 💭 👷 ☑ 👆 🈸. + +🖼, ➡️ 💬 👆 ⚙️ ⏬ `0.45.0` 👆 📱. + +🚥 👆 ⚙️ `requirements.txt` 📁 👆 💪 ✔ ⏬ ⏮️: + +```txt +fastapi==0.45.0 +``` + +👈 🔜 ⛓ 👈 👆 🔜 ⚙️ ⚫️❔ ⏬ `0.45.0`. + +⚖️ 👆 💪 📌 ⚫️ ⏮️: + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +👈 🔜 ⛓ 👈 👆 🔜 ⚙️ ⏬ `0.45.0` ⚖️ 🔛, ✋️ 🌘 🌘 `0.46.0`, 🖼, ⏬ `0.45.2` 🔜 🚫. + +🚥 👆 ⚙️ 🙆 🎏 🧰 🛠️ 👆 👷‍♂, 💖 🎶, Pipenv, ⚖️ 🎏, 👫 🌐 ✔️ 🌌 👈 👆 💪 ⚙️ 🔬 🎯 ⏬ 👆 📦. + +## 💪 ⏬ + +👆 💪 👀 💪 ⏬ (✅ ✅ ⚫️❔ ⏮️ 📰) [🚀 🗒](../release-notes.md){.internal-link target=_blank}. + +## 🔃 ⏬ + +📄 ⚛ 🛠️ 🏛, 🙆 ⏬ 🔛 `1.0.0` 💪 ⚠ 🚮 💔 🔀. + +FastAPI ⏩ 🏛 👈 🙆 "🐛" ⏬ 🔀 🐛 🔧 & 🚫-💔 🔀. + +!!! tip + "🐛" 🏁 🔢, 🖼, `0.2.3`, 🐛 ⏬ `3`. + +, 👆 🔜 💪 📌 ⏬ 💖: + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +💔 🔀 & 🆕 ⚒ 🚮 "🇺🇲" ⏬. + +!!! tip + "🇺🇲" 🔢 🖕, 🖼, `0.2.3`, 🇺🇲 ⏬ `2`. + +## ♻ FastAPI ⏬ + +👆 🔜 🚮 💯 👆 📱. + +⏮️ **FastAPI** ⚫️ 📶 ⏩ (👏 💃), ✅ 🩺: [🔬](../tutorial/testing.md){.internal-link target=_blank} + +⏮️ 👆 ✔️ 💯, ⤴️ 👆 💪 ♻ **FastAPI** ⏬ 🌖 ⏮️ 1️⃣, & ⚒ 💭 👈 🌐 👆 📟 👷 ☑ 🏃 👆 💯. + +🚥 🌐 👷, ⚖️ ⏮️ 👆 ⚒ 💪 🔀, & 🌐 👆 💯 🚶‍♀️, ⤴️ 👆 💪 📌 👆 `fastapi` 👈 🆕 ⏮️ ⏬. + +## 🔃 💃 + +👆 🚫🔜 🚫 📌 ⏬ `starlette`. + +🎏 ⏬ **FastAPI** 🔜 ⚙️ 🎯 🆕 ⏬ 💃. + +, 👆 💪 ➡️ **FastAPI** ⚙️ ☑ 💃 ⏬. + +## 🔃 Pydantic + +Pydantic 🔌 💯 **FastAPI** ⏮️ 🚮 👍 💯, 🆕 ⏬ Pydantic (🔛 `1.0.0`) 🕧 🔗 ⏮️ FastAPI. + +👆 💪 📌 Pydantic 🙆 ⏬ 🔛 `1.0.0` 👈 👷 👆 & 🔛 `2.0.0`. + +🖼: + +```txt +pydantic>=1.2.0,<2.0.0 +``` diff --git a/docs/em/docs/external-links.md b/docs/em/docs/external-links.md new file mode 100644 index 0000000000..5ba668bfac --- /dev/null +++ b/docs/em/docs/external-links.md @@ -0,0 +1,35 @@ +# 🔢 🔗 & 📄 + +**FastAPI** ✔️ 👑 👪 🕧 💗. + +📤 📚 🏤, 📄, 🧰, & 🏗, 🔗 **FastAPI**. + +📥 ❌ 📇 👫. + +!!! tip + 🚥 👆 ✔️ 📄, 🏗, 🧰, ⚖️ 🕳 🔗 **FastAPI** 👈 🚫 📇 📥, ✍ 🚲 📨 ❎ ⚫️. + +## 📄 + +{% for section_name, section_content in external_links.items() %} + +## {{ section_name }} + +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* {{ item.title }} by {{ item.author }}. + +{% endfor %} +{% endfor %} +{% endfor %} + +## 🏗 + +⏪ 📂 🏗 ⏮️ ❔ `fastapi`: + +
+
diff --git a/docs/em/docs/fastapi-people.md b/docs/em/docs/fastapi-people.md new file mode 100644 index 0000000000..dc94d80da7 --- /dev/null +++ b/docs/em/docs/fastapi-people.md @@ -0,0 +1,178 @@ +# FastAPI 👫👫 + +FastAPI ✔️ 🎆 👪 👈 🙋 👫👫 ⚪️➡️ 🌐 🖥. + +## 👼 - 🐛 + +🙋 ❗ 👶 + +👉 👤: + +{% if people %} +
+{% for user in people.maintainers %} + +
@{{ user.login }}
❔: {{ user.answers }}
🚲 📨: {{ user.prs }}
+{% endfor %} + +
+{% endif %} + +👤 👼 & 🐛 **FastAPI**. 👆 💪 ✍ 🌅 🔃 👈 [ℹ FastAPI - 🤚 ℹ - 🔗 ⏮️ 📕](help-fastapi.md#connect-with-the-author){.internal-link target=_blank}. + +...✋️ 📥 👤 💚 🎦 👆 👪. + +--- + +**FastAPI** 📨 📚 🐕‍🦺 ⚪️➡️ 👪. & 👤 💚 🎦 👫 💰. + +👫 👫👫 👈: + +* [ℹ 🎏 ⏮️ ❔ 📂](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank}. +* [✍ 🚲 📨](help-fastapi.md#create-a-pull-request){.internal-link target=_blank}. +* 📄 🚲 📨, [✴️ ⚠ ✍](contributing.md#translations){.internal-link target=_blank}. + +👏 👫. 👶 👶 + +## 🌅 🦁 👩‍💻 🏁 🗓️ + +👫 👩‍💻 👈 ✔️ [🤝 🎏 🏆 ⏮️ ❔ 📂](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} ⏮️ 🏁 🗓️. 👶 + +{% if people %} +
+{% for user in people.last_month_active %} + +
@{{ user.login }}
❔ 📨: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## 🕴 + +📥 **FastAPI 🕴**. 👶 + +👫 👩‍💻 👈 ✔️ [ℹ 🎏 🏆 ⏮️ ❔ 📂](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} 🔘 *🌐 🕰*. + +👫 ✔️ 🎦 🕴 🤝 📚 🎏. 👶 + +{% if people %} +
+{% for user in people.experts %} + +
@{{ user.login }}
❔ 📨: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## 🔝 👨‍🔬 + +📥 **🔝 👨‍🔬**. 👶 + +👉 👩‍💻 ✔️ [✍ 🏆 🚲 📨](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} 👈 ✔️ *🔗*. + +👫 ✔️ 📉 ℹ 📟, 🧾, ✍, ♒️. 👶 + +{% if people %} +
+{% for user in people.top_contributors %} + +
@{{ user.login }}
🚲 📨: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +📤 📚 🎏 👨‍🔬 (🌅 🌘 💯), 👆 💪 👀 👫 🌐 FastAPI 📂 👨‍🔬 📃. 👶 + +## 🔝 👨‍🔬 + +👫 👩‍💻 **🔝 👨‍🔬**. 👶 👶 + +### 📄 ✍ + +👤 🕴 💬 👩‍❤‍👨 🇪🇸 (& 🚫 📶 👍 👶). , 👨‍🔬 🕐 👈 ✔️ [**🏋️ ✔ ✍**](contributing.md#translations){.internal-link target=_blank} 🧾. 🍵 👫, 📤 🚫🔜 🧾 📚 🎏 🇪🇸. + +--- + +**🔝 👨‍🔬** 👶 👶 ✔️ 📄 🏆 🚲 📨 ⚪️➡️ 🎏, 🚚 🔆 📟, 🧾, & ✴️, **✍**. + +{% if people %} +
+{% for user in people.top_reviewers %} + +
@{{ user.login }}
📄: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## 💰 + +👫 **💰**. 👶 + +👫 🔗 👇 👷 ⏮️ **FastAPI** (& 🎏), ✴️ 🔘 📂 💰. + +{% if sponsors %} + +{% if sponsors.gold %} + +### 🌟 💰 + +{% for sponsor in sponsors.gold -%} + +{% endfor %} +{% endif %} + +{% if sponsors.silver %} + +### 🥇1st 💰 + +{% for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + +{% if sponsors.bronze %} + +### 🥈2nd 💰 + +{% for sponsor in sponsors.bronze -%} + +{% endfor %} +{% endif %} + +{% endif %} + +### 🎯 💰 + +{% if github_sponsors %} +{% for group in github_sponsors.sponsors %} + +
+ +{% for user in group %} +{% if user.login not in sponsors_badge.logins %} + + + +{% endif %} +{% endfor %} + +
+ +{% endfor %} +{% endif %} + +## 🔃 📊 - 📡 ℹ + +👑 🎯 👉 📃 🎦 🎯 👪 ℹ 🎏. + +✴️ ✅ 🎯 👈 🛎 🌘 ⭐, & 📚 💼 🌅 😩, 💖 🤝 🎏 ⏮️ ❔ & ⚖ 🚲 📨 ⏮️ ✍. + +💽 ⚖ 🔠 🗓️, 👆 💪 ✍ ℹ 📟 📥. + +📥 👤 🎦 💰 ⚪️➡️ 💰. + +👤 🏦 ▶️️ ℹ 📊, 📄, ⚡, ♒️ (💼 🤷). diff --git a/docs/em/docs/features.md b/docs/em/docs/features.md new file mode 100644 index 0000000000..19193da075 --- /dev/null +++ b/docs/em/docs/features.md @@ -0,0 +1,200 @@ +# ⚒ + +## FastAPI ⚒ + +**FastAPI** 🤝 👆 📄: + +### ⚓️ 🔛 📂 🐩 + +* 🗄 🛠️ 🏗, ✅ 📄 🛠️, 🔢, 💪 📨, 💂‍♂, ♒️. +* 🏧 📊 🏷 🧾 ⏮️ 🎻 🔗 (🗄 ⚫️ 🧢 🔛 🎻 🔗). +* 🔧 🤭 👫 🐩, ⏮️ 😔 🔬. ↩️ 👎 🧽 🔛 🔝. +* 👉 ✔ ⚙️ 🏧 **👩‍💻 📟 ⚡** 📚 🇪🇸. + +### 🏧 🩺 + +🎓 🛠️ 🧾 & 🔬 🕸 👩‍💻 🔢. 🛠️ ⚓️ 🔛 🗄, 📤 💗 🎛, 2️⃣ 🔌 🔢. + +* 🦁 🎚, ⏮️ 🎓 🔬, 🤙 & 💯 👆 🛠️ 🔗 ⚪️➡️ 🖥. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* 🎛 🛠️ 🧾 ⏮️ 📄. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### 🏛 🐍 + +⚫️ 🌐 ⚓️ 🔛 🐩 **🐍 3️⃣.6️⃣ 🆎** 📄 (👏 Pydantic). 🙅‍♂ 🆕 ❕ 💡. 🐩 🏛 🐍. + +🚥 👆 💪 2️⃣ ⏲ ↗️ ❔ ⚙️ 🐍 🆎 (🚥 👆 🚫 ⚙️ FastAPI), ✅ 📏 🔰: [🐍 🆎](python-types.md){.internal-link target=_blank}. + +👆 ✍ 🐩 🐍 ⏮️ 🆎: + +```Python +from datetime import date + +from pydantic import BaseModel + +# Declare a variable as a str +# and get editor support inside the function +def main(user_id: str): + return user_id + + +# A Pydantic model +class User(BaseModel): + id: int + name: str + joined: date +``` + +👈 💪 ⤴️ ⚙️ 💖: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +!!! info + `**second_user_data` ⛓: + + 🚶‍♀️ 🔑 & 💲 `second_user_data` #️⃣ 🔗 🔑-💲 ❌, 🌓: `User(id=4, name="Mary", joined="2018-11-30")` + +### 👨‍🎨 🐕‍🦺 + +🌐 🛠️ 🏗 ⏩ & 🏋️ ⚙️, 🌐 🚫 💯 🔛 💗 👨‍🎨 ⏭ ▶️ 🛠️, 🚚 🏆 🛠️ 💡. + +🏁 🐍 👩‍💻 🔬 ⚫️ 🆑 👈 🌅 ⚙️ ⚒ "✍". + +🎂 **FastAPI** 🛠️ ⚓️ 😌 👈. ✍ 👷 🌐. + +👆 🔜 🛎 💪 👟 🔙 🩺. + +📥 ❔ 👆 👨‍🎨 💪 ℹ 👆: + +* 🎙 🎙 📟: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* 🗒: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +👆 🔜 🤚 🛠️ 📟 👆 5️⃣📆 🤔 💪 ⏭. 🖼, `price` 🔑 🔘 🎻 💪 (👈 💪 ✔️ 🐦) 👈 👟 ⚪️➡️ 📨. + +🙅‍♂ 🌖 ⌨ ❌ 🔑 📛, 👟 🔙 & ➡ 🖖 🩺, ⚖️ 📜 🆙 & 🔽 🔎 🚥 👆 😒 ⚙️ `username` ⚖️ `user_name`. + +### 📏 + +⚫️ ✔️ 🤔 **🔢** 🌐, ⏮️ 📦 📳 🌐. 🌐 🔢 💪 👌-🎧 ⚫️❔ 👆 💪 & 🔬 🛠️ 👆 💪. + +✋️ 🔢, ⚫️ 🌐 **"👷"**. + +### 🔬 + +* 🔬 🌅 (⚖️ 🌐 ❓) 🐍 **💽 🆎**, 🔌: + * 🎻 🎚 (`dict`). + * 🎻 🎻 (`list`) ⚖ 🏬 🆎. + * 🎻 (`str`) 🏑, 🔬 🕙 & 👟 📐. + * 🔢 (`int`, `float`) ⏮️ 🕙 & 👟 💲, ♒️. + +* 🔬 🌅 😍 🆎, 💖: + * 📛. + * 📧. + * 🆔. + * ...& 🎏. + +🌐 🔬 🍵 👍-🏛 & 🏋️ **Pydantic**. + +### 💂‍♂ & 🤝 + +💂‍♂ & 🤝 🛠️. 🍵 🙆 ⚠ ⏮️ 💽 ⚖️ 📊 🏷. + +🌐 💂‍♂ ⚖ 🔬 🗄, 🔌: + +* 🇺🇸🔍 🔰. +* **Oauth2️⃣** (⏮️ **🥙 🤝**). ✅ 🔰 🔛 [Oauth2️⃣ ⏮️ 🥙](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* 🛠️ 🔑: + * 🎚. + * 🔢 🔢. + * 🍪, ♒️. + +➕ 🌐 💂‍♂ ⚒ ⚪️➡️ 💃 (🔌 **🎉 🍪**). + +🌐 🏗 ♻ 🧰 & 🦲 👈 ⏩ 🛠️ ⏮️ 👆 ⚙️, 📊 🏪, 🔗 & ☁ 💽, ♒️. + +### 🔗 💉 + +FastAPI 🔌 📶 ⏩ ⚙️, ✋️ 📶 🏋️ 🔗 💉 ⚙️. + +* 🔗 💪 ✔️ 🔗, 🏗 🔗 ⚖️ **"📊" 🔗**. +* 🌐 **🔁 🍵** 🛠️. +* 🌐 🔗 💪 🚚 💽 ⚪️➡️ 📨 & **↔ ➡ 🛠️** ⚛ & 🏧 🧾. +* **🏧 🔬** *➡ 🛠️* 🔢 🔬 🔗. +* 🐕‍🦺 🏗 👩‍💻 🤝 ⚙️, **💽 🔗**, ♒️. +* **🙅‍♂ ⚠** ⏮️ 💽, 🕸, ♒️. ✋️ ⏩ 🛠️ ⏮️ 🌐 👫. + +### ♾ "🔌-🔌" + +⚖️ 🎏 🌌, 🙅‍♂ 💪 👫, 🗄 & ⚙️ 📟 👆 💪. + +🙆 🛠️ 🏗 🙅 ⚙️ (⏮️ 🔗) 👈 👆 💪 ✍ "🔌-" 👆 🈸 2️⃣ ⏸ 📟 ⚙️ 🎏 📊 & ❕ ⚙️ 👆 *➡ 🛠️*. + +### 💯 + +* 1️⃣0️⃣0️⃣ 💯 💯 💰. +* 1️⃣0️⃣0️⃣ 💯 🆎 ✍ 📟 🧢. +* ⚙️ 🏭 🈸. + +## 💃 ⚒ + +**FastAPI** 🍕 🔗 ⏮️ (& ⚓️ 🔛) 💃. , 🙆 🌖 💃 📟 👆 ✔️, 🔜 👷. + +`FastAPI` 🤙 🎧-🎓 `Starlette`. , 🚥 👆 ⏪ 💭 ⚖️ ⚙️ 💃, 🌅 🛠️ 🔜 👷 🎏 🌌. + +⏮️ **FastAPI** 👆 🤚 🌐 **💃**'Ⓜ ⚒ (FastAPI 💃 🔛 💊): + +* 🤙 🎆 🎭. ⚫️ 1️⃣ ⏩ 🐍 🛠️ 💪, 🔛 🇷🇪 ⏮️ **✳** & **🚶**. +* ** *️⃣ ** 🐕‍🦺. +* -🛠️ 🖥 📋. +* 🕴 & 🤫 🎉. +* 💯 👩‍💻 🏗 🔛 🇸🇲. +* **⚜**, 🗜, 🎻 📁, 🎏 📨. +* **🎉 & 🍪** 🐕‍🦺. +* 1️⃣0️⃣0️⃣ 💯 💯 💰. +* 1️⃣0️⃣0️⃣ 💯 🆎 ✍ ✍. + +## Pydantic ⚒ + +**FastAPI** 🍕 🔗 ⏮️ (& ⚓️ 🔛) Pydantic. , 🙆 🌖 Pydantic 📟 👆 ✔️, 🔜 👷. + +✅ 🔢 🗃 ⚓️ 🔛 Pydantic, 🐜Ⓜ, 🏭Ⓜ 💽. + +👉 ⛓ 👈 📚 💼 👆 💪 🚶‍♀️ 🎏 🎚 👆 🤚 ⚪️➡️ 📨 **🔗 💽**, 🌐 ✔ 🔁. + +🎏 ✔ 🎏 🌌 🤭, 📚 💼 👆 💪 🚶‍♀️ 🎚 👆 🤚 ⚪️➡️ 💽 **🔗 👩‍💻**. + +⏮️ **FastAPI** 👆 🤚 🌐 **Pydantic**'Ⓜ ⚒ (FastAPI ⚓️ 🔛 Pydantic 🌐 💽 🚚): + +* **🙅‍♂ 🔠**: + * 🙅‍♂ 🆕 🔗 🔑 ◾-🇪🇸 💡. + * 🚥 👆 💭 🐍 🆎 👆 💭 ❔ ⚙️ Pydantic. +* 🤾 🎆 ⏮️ 👆 **💾/🧶/🧠**: + * ↩️ Pydantic 📊 📊 👐 🎓 👆 🔬; 🚘-🛠️, 🧽, ✍ & 👆 🤔 🔜 🌐 👷 ☑ ⏮️ 👆 ✔ 💽. +* **⏩**: + * 📇 Pydantic ⏩ 🌘 🌐 🎏 💯 🗃. +* ✔ **🏗 📊**: + * ⚙️ 🔗 Pydantic 🏷, 🐍 `typing`'Ⓜ `List` & `Dict`, ♒️. + * & 💳 ✔ 🏗 💽 🔗 🎯 & 💪 🔬, ✅ & 📄 🎻 🔗. + * 👆 💪 ✔️ 🙇 **🐦 🎻** 🎚 & ✔️ 👫 🌐 ✔ & ✍. +* **🏧**: + * Pydantic ✔ 🛃 📊 🆎 🔬 ⚖️ 👆 💪 ↔ 🔬 ⏮️ 👩‍🔬 🔛 🏷 🎀 ⏮️ 💳 👨‍🎨. +* 1️⃣0️⃣0️⃣ 💯 💯 💰. diff --git a/docs/em/docs/help-fastapi.md b/docs/em/docs/help-fastapi.md new file mode 100644 index 0000000000..b998ade42b --- /dev/null +++ b/docs/em/docs/help-fastapi.md @@ -0,0 +1,263 @@ +# ℹ FastAPI - 🤚 ℹ + +👆 💖 **FastAPI**❓ + +🔜 👆 💖 ℹ FastAPI, 🎏 👩‍💻, & 📕 ❓ + +⚖️ 🔜 👆 💖 🤚 ℹ ⏮️ **FastAPI**❓ + +📤 📶 🙅 🌌 ℹ (📚 🔌 1️⃣ ⚖️ 2️⃣ 🖊). + +& 📤 📚 🌌 🤚 ℹ 💁‍♂️. + +## 👱📔 📰 + +👆 💪 👱📔 (🐌) [**FastAPI & 👨‍👧‍👦** 📰](/newsletter/){.internal-link target=_blank} 🚧 ℹ 🔃: + +* 📰 🔃 FastAPI & 👨‍👧‍👦 👶 +* 🦮 👶 +* ⚒ 👶 +* 💔 🔀 👶 +* 💁‍♂ & 🎱 👶 + +## ⏩ FastAPI 🔛 👱📔 + +⏩ 🐶 Fastapi 🔛 **👱📔** 🤚 📰 📰 🔃 **FastAPI**. 👶 + +## ✴ **FastAPI** 📂 + +👆 💪 "✴" FastAPI 📂 (🖊 ✴ 🔼 🔝 ▶️️): https://github.com/tiangolo/fastapi. 👶 👶 + +❎ ✴, 🎏 👩‍💻 🔜 💪 🔎 ⚫️ 🌅 💪 & 👀 👈 ⚫️ ✔️ ⏪ ⚠ 🎏. + +## ⌚ 📂 🗃 🚀 + +👆 💪 "⌚" FastAPI 📂 (🖊 "⌚" 🔼 🔝 ▶️️): https://github.com/tiangolo/fastapi. 👶 + +📤 👆 💪 🖊 "🚀 🕴". + +🔨 ⚫️, 👆 🔜 📨 📨 (👆 📧) 🕐❔ 📤 🆕 🚀 (🆕 ⏬) **FastAPI** ⏮️ 🐛 🔧 & 🆕 ⚒. + +## 🔗 ⏮️ 📕 + +👆 💪 🔗 ⏮️ 👤 (🇹🇦 🇩🇬 / `tiangolo`), 📕. + +👆 💪: + +* ⏩ 👤 🔛 **📂**. + * 👀 🎏 📂 ℹ 🏗 👤 ✔️ ✍ 👈 💪 ℹ 👆. + * ⏩ 👤 👀 🕐❔ 👤 ✍ 🆕 📂 ℹ 🏗. +* ⏩ 👤 🔛 **👱📔** ⚖️ . + * 💬 👤 ❔ 👆 ⚙️ FastAPI (👤 💌 👂 👈). + * 👂 🕐❔ 👤 ⚒ 🎉 ⚖️ 🚀 🆕 🧰. + * 👆 💪 ⏩ 🐶 Fastapi 🔛 👱📔 (🎏 🏧). +* 🔗 ⏮️ 👤 🔛 **👱📔**. + * 👂 🕐❔ 👤 ⚒ 🎉 ⚖️ 🚀 🆕 🧰 (👐 👤 ⚙️ 👱📔 🌖 🛎 🤷 ♂). +* ✍ ⚫️❔ 👤 ✍ (⚖️ ⏩ 👤) 🔛 **🇸🇲.** ⚖️ **🔉**. + * ✍ 🎏 💭, 📄, & ✍ 🔃 🧰 👤 ✔️ ✍. + * ⏩ 👤 ✍ 🕐❔ 👤 ✍ 🕳 🆕. + +## 👱📔 🔃 **FastAPI** + +👱📔 🔃 **FastAPI** & ➡️ 👤 & 🎏 💭 ⚫️❔ 👆 💖 ⚫️. 👶 + +👤 💌 👂 🔃 ❔ **FastAPI** 💆‍♂ ⚙️, ⚫️❔ 👆 ✔️ 💖 ⚫️, ❔ 🏗/🏢 👆 ⚙️ ⚫️, ♒️. + +## 🗳 FastAPI + +* 🗳 **FastAPI** 📐. +* 🗳 **FastAPI** 📱. +* 💬 👆 ⚙️ **FastAPI** 🔛 ℹ. + +## ℹ 🎏 ⏮️ ❔ 📂 + +👆 💪 🔄 & ℹ 🎏 ⏮️ 👫 ❔: + +* 📂 💬 +* 📂 ❔ + +📚 💼 👆 5️⃣📆 ⏪ 💭 ❔ 📚 ❔. 👶 + +🚥 👆 🤝 📚 👫👫 ⏮️ 👫 ❔, 👆 🔜 ▶️️ 🛂 [FastAPI 🕴](fastapi-people.md#experts){.internal-link target=_blank}. 👶 + +💭, 🏆 ⚠ ☝: 🔄 😇. 👫👫 👟 ⏮️ 👫 😩 & 📚 💼 🚫 💭 🏆 🌌, ✋️ 🔄 🏆 👆 💪 😇. 👶 + +💭 **FastAPI** 👪 😇 & 👍. 🎏 🕰, 🚫 🚫 🎭 ⚖️ 😛 🎭 ⤵ 🎏. 👥 ✔️ ✊ 💅 🔠 🎏. + +--- + +📥 ❔ ℹ 🎏 ⏮️ ❔ (💬 ⚖️ ❔): + +### 🤔 ❔ + +* ✅ 🚥 👆 💪 🤔 ⚫️❔ **🎯** & ⚙️ 💼 👨‍💼 💬. + +* ⤴️ ✅ 🚥 ❔ (⭕ 👪 ❔) **🆑**. + +* 📚 💼 ❔ 💭 🔃 👽 ⚗ ⚪️➡️ 👩‍💻, ✋️ 📤 💪 **👍** 1️⃣. 🚥 👆 💪 🤔 ⚠ & ⚙️ 💼 👍, 👆 💪 💪 🤔 👍 **🎛 ⚗**. + +* 🚥 👆 💪 🚫 🤔 ❔, 💭 🌖 **ℹ**. + +### 🔬 ⚠ + +🌅 💼 & 🏆 ❔ 📤 🕳 🔗 👨‍💼 **⏮️ 📟**. + +📚 💼 👫 🔜 🕴 📁 🧬 📟, ✋️ 👈 🚫 🥃 **🔬 ⚠**. + +* 👆 💪 💭 👫 🚚 ⭐, 🔬, 🖼, 👈 👆 💪 **📁-📋** & 🏃 🌐 👀 🎏 ❌ ⚖️ 🎭 👫 👀, ⚖️ 🤔 👫 ⚙️ 💼 👍. + +* 🚥 👆 😟 💁‍♂️ 👍, 👆 💪 🔄 **✍ 🖼** 💖 👈 👆, 🧢 🔛 📛 ⚠. ✔️ 🤯 👈 👉 💪 ✊ 📚 🕰 & ⚫️ 💪 👻 💭 👫 ✍ ⚠ 🥇. + +### 🤔 ⚗ + +* ⏮️ 💆‍♂ 💪 🤔 ❔, 👆 💪 🤝 👫 💪 **❔**. + +* 📚 💼, ⚫️ 👍 🤔 👫 **📈 ⚠ ⚖️ ⚙️ 💼**, ↩️ 📤 5️⃣📆 👍 🌌 ❎ ⚫️ 🌘 ⚫️❔ 👫 🔄. + +### 💭 🔐 + +🚥 👫 📨, 📤 ↕ 🤞 👆 🔜 ✔️ ❎ 👫 ⚠, ㊗, **👆 💂**❗ 🦸 + +* 🔜, 🚥 👈 ❎ 👫 ⚠, 👆 💪 💭 👫: + + * 📂 💬: ™ 🏤 **❔**. + * 📂 ❔: **🔐** ❔**. + +## ⌚ 📂 🗃 + +👆 💪 "⌚" FastAPI 📂 (🖊 "⌚" 🔼 🔝 ▶️️): https://github.com/tiangolo/fastapi. 👶 + +🚥 👆 🖊 "👀" ↩️ "🚀 🕴" 👆 🔜 📨 📨 🕐❔ 👱 ✍ 🆕 ❔ ⚖️ ❔. 👆 💪 ✔ 👈 👆 🕴 💚 🚨 🔃 🆕 ❔, ⚖️ 💬, ⚖️ 🎸, ♒️. + +⤴️ 👆 💪 🔄 & ℹ 👫 ❎ 👈 ❔. + +## 💭 ❔ + +👆 💪 ✍ 🆕 ❔ 📂 🗃, 🖼: + +* 💭 **❔** ⚖️ 💭 🔃 **⚠**. +* 🤔 🆕 **⚒**. + +**🗒**: 🚥 👆 ⚫️, ⤴️ 👤 🔜 💭 👆 ℹ 🎏. 👶 + +## 📄 🚲 📨 + +👆 💪 ℹ 👤 📄 🚲 📨 ⚪️➡️ 🎏. + +🔄, 🙏 🔄 👆 🏆 😇. 👶 + +--- + +📥 ⚫️❔ ✔️ 🤯 & ❔ 📄 🚲 📨: + +### 🤔 ⚠ + +* 🥇, ⚒ 💭 👆 **🤔 ⚠** 👈 🚲 📨 🔄 ❎. ⚫️ 💪 ✔️ 📏 💬 📂 💬 ⚖️ ❔. + +* 📤 👍 🤞 👈 🚲 📨 🚫 🤙 💪 ↩️ ⚠ 💪 ❎ **🎏 🌌**. ⤴️ 👆 💪 🤔 ⚖️ 💭 🔃 👈. + +### 🚫 😟 🔃 👗 + +* 🚫 😟 💁‍♂️ 🌅 🔃 👜 💖 💕 📧 👗, 👤 🔜 🥬 & 🔗 🛃 💕 ❎. + +* 🚫 😟 🔃 👗 🚫, 📤 ⏪ 🏧 🧰 ✅ 👈. + +& 🚥 📤 🙆 🎏 👗 ⚖️ ⚖ 💪, 👤 🔜 💭 🔗 👈, ⚖️ 👤 🔜 🚮 💕 🔛 🔝 ⏮️ 💪 🔀. + +### ✅ 📟 + +* ✅ & ✍ 📟, 👀 🚥 ⚫️ ⚒ 🔑, **🏃 ⚫️ 🌐** & 👀 🚥 ⚫️ 🤙 ❎ ⚠. + +* ⤴️ **🏤** 💬 👈 👆 👈, 👈 ❔ 👤 🔜 💭 👆 🤙 ✅ ⚫️. + +!!! info + 👐, 👤 💪 🚫 🎯 💙 🎸 👈 ✔️ 📚 ✔. + + 📚 🕰 ⚫️ ✔️ 🔨 👈 📤 🎸 ⏮️ 3️⃣, 5️⃣ ⚖️ 🌅 ✔, 🎲 ↩️ 📛 😌, ✋️ 🕐❔ 👤 ✅ 🎸, 👫 🤙 💔, ✔️ 🐛, ⚖️ 🚫 ❎ ⚠ 👫 🛄 ❎. 👶 + + , ⚫️ 🤙 ⚠ 👈 👆 🤙 ✍ & 🏃 📟, & ➡️ 👤 💭 🏤 👈 👆. 👶 + +* 🚥 🇵🇷 💪 📉 🌌, 👆 💪 💭 👈, ✋️ 📤 🙅‍♂ 💪 💁‍♂️ 😟, 📤 5️⃣📆 📚 🤔 ☝ 🎑 (& 👤 🔜 ✔️ 👇 👍 👍 👶), ⚫️ 👻 🚥 👆 💪 🎯 🔛 ⚛ 👜. + +### 💯 + +* ℹ 👤 ✅ 👈 🇵🇷 ✔️ **💯**. + +* ✅ 👈 💯 **❌** ⏭ 🇵🇷. 👶 + +* ⤴️ ✅ 👈 💯 **🚶‍♀️** ⏮️ 🇵🇷. 👶 + +* 📚 🎸 🚫 ✔️ 💯, 👆 💪 **🎗** 👫 🚮 💯, ⚖️ 👆 💪 **🤔** 💯 👆. 👈 1️⃣ 👜 👈 🍴 🌅 🕰 & 👆 💪 ℹ 📚 ⏮️ 👈. + +* ⤴️ 🏤 ⚫️❔ 👆 🔄, 👈 🌌 👤 🔜 💭 👈 👆 ✅ ⚫️. 👶 + +## ✍ 🚲 📨 + +👆 💪 [📉](contributing.md){.internal-link target=_blank} ℹ 📟 ⏮️ 🚲 📨, 🖼: + +* 🔧 🤭 👆 🔎 🔛 🧾. +* 💰 📄, 📹, ⚖️ 📻 👆 ✍ ⚖️ 🔎 🔃 FastAPI ✍ 👉 📁. + * ⚒ 💭 👆 🚮 👆 🔗 ▶️ 🔗 📄. +* ℹ [💬 🧾](contributing.md#translations){.internal-link target=_blank} 👆 🇪🇸. + * 👆 💪 ℹ 📄 ✍ ✍ 🎏. +* 🛠️ 🆕 🧾 📄. +* 🔧 ♻ ❔/🐛. + * ⚒ 💭 🚮 💯. +* 🚮 🆕 ⚒. + * ⚒ 💭 🚮 💯. + * ⚒ 💭 🚮 🧾 🚥 ⚫️ 🔗. + +## ℹ 🚧 FastAPI + +ℹ 👤 🚧 **FastAPI**❗ 👶 + +📤 📚 👷, & 🏆 ⚫️, **👆** 💪 ⚫️. + +👑 📋 👈 👆 💪 ▶️️ 🔜: + +* [ℹ 🎏 ⏮️ ❔ 📂](#help-others-with-questions-in-github){.internal-link target=_blank} (👀 📄 🔛). +* [📄 🚲 📨](#review-pull-requests){.internal-link target=_blank} (👀 📄 🔛). + +👈 2️⃣ 📋 ⚫️❔ **🍴 🕰 🏆**. 👈 👑 👷 🏆 FastAPI. + +🚥 👆 💪 ℹ 👤 ⏮️ 👈, **👆 🤝 👤 🚧 FastAPI** & ⚒ 💭 ⚫️ 🚧 **🛠️ ⏩ & 👻**. 👶 + +## 🛑 💬 + +🛑 👶 😧 💬 💽 👶 & 🤙 👅 ⏮️ 🎏 FastAPI 👪. + +!!! tip + ❔, 💭 👫 📂 💬, 📤 🌅 👍 🤞 👆 🔜 📨 ℹ [FastAPI 🕴](fastapi-people.md#experts){.internal-link target=_blank}. + + ⚙️ 💬 🕴 🎏 🏢 💬. + +### 🚫 ⚙️ 💬 ❔ + +✔️ 🤯 👈 💬 ✔ 🌅 "🆓 💬", ⚫️ ⏩ 💭 ❔ 👈 💁‍♂️ 🏢 & 🌅 ⚠ ❔,, 👆 💪 🚫 📨 ❔. + +📂, 📄 🔜 🦮 👆 ✍ ▶️️ ❔ 👈 👆 💪 🌖 💪 🤚 👍 ❔, ⚖️ ❎ ⚠ 👆 ⏭ 💬. & 📂 👤 💪 ⚒ 💭 👤 🕧 ❔ 🌐, 🚥 ⚫️ ✊ 🕰. 👤 💪 🚫 🤙 👈 ⏮️ 💬 ⚙️. 👶 + +💬 💬 ⚙️ 🚫 💪 📇 📂, ❔ & ❔ 5️⃣📆 🤚 💸 💬. & 🕴 🕐 📂 💯 ▶️️ [FastAPI 🕴](fastapi-people.md#experts){.internal-link target=_blank}, 👆 🔜 🌅 🎲 📨 🌅 🙋 📂. + +🔛 🎏 🚄, 📤 💯 👩‍💻 💬 ⚙️, 📤 ↕ 🤞 👆 🔜 🔎 👱 💬 📤, 🌖 🌐 🕰. 👶 + +## 💰 📕 + +👆 💪 💰 🐕‍🦺 📕 (👤) 🔘 📂 💰. + +📤 👆 💪 🛍 👤 ☕ 👶 👶 💬 👏. 👶 + +& 👆 💪 ▶️️ 🥇1st ⚖️ 🌟 💰 FastAPI. 👶 👶 + +## 💰 🧰 👈 🏋️ FastAPI + +👆 ✔️ 👀 🧾, FastAPI 🧍 🔛 ⌚ 🐘, 💃 & Pydantic. + +👆 💪 💰: + +* ✡ 🍏 (Pydantic) +* 🗜 (💃, Uvicorn) + +--- + +👏 ❗ 👶 diff --git a/docs/em/docs/history-design-future.md b/docs/em/docs/history-design-future.md new file mode 100644 index 0000000000..7e39972de8 --- /dev/null +++ b/docs/em/docs/history-design-future.md @@ -0,0 +1,79 @@ +# 📖, 🔧 & 🔮 + +🕰 🏁, **FastAPI** 👩‍💻 💭: + +> ⚫️❔ 📖 👉 🏗 ❓ ⚫️ 😑 ✔️ 👟 ⚪️➡️ 🕳 👌 👩‍❤‍👨 🗓️ [...] + +📥 🐥 🍖 👈 📖. + +## 🎛 + +👤 ✔️ 🏗 🔗 ⏮️ 🏗 📄 📚 1️⃣2️⃣🗓️ (🎰 🏫, 📎 ⚙️, 🔁 👨‍🏭, ☁ 💽, ♒️), ↘️ 📚 🏉 👩‍💻. + +🍕 👈, 👤 💪 🔬, 💯 & ⚙️ 📚 🎛. + +📖 **FastAPI** 👑 🍕 📖 🚮 ⏪. + +🙆‍♀ 📄 [🎛](alternatives.md){.internal-link target=_blank}: + +
+ +**FastAPI** 🚫🔜 🔀 🚥 🚫 ⏮️ 👷 🎏. + +📤 ✔️ 📚 🧰 ✍ ⏭ 👈 ✔️ ℹ 😮 🚮 🏗. + +👤 ✔️ ❎ 🏗 🆕 🛠️ 📚 1️⃣2️⃣🗓️. 🥇 👤 🔄 ❎ 🌐 ⚒ 📔 **FastAPI** ⚙️ 📚 🎏 🛠️, 🔌-🔌, & 🧰. + +✋️ ☝, 📤 🙅‍♂ 🎏 🎛 🌘 🏗 🕳 👈 🚚 🌐 👫 ⚒, ✊ 🏆 💭 ⚪️➡️ ⏮️ 🧰, & 🌀 👫 🏆 🌌 💪, ⚙️ 🇪🇸 ⚒ 👈 ➖🚫 💪 ⏭ (🐍 3️⃣.6️⃣ ➕ 🆎 🔑). + +
+ +## 🔬 + +⚙️ 🌐 ⏮️ 🎛 👤 ✔️ 🤞 💡 ⚪️➡️ 🌐 👫, ✊ 💭, & 🌀 👫 🏆 🌌 👤 💪 🔎 👤 & 🏉 👩‍💻 👤 ✔️ 👷 ⏮️. + +🖼, ⚫️ 🆑 👈 🎲 ⚫️ 🔜 ⚓️ 🔛 🐩 🐍 🆎 🔑. + +, 🏆 🎯 ⚙️ ⏪ ♻ 🐩. + +, ⏭ ▶️ 📟 **FastAPI**, 👤 💸 📚 🗓️ 🎓 🔌 🗄, 🎻 🔗, Oauth2️⃣, ♒️. 🎯 👫 💛, 🔀, & 🔺. + +## 🔧 + +⤴️ 👤 💸 🕰 🔧 👩‍💻 "🛠️" 👤 💚 ✔️ 👩‍💻 (👩‍💻 ⚙️ FastAPI). + +👤 💯 📚 💭 🏆 🌟 🐍 👨‍🎨: 🗒, 🆚 📟, 🎠 🧢 👨‍🎨. + +🏁 🐍 👩‍💻 🔬, 👈 📔 🔃 8️⃣0️⃣ 💯 👩‍💻. + +⚫️ ⛓ 👈 **FastAPI** 🎯 💯 ⏮️ 👨‍🎨 ⚙️ 8️⃣0️⃣ 💯 🐍 👩‍💻. & 🏆 🎏 👨‍🎨 😑 👷 ➡, 🌐 🚮 💰 🔜 👷 🌖 🌐 👨‍🎨. + +👈 🌌 👤 💪 🔎 🏆 🌌 📉 📟 ❎ 🌅 💪, ✔️ 🛠️ 🌐, 🆎 & ❌ ✅, ♒️. + +🌐 🌌 👈 🚚 🏆 🛠️ 💡 🌐 👩‍💻. + +## 📄 + +⏮️ 🔬 📚 🎛, 👤 💭 👈 👤 🔜 ⚙️ **Pydantic** 🚮 📈. + +⤴️ 👤 📉 ⚫️, ⚒ ⚫️ 🍕 🛠️ ⏮️ 🎻 🔗, 🐕‍🦺 🎏 🌌 🔬 ⚛ 📄, & 📉 👨‍🎨 🐕‍🦺 (🆎 ✅, ✍) ⚓️ 🔛 💯 📚 👨‍🎨. + +⏮️ 🛠️, 👤 📉 **💃**, 🎏 🔑 📄. + +## 🛠️ + +🕰 👤 ▶️ 🏗 **FastAPI** ⚫️, 🏆 🍖 ⏪ 🥉, 🔧 🔬, 📄 & 🧰 🔜, & 💡 🔃 🐩 & 🔧 🆑 & 🍋. + +## 🔮 + +👉 ☝, ⚫️ ⏪ 🆑 👈 **FastAPI** ⏮️ 🚮 💭 ➖ ⚠ 📚 👫👫. + +⚫️ 💆‍♂ 👐 🤭 ⏮️ 🎛 ♣ 📚 ⚙️ 💼 👍. + +📚 👩‍💻 & 🏉 ⏪ 🪀 🔛 **FastAPI** 👫 🏗 (🔌 👤 & 👇 🏉). + +✋️, 📤 📚 📈 & ⚒ 👟. + +**FastAPI** ✔️ 👑 🔮 ⤴️. + +& [👆 ℹ](help-fastapi.md){.internal-link target=_blank} 📉 👍. diff --git a/docs/em/docs/how-to/conditional-openapi.md b/docs/em/docs/how-to/conditional-openapi.md new file mode 100644 index 0000000000..a17ba4eec5 --- /dev/null +++ b/docs/em/docs/how-to/conditional-openapi.md @@ -0,0 +1,58 @@ +# 🎲 🗄 + +🚥 👆 💪, 👆 💪 ⚙️ ⚒ & 🌐 🔢 🔗 🗄 ✔ ⚓️ 🔛 🌐, & ❎ ⚫️ 🍕. + +## 🔃 💂‍♂, 🔗, & 🩺 + +🕵‍♂ 👆 🧾 👩‍💻 🔢 🏭 *🚫🔜 🚫* 🌌 🛡 👆 🛠️. + +👈 🚫 🚮 🙆 ➕ 💂‍♂ 👆 🛠️, *➡ 🛠️* 🔜 💪 🌐❔ 👫. + +🚥 📤 💂‍♂ ⚠ 👆 📟, ⚫️ 🔜 🔀. + +🕵‍♂ 🧾 ⚒ ⚫️ 🌅 ⚠ 🤔 ❔ 🔗 ⏮️ 👆 🛠️, & 💪 ⚒ ⚫️ 🌅 ⚠ 👆 ℹ ⚫️ 🏭. ⚫️ 💪 🤔 🎯 📨 💂‍♂ 🔘 🌌. + +🚥 👆 💚 🔐 👆 🛠️, 📤 📚 👍 👜 👆 💪, 🖼: + +* ⚒ 💭 👆 ✔️ 👍 🔬 Pydantic 🏷 👆 📨 💪 & 📨. +* 🔗 🙆 ✔ ✔ & 🔑 ⚙️ 🔗. +* 🙅 🏪 🔢 🔐, 🕴 🔐#️⃣. +* 🛠️ & ⚙️ 👍-💭 🔐 🧰, 💖 🇸🇲 & 🥙 🤝, ♒️. +* 🚮 🌅 🧽 ✔ 🎛 ⏮️ Oauth2️⃣ ↔ 🌐❔ 💪. +* ...♒️. + +👐, 👆 5️⃣📆 ✔️ 📶 🎯 ⚙️ 💼 🌐❔ 👆 🤙 💪 ❎ 🛠️ 🩺 🌐 (✅ 🏭) ⚖️ ⚓️ 🔛 📳 ⚪️➡️ 🌐 🔢. + +## 🎲 🗄 ⚪️➡️ ⚒ & 🇨🇻 { + +👆 💪 💪 ⚙️ 🎏 Pydantic ⚒ 🔗 👆 🏗 🗄 & 🩺 ⚜. + +🖼: + +```Python hl_lines="6 11" +{!../../../docs_src/conditional_openapi/tutorial001.py!} +``` + +📥 👥 📣 ⚒ `openapi_url` ⏮️ 🎏 🔢 `"/openapi.json"`. + +& ⤴️ 👥 ⚙️ ⚫️ 🕐❔ 🏗 `FastAPI` 📱. + +⤴️ 👆 💪 ❎ 🗄 (✅ 🎚 🩺) ⚒ 🌐 🔢 `OPENAPI_URL` 🛁 🎻, 💖: + +
+ +```console +$ OPENAPI_URL= uvicorn main:app + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +⤴️ 🚥 👆 🚶 📛 `/openapi.json`, `/docs`, ⚖️ `/redoc` 👆 🔜 🤚 `404 Not Found` ❌ 💖: + +```JSON +{ + "detail": "Not Found" +} +``` diff --git a/docs/em/docs/how-to/custom-request-and-route.md b/docs/em/docs/how-to/custom-request-and-route.md new file mode 100644 index 0000000000..d6fafa2ea6 --- /dev/null +++ b/docs/em/docs/how-to/custom-request-and-route.md @@ -0,0 +1,109 @@ +# 🛃 📨 & APIRoute 🎓 + +💼, 👆 5️⃣📆 💚 🔐 ⚛ ⚙️ `Request` & `APIRoute` 🎓. + +🎯, 👉 5️⃣📆 👍 🎛 ⚛ 🛠️. + +🖼, 🚥 👆 💚 ✍ ⚖️ 🔬 📨 💪 ⏭ ⚫️ 🛠️ 👆 🈸. + +!!! danger + 👉 "🏧" ⚒. + + 🚥 👆 ▶️ ⏮️ **FastAPI** 👆 💪 💚 🚶 👉 📄. + +## ⚙️ 💼 + +⚙️ 💼 🔌: + +* 🏭 🚫-🎻 📨 💪 🎻 (✅ `msgpack`). +* 🗜 🗜-🗜 📨 💪. +* 🔁 🚨 🌐 📨 💪. + +## 🚚 🛃 📨 💪 🔢 + +➡️ 👀 ❔ ⚒ ⚙️ 🛃 `Request` 🏿 🗜 🗜 📨. + +& `APIRoute` 🏿 ⚙️ 👈 🛃 📨 🎓. + +### ✍ 🛃 `GzipRequest` 🎓 + +!!! tip + 👉 🧸 🖼 🎦 ❔ ⚫️ 👷, 🚥 👆 💪 🗜 🐕‍🦺, 👆 💪 ⚙️ 🚚 [`GzipMiddleware`](./middleware.md#gzipmiddleware){.internal-link target=_blank}. + +🥇, 👥 ✍ `GzipRequest` 🎓, ❔ 🔜 📁 `Request.body()` 👩‍🔬 🗜 💪 🔍 ☑ 🎚. + +🚥 📤 🙅‍♂ `gzip` 🎚, ⚫️ 🔜 🚫 🔄 🗜 💪. + +👈 🌌, 🎏 🛣 🎓 💪 🍵 🗜 🗜 ⚖️ 🗜 📨. + +```Python hl_lines="8-15" +{!../../../docs_src/custom_request_and_route/tutorial001.py!} +``` + +### ✍ 🛃 `GzipRoute` 🎓 + +⏭, 👥 ✍ 🛃 🏿 `fastapi.routing.APIRoute` 👈 🔜 ⚒ ⚙️ `GzipRequest`. + +👉 🕰, ⚫️ 🔜 📁 👩‍🔬 `APIRoute.get_route_handler()`. + +👉 👩‍🔬 📨 🔢. & 👈 🔢 ⚫️❔ 🔜 📨 📨 & 📨 📨. + +📥 👥 ⚙️ ⚫️ ✍ `GzipRequest` ⚪️➡️ ⏮️ 📨. + +```Python hl_lines="18-26" +{!../../../docs_src/custom_request_and_route/tutorial001.py!} +``` + +!!! note "📡 ℹ" + `Request` ✔️ `request.scope` 🔢, 👈 🐍 `dict` ⚗ 🗃 🔗 📨. + + `Request` ✔️ `request.receive`, 👈 🔢 "📨" 💪 📨. + + `scope` `dict` & `receive` 🔢 👯‍♂️ 🍕 🔫 🔧. + + & 👈 2️⃣ 👜, `scope` & `receive`, ⚫️❔ 💪 ✍ 🆕 `Request` 👐. + + 💡 🌅 🔃 `Request` ✅ 💃 🩺 🔃 📨. + +🕴 👜 🔢 📨 `GzipRequest.get_route_handler` 🔨 🎏 🗜 `Request` `GzipRequest`. + +🔨 👉, 👆 `GzipRequest` 🔜 ✊ 💅 🗜 📊 (🚥 💪) ⏭ 🚶‍♀️ ⚫️ 👆 *➡ 🛠️*. + +⏮️ 👈, 🌐 🏭 ⚛ 🎏. + +✋️ ↩️ 👆 🔀 `GzipRequest.body`, 📨 💪 🔜 🔁 🗜 🕐❔ ⚫️ 📐 **FastAPI** 🕐❔ 💪. + +## 🔐 📨 💪 ⚠ 🐕‍🦺 + +!!! tip + ❎ 👉 🎏 ⚠, ⚫️ 🎲 📚 ⏩ ⚙️ `body` 🛃 🐕‍🦺 `RequestValidationError` ([🚚 ❌](../tutorial/handling-errors.md#use-the-requestvalidationerror-body){.internal-link target=_blank}). + + ✋️ 👉 🖼 ☑ & ⚫️ 🎦 ❔ 🔗 ⏮️ 🔗 🦲. + +👥 💪 ⚙️ 👉 🎏 🎯 🔐 📨 💪 ⚠ 🐕‍🦺. + +🌐 👥 💪 🍵 📨 🔘 `try`/`except` 🍫: + +```Python hl_lines="13 15" +{!../../../docs_src/custom_request_and_route/tutorial002.py!} +``` + +🚥 ⚠ 📉, `Request` 👐 🔜 ↔, 👥 💪 ✍ & ⚒ ⚙️ 📨 💪 🕐❔ 🚚 ❌: + +```Python hl_lines="16-18" +{!../../../docs_src/custom_request_and_route/tutorial002.py!} +``` + +## 🛃 `APIRoute` 🎓 📻 + +👆 💪 ⚒ `route_class` 🔢 `APIRouter`: + +```Python hl_lines="26" +{!../../../docs_src/custom_request_and_route/tutorial003.py!} +``` + +👉 🖼, *➡ 🛠️* 🔽 `router` 🔜 ⚙️ 🛃 `TimedRoute` 🎓, & 🔜 ✔️ ➕ `X-Response-Time` 🎚 📨 ⏮️ 🕰 ⚫️ ✊ 🏗 📨: + +```Python hl_lines="13-20" +{!../../../docs_src/custom_request_and_route/tutorial003.py!} +``` diff --git a/docs/em/docs/how-to/extending-openapi.md b/docs/em/docs/how-to/extending-openapi.md new file mode 100644 index 0000000000..6b3bc00757 --- /dev/null +++ b/docs/em/docs/how-to/extending-openapi.md @@ -0,0 +1,90 @@ +# ↔ 🗄 + +!!! warning + 👉 👍 🏧 ⚒. 👆 🎲 💪 🚶 ⚫️. + + 🚥 👆 📄 🔰 - 👩‍💻 🦮, 👆 💪 🎲 🚶 👉 📄. + + 🚥 👆 ⏪ 💭 👈 👆 💪 🔀 🏗 🗄 🔗, 😣 👂. + +📤 💼 🌐❔ 👆 💪 💪 🔀 🏗 🗄 🔗. + +👉 📄 👆 🔜 👀 ❔. + +## 😐 🛠️ + +😐 (🔢) 🛠️, ⏩. + +`FastAPI` 🈸 (👐) ✔️ `.openapi()` 👩‍🔬 👈 📈 📨 🗄 🔗. + +🍕 🈸 🎚 🏗, *➡ 🛠️* `/openapi.json` (⚖️ ⚫️❔ 👆 ⚒ 👆 `openapi_url`) ®. + +⚫️ 📨 🎻 📨 ⏮️ 🏁 🈸 `.openapi()` 👩‍🔬. + +🔢, ⚫️❔ 👩‍🔬 `.openapi()` 🔨 ✅ 🏠 `.openapi_schema` 👀 🚥 ⚫️ ✔️ 🎚 & 📨 👫. + +🚥 ⚫️ 🚫, ⚫️ 🏗 👫 ⚙️ 🚙 🔢 `fastapi.openapi.utils.get_openapi`. + +& 👈 🔢 `get_openapi()` 📨 🔢: + +* `title`: 🗄 📛, 🎦 🩺. +* `version`: ⏬ 👆 🛠️, ✅ `2.5.0`. +* `openapi_version`: ⏬ 🗄 🔧 ⚙️. 🔢, ⏪: `3.0.2`. +* `description`: 📛 👆 🛠️. +* `routes`: 📇 🛣, 👫 🔠 ® *➡ 🛠️*. 👫 ✊ ⚪️➡️ `app.routes`. + +## 🔑 🔢 + +⚙️ ℹ 🔛, 👆 💪 ⚙️ 🎏 🚙 🔢 🏗 🗄 🔗 & 🔐 🔠 🍕 👈 👆 💪. + +🖼, ➡️ 🚮 📄 🗄 ↔ 🔌 🛃 🔱. + +### 😐 **FastAPI** + +🥇, ✍ 🌐 👆 **FastAPI** 🈸 🛎: + +```Python hl_lines="1 4 7-9" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 🏗 🗄 🔗 + +⤴️, ⚙️ 🎏 🚙 🔢 🏗 🗄 🔗, 🔘 `custom_openapi()` 🔢: + +```Python hl_lines="2 15-20" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 🔀 🗄 🔗 + +🔜 👆 💪 🚮 📄 ↔, ❎ 🛃 `x-logo` `info` "🎚" 🗄 🔗: + +```Python hl_lines="21-23" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 💾 🗄 🔗 + +👆 💪 ⚙️ 🏠 `.openapi_schema` "💾", 🏪 👆 🏗 🔗. + +👈 🌌, 👆 🈸 🏆 🚫 ✔️ 🏗 🔗 🔠 🕰 👩‍💻 📂 👆 🛠️ 🩺. + +⚫️ 🔜 🏗 🕴 🕐, & ⤴️ 🎏 💾 🔗 🔜 ⚙️ ⏭ 📨. + +```Python hl_lines="13-14 24-25" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 🔐 👩‍🔬 + +🔜 👆 💪 ❎ `.openapi()` 👩‍🔬 ⏮️ 👆 🆕 🔢. + +```Python hl_lines="28" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### ✅ ⚫️ + +🕐 👆 🚶 http://127.0.0.1:8000/redoc 👆 🔜 👀 👈 👆 ⚙️ 👆 🛃 🔱 (👉 🖼, **FastAPI**'Ⓜ 🔱): + + diff --git a/docs/em/docs/how-to/graphql.md b/docs/em/docs/how-to/graphql.md new file mode 100644 index 0000000000..8509643ce8 --- /dev/null +++ b/docs/em/docs/how-to/graphql.md @@ -0,0 +1,56 @@ +# 🕹 + +**FastAPI** ⚓️ 🔛 **🔫** 🐩, ⚫️ 📶 ⏩ 🛠️ 🙆 **🕹** 🗃 🔗 ⏮️ 🔫. + +👆 💪 🌀 😐 FastAPI *➡ 🛠️* ⏮️ 🕹 🔛 🎏 🈸. + +!!! tip + **🕹** ❎ 📶 🎯 ⚙️ 💼. + + ⚫️ ✔️ **📈** & **⚠** 🕐❔ 🔬 ⚠ **🕸 🔗**. + + ⚒ 💭 👆 🔬 🚥 **💰** 👆 ⚙️ 💼 ⚖ **👐**. 👶 + +## 🕹 🗃 + +📥 **🕹** 🗃 👈 ✔️ **🔫** 🐕‍🦺. 👆 💪 ⚙️ 👫 ⏮️ **FastAPI**: + +* 🍓 👶 + * ⏮️ 🩺 FastAPI +* 👸 + * ⏮️ 🩺 💃 (👈 ✔ FastAPI) +* 🍟 + * ⏮️ 🍟 🔫 🚚 🔫 🛠️ +* + * ⏮️ 💃-Graphene3️⃣ + +## 🕹 ⏮️ 🍓 + +🚥 👆 💪 ⚖️ 💚 👷 ⏮️ **🕹**, **🍓** **👍** 🗃 ⚫️ ✔️ 🔧 🔐 **FastAPI** 🔧, ⚫️ 🌐 ⚓️ 🔛 **🆎 ✍**. + +⚓️ 🔛 👆 ⚙️ 💼, 👆 5️⃣📆 💖 ⚙️ 🎏 🗃, ✋️ 🚥 👆 💭 👤, 👤 🔜 🎲 🤔 👆 🔄 **🍓**. + +📥 🤪 🎮 ❔ 👆 💪 🛠️ 🍓 ⏮️ FastAPI: + +```Python hl_lines="3 22 25-26" +{!../../../docs_src/graphql/tutorial001.py!} +``` + +👆 💪 💡 🌅 🔃 🍓 🍓 🧾. + +& 🩺 🔃 🍓 ⏮️ FastAPI. + +## 🗝 `GraphQLApp` ⚪️➡️ 💃 + +⏮️ ⏬ 💃 🔌 `GraphQLApp` 🎓 🛠️ ⏮️ . + +⚫️ 😢 ⚪️➡️ 💃, ✋️ 🚥 👆 ✔️ 📟 👈 ⚙️ ⚫️, 👆 💪 💪 **↔** 💃-Graphene3️⃣, 👈 📔 🎏 ⚙️ 💼 & ✔️ **🌖 🌓 🔢**. + +!!! tip + 🚥 👆 💪 🕹, 👤 🔜 👍 👆 ✅ 👅 🍓, ⚫️ ⚓️ 🔛 🆎 ✍ ↩️ 🛃 🎓 & 🆎. + +## 💡 🌅 + +👆 💪 💡 🌅 🔃 **🕹** 🛂 🕹 🧾. + +👆 💪 ✍ 🌅 🔃 🔠 👈 🗃 🔬 🔛 👫 🔗. diff --git a/docs/em/docs/how-to/sql-databases-peewee.md b/docs/em/docs/how-to/sql-databases-peewee.md new file mode 100644 index 0000000000..62619fc2c3 --- /dev/null +++ b/docs/em/docs/how-to/sql-databases-peewee.md @@ -0,0 +1,529 @@ +# 🗄 (🔗) 💽 ⏮️ 🏒 + +!!! warning + 🚥 👆 ▶️, 🔰 [🗄 (🔗) 💽](../tutorial/sql-databases.md){.internal-link target=_blank} 👈 ⚙️ 🇸🇲 🔜 🥃. + + 💭 🆓 🚶 👉. + +🚥 👆 ▶️ 🏗 ⚪️➡️ 🖌, 👆 🎲 👻 📆 ⏮️ 🇸🇲 🐜 ([🗄 (🔗) 💽](../tutorial/sql-databases.md){.internal-link target=_blank}), ⚖️ 🙆 🎏 🔁 🐜. + +🚥 👆 ⏪ ✔️ 📟 🧢 👈 ⚙️ 🏒 🐜, 👆 💪 ✅ 📥 ❔ ⚙️ ⚫️ ⏮️ **FastAPI**. + +!!! warning "🐍 3️⃣.7️⃣ ➕ ✔" + 👆 🔜 💪 🐍 3️⃣.7️⃣ ⚖️ 🔛 🔒 ⚙️ 🏒 ⏮️ FastAPI. + +## 🏒 🔁 + +🏒 🚫 🔧 🔁 🛠️, ⚖️ ⏮️ 👫 🤯. + +🏒 ✔️ 🏋️ 🔑 🔃 🚮 🔢 & 🔃 ❔ ⚫️ 🔜 ⚙️. + +🚥 👆 🛠️ 🈸 ⏮️ 🗝 🚫-🔁 🛠️, & 💪 👷 ⏮️ 🌐 🚮 🔢, **⚫️ 💪 👑 🧰**. + +✋️ 🚥 👆 💪 🔀 🔢, 🐕‍🦺 🌖 🌘 1️⃣ 🔁 💽, 👷 ⏮️ 🔁 🛠️ (💖 FastAPI), ♒️, 👆 🔜 💪 🚮 🏗 ➕ 📟 🔐 👈 🔢. + +👐, ⚫️ 💪 ⚫️, & 📥 👆 🔜 👀 ⚫️❔ ⚫️❔ 📟 👆 ✔️ 🚮 💪 ⚙️ 🏒 ⏮️ FastAPI. + +!!! note "📡 ℹ" + 👆 💪 ✍ 🌅 🔃 🏒 🧍 🔃 🔁 🐍 🩺, , 🇵🇷. + +## 🎏 📱 + +👥 🔜 ✍ 🎏 🈸 🇸🇲 🔰 ([🗄 (🔗) 💽](../tutorial/sql-databases.md){.internal-link target=_blank}). + +🌅 📟 🤙 🎏. + +, 👥 🔜 🎯 🕴 🔛 🔺. + +## 📁 📊 + +➡️ 💬 👆 ✔️ 📁 📛 `my_super_project` 👈 🔌 🎧-📁 🤙 `sql_app` ⏮️ 📊 💖 👉: + +``` +. +└── sql_app + ├── __init__.py + ├── crud.py + ├── database.py + ├── main.py + └── schemas.py +``` + +👉 🌖 🎏 📊 👥 ✔️ 🇸🇲 🔰. + +🔜 ➡️ 👀 ⚫️❔ 🔠 📁/🕹 🔨. + +## ✍ 🏒 🍕 + +➡️ 🔗 📁 `sql_app/database.py`. + +### 🐩 🏒 📟 + +➡️ 🥇 ✅ 🌐 😐 🏒 📟, ✍ 🏒 💽: + +```Python hl_lines="3 5 22" +{!../../../docs_src/sql_databases_peewee/sql_app/database.py!} +``` + +!!! tip + ✔️ 🤯 👈 🚥 👆 💚 ⚙️ 🎏 💽, 💖 ✳, 👆 🚫 🚫 🔀 🎻. 👆 🔜 💪 ⚙️ 🎏 🏒 💽 🎓. + +#### 🗒 + +❌: + +```Python +check_same_thread=False +``` + +🌓 1️⃣ 🇸🇲 🔰: + +```Python +connect_args={"check_same_thread": False} +``` + +...⚫️ 💪 🕴 `SQLite`. + +!!! info "📡 ℹ" + + ⚫️❔ 🎏 📡 ℹ [🗄 (🔗) 💽](../tutorial/sql-databases.md#note){.internal-link target=_blank} ✔. + +### ⚒ 🏒 🔁-🔗 `PeeweeConnectionState` + +👑 ❔ ⏮️ 🏒 & FastAPI 👈 🏒 ⚓️ 🙇 🔛 🐍 `threading.local`, & ⚫️ 🚫 ✔️ 🎯 🌌 🔐 ⚫️ ⚖️ ➡️ 👆 🍵 🔗/🎉 🔗 (🔨 🇸🇲 🔰). + +& `threading.local` 🚫 🔗 ⏮️ 🆕 🔁 ⚒ 🏛 🐍. + +!!! note "📡 ℹ" + `threading.local` ⚙️ ✔️ "🎱" 🔢 👈 ✔️ 🎏 💲 🔠 🧵. + + 👉 ⚠ 🗝 🛠️ 🏗 ✔️ 1️⃣ 👁 🧵 📍 📨, 🙅‍♂ 🌖, 🙅‍♂ 🌘. + + ⚙️ 👉, 🔠 📨 🔜 ✔️ 🚮 👍 💽 🔗/🎉, ❔ ☑ 🏁 🥅. + + ✋️ FastAPI, ⚙️ 🆕 🔁 ⚒, 💪 🍵 🌅 🌘 1️⃣ 📨 🔛 🎏 🧵. & 🎏 🕰, 👁 📨, ⚫️ 💪 🏃 💗 👜 🎏 🧵 (🧵), ⚓️ 🔛 🚥 👆 ⚙️ `async def` ⚖️ 😐 `def`. 👉 ⚫️❔ 🤝 🌐 🎭 📈 FastAPI. + +✋️ 🐍 3️⃣.7️⃣ & 🔛 🚚 🌖 🏧 🎛 `threading.local`, 👈 💪 ⚙️ 🥉 🌐❔ `threading.local` 🔜 ⚙️, ✋️ 🔗 ⏮️ 🆕 🔁 ⚒. + +👥 🔜 ⚙️ 👈. ⚫️ 🤙 `contextvars`. + +👥 🔜 🔐 🔗 🍕 🏒 👈 ⚙️ `threading.local` & ❎ 👫 ⏮️ `contextvars`, ⏮️ 🔗 ℹ. + +👉 5️⃣📆 😑 🍖 🏗 (& ⚫️ 🤙), 👆 🚫 🤙 💪 🍕 🤔 ❔ ⚫️ 👷 ⚙️ ⚫️. + +👥 🔜 ✍ `PeeweeConnectionState`: + +```Python hl_lines="10-19" +{!../../../docs_src/sql_databases_peewee/sql_app/database.py!} +``` + +👉 🎓 😖 ⚪️➡️ 🎁 🔗 🎓 ⚙️ 🏒. + +⚫️ ✔️ 🌐 ⚛ ⚒ 🏒 ⚙️ `contextvars` ↩️ `threading.local`. + +`contextvars` 👷 🍖 🎏 🌘 `threading.local`. ✋️ 🎂 🏒 🔗 📟 🤔 👈 👉 🎓 👷 ⏮️ `threading.local`. + +, 👥 💪 ➕ 🎱 ⚒ ⚫️ 👷 🚥 ⚫️ ⚙️ `threading.local`. `__init__`, `__setattr__`, & `__getattr__` 🛠️ 🌐 ✔ 🎱 👉 ⚙️ 🏒 🍵 🤔 👈 ⚫️ 🔜 🔗 ⏮️ FastAPI. + +!!! tip + 👉 🔜 ⚒ 🏒 🎭 ☑ 🕐❔ ⚙️ ⏮️ FastAPI. 🚫 🎲 📂 ⚖️ 📪 🔗 👈 ➖ ⚙️, 🏗 ❌, ♒️. + + ✋️ ⚫️ 🚫 🤝 🏒 🔁 💎-🏋️. 👆 🔜 ⚙️ 😐 `def` 🔢 & 🚫 `async def`. + +### ⚙️ 🛃 `PeeweeConnectionState` 🎓 + +🔜, 📁 `._state` 🔗 🔢 🏒 💽 `db` 🎚 ⚙️ 🆕 `PeeweeConnectionState`: + +```Python hl_lines="24" +{!../../../docs_src/sql_databases_peewee/sql_app/database.py!} +``` + +!!! tip + ⚒ 💭 👆 📁 `db._state` *⏮️* 🏗 `db`. + +!!! tip + 👆 🔜 🎏 🙆 🎏 🏒 💽, 🔌 `PostgresqlDatabase`, `MySQLDatabase`, ♒️. + +## ✍ 💽 🏷 + +➡️ 🔜 👀 📁 `sql_app/models.py`. + +### ✍ 🏒 🏷 👆 💽 + +🔜 ✍ 🏒 🏷 (🎓) `User` & `Item`. + +👉 🎏 👆 🔜 🚥 👆 ⏩ 🏒 🔰 & ℹ 🏷 ✔️ 🎏 💽 🇸🇲 🔰. + +!!! tip + 🏒 ⚙️ ⚖ "**🏷**" 🔗 👉 🎓 & 👐 👈 🔗 ⏮️ 💽. + + ✋️ Pydantic ⚙️ ⚖ "**🏷**" 🔗 🕳 🎏, 💽 🔬, 🛠️, & 🧾 🎓 & 👐. + +🗄 `db` ⚪️➡️ `database` (📁 `database.py` ⚪️➡️ 🔛) & ⚙️ ⚫️ 📥. + +```Python hl_lines="3 6-12 15-21" +{!../../../docs_src/sql_databases_peewee/sql_app/models.py!} +``` + +!!! tip + 🏒 ✍ 📚 🎱 🔢. + + ⚫️ 🔜 🔁 🚮 `id` 🔢 🔢 👑 🔑. + + ⚫️ 🔜 ⚒ 📛 🏓 ⚓️ 🔛 🎓 📛. + + `Item`, ⚫️ 🔜 ✍ 🔢 `owner_id` ⏮️ 🔢 🆔 `User`. ✋️ 👥 🚫 📣 ⚫️ 🙆. + +## ✍ Pydantic 🏷 + +🔜 ➡️ ✅ 📁 `sql_app/schemas.py`. + +!!! tip + ❎ 😨 🖖 🏒 *🏷* & Pydantic *🏷*, 👥 🔜 ✔️ 📁 `models.py` ⏮️ 🏒 🏷, & 📁 `schemas.py` ⏮️ Pydantic 🏷. + + 👫 Pydantic 🏷 🔬 🌅 ⚖️ 🌘 "🔗" (☑ 📊 💠). + + 👉 🔜 ℹ 👥 ❎ 😨 ⏪ ⚙️ 👯‍♂️. + +### ✍ Pydantic *🏷* / 🔗 + +✍ 🌐 🎏 Pydantic 🏷 🇸🇲 🔰: + +```Python hl_lines="16-18 21-22 25-30 34-35 38-39 42-48" +{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!} +``` + +!!! tip + 📥 👥 🏗 🏷 ⏮️ `id`. + + 👥 🚫 🎯 ✔ `id` 🔢 🏒 🏷, ✋️ 🏒 🚮 1️⃣ 🔁. + + 👥 ❎ 🎱 `owner_id` 🔢 `Item`. + +### ✍ `PeeweeGetterDict` Pydantic *🏷* / 🔗 + +🕐❔ 👆 🔐 💛 🏒 🎚, 💖 `some_user.items`, 🏒 🚫 🚚 `list` `Item`. + +⚫️ 🚚 🎁 🛃 🎚 🎓 `ModelSelect`. + +⚫️ 💪 ✍ `list` 🚮 🏬 ⏮️ `list(some_user.items)`. + +✋️ 🎚 ⚫️ 🚫 `list`. & ⚫️ 🚫 ☑ 🐍 🚂. ↩️ 👉, Pydantic 🚫 💭 🔢 ❔ 🗜 ⚫️ `list` Pydantic *🏷* / 🔗. + +✋️ ⏮️ ⏬ Pydantic ✔ 🚚 🛃 🎓 👈 😖 ⚪️➡️ `pydantic.utils.GetterDict`, 🚚 🛠️ ⚙️ 🕐❔ ⚙️ `orm_mode = True` 🗃 💲 🐜 🏷 🔢. + +👥 🔜 ✍ 🛃 `PeeweeGetterDict` 🎓 & ⚙️ ⚫️ 🌐 🎏 Pydantic *🏷* / 🔗 👈 ⚙️ `orm_mode`: + +```Python hl_lines="3 8-13 31 49" +{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!} +``` + +📥 👥 ✅ 🚥 🔢 👈 ➖ 🔐 (✅ `.items` `some_user.items`) 👐 `peewee.ModelSelect`. + +& 🚥 👈 💼, 📨 `list` ⏮️ ⚫️. + +& ⤴️ 👥 ⚙️ ⚫️ Pydantic *🏷* / 🔗 👈 ⚙️ `orm_mode = True`, ⏮️ 📳 🔢 `getter_dict = PeeweeGetterDict`. + +!!! tip + 👥 🕴 💪 ✍ 1️⃣ `PeeweeGetterDict` 🎓, & 👥 💪 ⚙️ ⚫️ 🌐 Pydantic *🏷* / 🔗. + +## 💩 🇨🇻 + +🔜 ➡️ 👀 📁 `sql_app/crud.py`. + +### ✍ 🌐 💩 🇨🇻 + +✍ 🌐 🎏 💩 🇨🇻 🇸🇲 🔰, 🌐 📟 📶 🎏: + +```Python hl_lines="1 4-5 8-9 12-13 16-20 23-24 27-30" +{!../../../docs_src/sql_databases_peewee/sql_app/crud.py!} +``` + +📤 🔺 ⏮️ 📟 🇸🇲 🔰. + +👥 🚫 🚶‍♀️ `db` 🔢 🤭. ↩️ 👥 ⚙️ 🏷 🔗. 👉 ↩️ `db` 🎚 🌐 🎚, 👈 🔌 🌐 🔗 ⚛. 👈 ⚫️❔ 👥 ✔️ 🌐 `contextvars` ℹ 🔛. + +🆖, 🕐❔ 🛬 📚 🎚, 💖 `get_users`, 👥 🔗 🤙 `list`, 💖: + +```Python +list(models.User.select()) +``` + +👉 🎏 🤔 👈 👥 ✔️ ✍ 🛃 `PeeweeGetterDict`. ✋️ 🛬 🕳 👈 ⏪ `list` ↩️ `peewee.ModelSelect` `response_model` *➡ 🛠️* ⏮️ `List[models.User]` (👈 👥 🔜 👀 ⏪) 🔜 👷 ☑. + +## 👑 **FastAPI** 📱 + +& 🔜 📁 `sql_app/main.py` ➡️ 🛠️ & ⚙️ 🌐 🎏 🍕 👥 ✍ ⏭. + +### ✍ 💽 🏓 + +📶 🙃 🌌 ✍ 💽 🏓: + +```Python hl_lines="9-11" +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} +``` + +### ✍ 🔗 + +✍ 🔗 👈 🔜 🔗 💽 ▶️️ ▶️ 📨 & 🔌 ⚫️ 🔚: + +```Python hl_lines="23-29" +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} +``` + +📥 👥 ✔️ 🛁 `yield` ↩️ 👥 🤙 🚫 ⚙️ 💽 🎚 🔗. + +⚫️ 🔗 💽 & ♻ 🔗 💽 🔗 🔢 👈 🔬 🔠 📨 (⚙️ `contextvars` 🎱 ⚪️➡️ 🔛). + +↩️ 💽 🔗 ⚠ 👤/🅾 🚧, 👉 🔗 ✍ ⏮️ 😐 `def` 🔢. + +& ⤴️, 🔠 *➡ 🛠️ 🔢* 👈 💪 🔐 💽 👥 🚮 ⚫️ 🔗. + +✋️ 👥 🚫 ⚙️ 💲 👐 👉 🔗 (⚫️ 🤙 🚫 🤝 🙆 💲, ⚫️ ✔️ 🛁 `yield`). , 👥 🚫 🚮 ⚫️ *➡ 🛠️ 🔢* ✋️ *➡ 🛠️ 👨‍🎨* `dependencies` 🔢: + +```Python hl_lines="32 40 47 59 65 72" +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} +``` + +### 🔑 🔢 🎧-🔗 + +🌐 `contextvars` 🍕 👷, 👥 💪 ⚒ 💭 👥 ✔️ 🔬 💲 `ContextVar` 🔠 📨 👈 ⚙️ 💽, & 👈 💲 🔜 ⚙️ 💽 🇵🇸 (🔗, 💵, ♒️) 🎂 📨. + +👈, 👥 💪 ✍ ➕1️⃣ `async` 🔗 `reset_db_state()` 👈 ⚙️ 🎧-🔗 `get_db()`. ⚫️ 🔜 ⚒ 💲 🔑 🔢 (⏮️ 🔢 `dict`) 👈 🔜 ⚙️ 💽 🇵🇸 🎂 📨. & ⤴️ 🔗 `get_db()` 🔜 🏪 ⚫️ 💽 🇵🇸 (🔗, 💵, ♒️). + +```Python hl_lines="18-20" +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} +``` + +**⏭ 📨**, 👥 🔜 ⏲ 👈 🔑 🔢 🔄 `async` 🔗 `reset_db_state()` & ⤴️ ✍ 🆕 🔗 `get_db()` 🔗, 👈 🆕 📨 🔜 ✔️ 🚮 👍 💽 🇵🇸 (🔗, 💵, ♒️). + +!!! tip + FastAPI 🔁 🛠️, 1️⃣ 📨 💪 ▶️ ➖ 🛠️, & ⏭ 🏁, ➕1️⃣ 📨 💪 📨 & ▶️ 🏭 👍, & ⚫️ 🌐 💪 🛠️ 🎏 🧵. + + ✋️ 🔑 🔢 🤔 👫 🔁 ⚒,, 🏒 💽 🇵🇸 ⚒ `async` 🔗 `reset_db_state()` 🔜 🚧 🚮 👍 💽 🎂 🎂 📨. + + & 🎏 🕰, 🎏 🛠️ 📨 🔜 ✔️ 🚮 👍 💽 🇵🇸 👈 🔜 🔬 🎂 📨. + +#### 🏒 🗳 + +🚥 👆 ⚙️ 🏒 🗳, ☑ 💽 `db.obj`. + +, 👆 🔜 ⏲ ⚫️ ⏮️: + +```Python hl_lines="3-4" +async def reset_db_state(): + database.db.obj._state._state.set(db_state_default.copy()) + database.db.obj._state.reset() +``` + +### ✍ 👆 **FastAPI** *➡ 🛠️* + +🔜, 😒, 📥 🐩 **FastAPI** *➡ 🛠️* 📟. + +```Python hl_lines="32-37 40-43 46-53 56-62 65-68 71-79" +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} +``` + +### 🔃 `def` 🆚 `async def` + +🎏 ⏮️ 🇸🇲, 👥 🚫 🔨 🕳 💖: + +```Python +user = await models.User.select().first() +``` + +...✋️ ↩️ 👥 ⚙️: + +```Python +user = models.User.select().first() +``` + +, 🔄, 👥 🔜 📣 *➡ 🛠️ 🔢* & 🔗 🍵 `async def`, ⏮️ 😐 `def`,: + +```Python hl_lines="2" +# Something goes here +def read_users(skip: int = 0, limit: int = 100): + # Something goes here +``` + +## 🔬 🏒 ⏮️ 🔁 + +👉 🖼 🔌 ➕ *➡ 🛠️* 👈 🔬 📏 🏭 📨 ⏮️ `time.sleep(sleep_time)`. + +⚫️ 🔜 ✔️ 💽 🔗 📂 ▶️ & 🔜 ⌛ 🥈 ⏭ 🙇 🔙. & 🔠 🆕 📨 🔜 ⌛ 🕐 🥈 🌘. + +👉 🔜 💪 ➡️ 👆 💯 👈 👆 📱 ⏮️ 🏒 & FastAPI 🎭 ☑ ⏮️ 🌐 💩 🔃 🧵. + +🚥 👆 💚 ✅ ❔ 🏒 🔜 💔 👆 📱 🚥 ⚙️ 🍵 🛠️, 🚶 `sql_app/database.py` 📁 & 🏤 ⏸: + +```Python +# db._state = PeeweeConnectionState() +``` + +& 📁 `sql_app/main.py` 📁, 🏤 💪 `async` 🔗 `reset_db_state()` & ❎ ⚫️ ⏮️ `pass`: + +```Python +async def reset_db_state(): +# database.db._state._state.set(db_state_default.copy()) +# database.db._state.reset() + pass +``` + +⤴️ 🏃 👆 📱 ⏮️ Uvicorn: + +
+ +```console +$ uvicorn sql_app.main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +📂 👆 🖥 http://127.0.0.1:8000/docs & ✍ 👩‍❤‍👨 👩‍💻. + +⤴️ 📂 1️⃣0️⃣ 📑 http://127.0.0.1:8000/docs#/default/read_🐌_👩‍💻_slowusers_ = 🎏 🕰. + +🚶 *➡ 🛠️* "🤚 `/slowusers/`" 🌐 📑. ⚙️ "🔄 ⚫️ 👅" 🔼 & 🛠️ 📨 🔠 📑, 1️⃣ ▶️️ ⏮️ 🎏. + +📑 🔜 ⌛ 🍖 & ⤴️ 👫 🔜 🎦 `Internal Server Error`. + +### ⚫️❔ 🔨 + +🥇 📑 🔜 ⚒ 👆 📱 ✍ 🔗 💽 & ⌛ 🥈 ⏭ 🙇 🔙 & 📪 💽 🔗. + +⤴️, 📨 ⏭ 📑, 👆 📱 🔜 ⌛ 🕐 🥈 🌘, & 🔛. + +👉 ⛓ 👈 ⚫️ 🔜 🔚 🆙 🏁 🏁 📑' 📨 ⏪ 🌘 ⏮️ 🕐. + +⤴️ 1️⃣ 🏁 📨 👈 ⌛ 🌘 🥈 🔜 🔄 📂 💽 🔗, ✋️ 1️⃣ 📚 ⏮️ 📨 🎏 📑 🔜 🎲 🍵 🎏 🧵 🥇 🕐, ⚫️ 🔜 ✔️ 🎏 💽 🔗 👈 ⏪ 📂, & 🏒 🔜 🚮 ❌ & 👆 🔜 👀 ⚫️ 📶, & 📨 🔜 ✔️ `Internal Server Error`. + +👉 🔜 🎲 🔨 🌅 🌘 1️⃣ 📚 📑. + +🚥 👆 ✔️ 💗 👩‍💻 💬 👆 📱 ⚫️❔ 🎏 🕰, 👉 ⚫️❔ 💪 🔨. + +& 👆 📱 ▶️ 🍵 🌅 & 🌖 👩‍💻 🎏 🕰, ⌛ 🕰 👁 📨 💪 📏 & 📏 ⏲ ❌. + +### 🔧 🏒 ⏮️ FastAPI + +🔜 🚶 🔙 📁 `sql_app/database.py`, & ✍ ⏸: + +```Python +db._state = PeeweeConnectionState() +``` + +& 📁 `sql_app/main.py` 📁, ✍ 💪 `async` 🔗 `reset_db_state()`: + +```Python +async def reset_db_state(): + database.db._state._state.set(db_state_default.copy()) + database.db._state.reset() +``` + +❎ 👆 🏃‍♂ 📱 & ▶️ ⚫️ 🔄. + +🔁 🎏 🛠️ ⏮️ 1️⃣0️⃣ 📑. 👉 🕰 🌐 👫 🔜 ⌛ & 👆 🔜 🤚 🌐 🏁 🍵 ❌. + +...👆 🔧 ⚫️ ❗ + +## 📄 🌐 📁 + + 💭 👆 🔜 ✔️ 📁 📛 `my_super_project` (⚖️ 👐 👆 💚) 👈 🔌 🎧-📁 🤙 `sql_app`. + +`sql_app` 🔜 ✔️ 📄 📁: + +* `sql_app/__init__.py`: 🛁 📁. + +* `sql_app/database.py`: + +```Python +{!../../../docs_src/sql_databases_peewee/sql_app/database.py!} +``` + +* `sql_app/models.py`: + +```Python +{!../../../docs_src/sql_databases_peewee/sql_app/models.py!} +``` + +* `sql_app/schemas.py`: + +```Python +{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!} +``` + +* `sql_app/crud.py`: + +```Python +{!../../../docs_src/sql_databases_peewee/sql_app/crud.py!} +``` + +* `sql_app/main.py`: + +```Python +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} +``` + +## 📡 ℹ + +!!! warning + 👉 📶 📡 ℹ 👈 👆 🎲 🚫 💪. + +### ⚠ + +🏒 ⚙️ `threading.local` 🔢 🏪 ⚫️ 💽 "🇵🇸" 💽 (🔗, 💵, ♒️). + +`threading.local` ✍ 💲 🌟 ⏮️ 🧵, ✋️ 🔁 🛠️ 🔜 🏃 🌐 📟 (✅ 🔠 📨) 🎏 🧵, & 🎲 🚫 ✔. + +🔛 🔝 👈, 🔁 🛠️ 💪 🏃 🔁 📟 🧵 (⚙️ `asyncio.run_in_executor`), ✋️ 🔗 🎏 📨. + +👉 ⛓ 👈, ⏮️ 🏒 ⏮️ 🛠️, 💗 📋 💪 ⚙️ 🎏 `threading.local` 🔢 & 🔚 🆙 🤝 🎏 🔗 & 💽 (👈 👫 🚫🔜 🚫), & 🎏 🕰, 🚥 👫 🛠️ 🔁 👤/🅾-🚧 📟 🧵 (⏮️ 😐 `def` 🔢 FastAPI, *➡ 🛠️* & 🔗), 👈 📟 🏆 🚫 ✔️ 🔐 💽 🇵🇸 🔢, ⏪ ⚫️ 🍕 🎏 📨 & ⚫️ 🔜 💪 🤚 🔐 🎏 💽 🇵🇸. + +### 🔑 🔢 + +🐍 3️⃣.7️⃣ ✔️ `contextvars` 👈 💪 ✍ 🇧🇿 🔢 📶 🎏 `threading.local`, ✋️ 🔗 👫 🔁 ⚒. + +📤 📚 👜 ✔️ 🤯. + +`ContextVar` ✔️ ✍ 🔝 🕹, 💖: + +```Python +some_var = ContextVar("some_var", default="default value") +``` + +⚒ 💲 ⚙️ ⏮️ "🔑" (✅ ⏮️ 📨) ⚙️: + +```Python +some_var.set("new value") +``` + +🤚 💲 🙆 🔘 🔑 (✅ 🙆 🍕 🚚 ⏮️ 📨) ⚙️: + +```Python +some_var.get() +``` + +### ⚒ 🔑 🔢 `async` 🔗 `reset_db_state()` + +🚥 🍕 🔁 📟 ⚒ 💲 ⏮️ `some_var.set("updated in function")` (✅ 💖 `async` 🔗), 🎂 📟 ⚫️ & 📟 👈 🚶 ⏮️ (✅ 📟 🔘 `async` 🔢 🤙 ⏮️ `await`) 🔜 👀 👈 🆕 💲. + +, 👆 💼, 🚥 👥 ⚒ 🏒 🇵🇸 🔢 (⏮️ 🔢 `dict`) `async` 🔗, 🌐 🎂 🔗 📟 👆 📱 🔜 👀 👉 💲 & 🔜 💪 ♻ ⚫️ 🎂 📨. + +& 🔑 🔢 🔜 ⚒ 🔄 ⏭ 📨, 🚥 👫 🛠️. + +### ⚒ 💽 🇵🇸 🔗 `get_db()` + +`get_db()` 😐 `def` 🔢, **FastAPI** 🔜 ⚒ ⚫️ 🏃 🧵, ⏮️ *📁* "🔑", 🧑‍🤝‍🧑 🎏 💲 🔑 🔢 ( `dict` ⏮️ ⏲ 💽 🇵🇸). ⤴️ ⚫️ 💪 🚮 💽 🇵🇸 👈 `dict`, 💖 🔗, ♒️. + +✋️ 🚥 💲 🔑 🔢 (🔢 `dict`) ⚒ 👈 😐 `def` 🔢, ⚫️ 🔜 ✍ 🆕 💲 👈 🔜 🚧 🕴 👈 🧵 🧵, & 🎂 📟 (💖 *➡ 🛠️ 🔢*) 🚫🔜 ✔️ 🔐 ⚫️. `get_db()` 👥 💪 🕴 ⚒ 💲 `dict`, ✋️ 🚫 🎂 `dict` ⚫️. + +, 👥 💪 ✔️ `async` 🔗 `reset_db_state()` ⚒ `dict` 🔑 🔢. 👈 🌌, 🌐 📟 ✔️ 🔐 🎏 `dict` 💽 🇵🇸 👁 📨. + +### 🔗 & 🔌 🔗 `get_db()` + +⤴️ ⏭ ❔ 🔜, ⚫️❔ 🚫 🔗 & 🔌 💽 `async` 🔗 ⚫️, ↩️ `get_db()`❓ + +`async` 🔗 ✔️ `async` 🔑 🔢 🛡 🎂 📨, ✋️ 🏗 & 📪 💽 🔗 ⚠ 🚧, ⚫️ 💪 📉 🎭 🚥 ⚫️ 📤. + +👥 💪 😐 `def` 🔗 `get_db()`. diff --git a/docs/em/docs/index.md b/docs/em/docs/index.md new file mode 100644 index 0000000000..c7df281609 --- /dev/null +++ b/docs/em/docs/index.md @@ -0,0 +1,469 @@ +

+ FastAPI +

+

+ FastAPI 🛠️, ↕ 🎭, ⏩ 💡, ⏩ 📟, 🔜 🏭 +

+

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

+ +--- + +**🧾**: https://fastapi.tiangolo.com + +**ℹ 📟**: https://github.com/tiangolo/fastapi + +--- + +FastAPI 🏛, ⏩ (↕-🎭), 🕸 🛠️ 🏗 🛠️ ⏮️ 🐍 3️⃣.8️⃣ ➕ ⚓️ 🔛 🐩 🐍 🆎 🔑. + +🔑 ⚒: + +* **⏩**: 📶 ↕ 🎭, 🔛 🇷🇪 ⏮️ **✳** & **🚶** (👏 💃 & Pydantic). [1️⃣ ⏩ 🐍 🛠️ 💪](#performance). +* **⏩ 📟**: 📈 🚅 🛠️ ⚒ 🔃 2️⃣0️⃣0️⃣ 💯 3️⃣0️⃣0️⃣ 💯. * +* **👩‍❤‍👨 🐛**: 📉 🔃 4️⃣0️⃣ 💯 🗿 (👩‍💻) 📉 ❌. * +* **🏋️**: 👑 👨‍🎨 🐕‍🦺. 🛠️ 🌐. 🌘 🕰 🛠️. +* **⏩**: 🔧 ⏩ ⚙️ & 💡. 🌘 🕰 👂 🩺. +* **📏**: 📉 📟 ❎. 💗 ⚒ ⚪️➡️ 🔠 🔢 📄. 👩‍❤‍👨 🐛. +* **🏋️**: 🤚 🏭-🔜 📟. ⏮️ 🏧 🎓 🧾. +* **🐩-⚓️**: ⚓️ 🔛 (& 🍕 🔗 ⏮️) 📂 🐩 🔗: 🗄 (⏪ 💭 🦁) & 🎻 🔗. + +* ⚖ ⚓️ 🔛 💯 🔛 🔗 🛠️ 🏉, 🏗 🏭 🈸. + +## 💰 + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +🎏 💰 + +## 🤔 + +"_[...] 👤 ⚙️ **FastAPI** 📚 👫 📆. [...] 👤 🤙 📆 ⚙️ ⚫️ 🌐 👇 🏉 **⚗ 🐕‍🦺 🤸‍♂**. 👫 💆‍♂ 🛠️ 🔘 🐚 **🖥** 🏬 & **📠** 🏬._" + +
🧿 🇵🇰 - 🤸‍♂ (🇦🇪)
+ +--- + +"_👥 🛠️ **FastAPI** 🗃 🤖 **🎂** 💽 👈 💪 🔢 🚚 **🔮**. [👨📛]_" + +
🇮🇹 🇸🇻, 👨📛 👨📛, & 🇱🇰 🕉 🕉 - 🙃 (🇦🇪)
+ +--- + +"_**📺** 🙏 📣 📂-ℹ 🚀 👆 **⚔ 🧾** 🎶 🛠️: **📨**❗ [🏗 ⏮️ **FastAPI**]_" + +
✡ 🍏, 👖 🇪🇸, 🌲 🍏 - 📺 (🇦🇪)
+ +--- + +"_👤 🤭 🌕 😄 🔃 **FastAPI**. ⚫️ 🎊 ❗_" + +
✡ 🇭🇰 - 🐍 🔢 📻 🦠 (🇦🇪)
+ +--- + +"_🤙, ⚫️❔ 👆 ✔️ 🏗 👀 💎 💠 & 🇵🇱. 📚 🌌, ⚫️ ⚫️❔ 👤 💚 **🤗** - ⚫️ 🤙 😍 👀 👱 🏗 👈._" + +
✡ 🗄 - 🤗 👼 (🇦🇪)
+ +--- + +"_🚥 👆 👀 💡 1️⃣ **🏛 🛠️** 🏗 🎂 🔗, ✅ 👅 **FastAPI** [...] ⚫️ ⏩, ⏩ ⚙️ & ⏩ 💡 [...]_" + +"_👥 ✔️ 🎛 🤭 **FastAPI** 👆 **🔗** [...] 👤 💭 👆 🔜 💖 ⚫️ [...]_" + +
🇱🇨 🇸🇲 - ✡ Honnibal - 💥 👲 🕴 - 🌈 👼 (🇦🇪) - (🇦🇪)
+ +--- + +"_🚥 🙆 👀 🏗 🏭 🐍 🛠️, 👤 🔜 🏆 👍 **FastAPI**. ⚫️ **💎 🏗**, **🙅 ⚙️** & **🏆 🛠️**, ⚫️ ✔️ ▶️️ **🔑 🦲** 👆 🛠️ 🥇 🛠️ 🎛 & 🚘 📚 🏧 & 🐕‍🦺 ✅ 👆 🕹 🔫 👨‍💻._" + +
🇹🇦 🍰 - 📻 (🇦🇪)
+ +--- + +## **🏎**, FastAPI 🇳🇨 + + + +🚥 👆 🏗 📱 ⚙️ 📶 ↩️ 🕸 🛠️, ✅ 👅 **🏎**. + +**🏎** FastAPI 🐥 👪. & ⚫️ 🎯 **FastAPI 🇳🇨**. 👶 👶 👶 + +## 📄 + +🐍 3️⃣.7️⃣ ➕ + +FastAPI 🧍 🔛 ⌚ 🐘: + +* 💃 🕸 🍕. +* Pydantic 📊 🍕. + +## 👷‍♂ + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +👆 🔜 💪 🔫 💽, 🏭 ✅ Uvicorn ⚖️ Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## 🖼 + +### ✍ ⚫️ + +* ✍ 📁 `main.py` ⏮️: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+⚖️ ⚙️ async def... + +🚥 👆 📟 ⚙️ `async` / `await`, ⚙️ `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**🗒**: + +🚥 👆 🚫 💭, ✅ _"🏃 ❓" _ 📄 🔃 `async` & `await` 🩺. + +
+ +### 🏃 ⚫️ + +🏃 💽 ⏮️: + +
+ +```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. +``` + +
+ +
+🔃 📋 uvicorn main:app --reload... + +📋 `uvicorn main:app` 🔗: + +* `main`: 📁 `main.py` (🐍 "🕹"). +* `app`: 🎚 ✍ 🔘 `main.py` ⏮️ ⏸ `app = FastAPI()`. +* `--reload`: ⚒ 💽 ⏏ ⏮️ 📟 🔀. 🕴 👉 🛠️. + +
+ +### ✅ ⚫️ + +📂 👆 🖥 http://127.0.0.1:8000/items/5?q=somequery. + +👆 🔜 👀 🎻 📨: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +👆 ⏪ ✍ 🛠️ 👈: + +* 📨 🇺🇸🔍 📨 _➡_ `/` & `/items/{item_id}`. +* 👯‍♂️ _➡_ ✊ `GET` 🛠️ (💭 🇺🇸🔍 _👩‍🔬_). +* _➡_ `/items/{item_id}` ✔️ _➡ 🔢_ `item_id` 👈 🔜 `int`. +* _➡_ `/items/{item_id}` ✔️ 📦 `str` _🔢 = `q`. + +### 🎓 🛠️ 🩺 + +🔜 🚶 http://127.0.0.1:8000/docs. + +👆 🔜 👀 🏧 🎓 🛠️ 🧾 (🚚 🦁 🎚): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### 🎛 🛠️ 🩺 + +& 🔜, 🚶 http://127.0.0.1:8000/redoc. + +👆 🔜 👀 🎛 🏧 🧾 (🚚 📄): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## 🖼 ♻ + +🔜 🔀 📁 `main.py` 📨 💪 ⚪️➡️ `PUT` 📨. + +📣 💪 ⚙️ 🐩 🐍 🆎, 👏 Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +💽 🔜 🔃 🔁 (↩️ 👆 🚮 `--reload` `uvicorn` 📋 🔛). + +### 🎓 🛠️ 🩺 ♻ + +🔜 🚶 http://127.0.0.1:8000/docs. + +* 🎓 🛠️ 🧾 🔜 🔁 ℹ, 🔌 🆕 💪: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* 🖊 🔛 🔼 "🔄 ⚫️ 👅", ⚫️ ✔ 👆 🥧 🔢 & 🔗 🔗 ⏮️ 🛠️: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* ⤴️ 🖊 🔛 "🛠️" 🔼, 👩‍💻 🔢 🔜 🔗 ⏮️ 👆 🛠️, 📨 🔢, 🤚 🏁 & 🎦 👫 🔛 🖥: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### 🎛 🛠️ 🩺 ♻ + +& 🔜, 🚶 http://127.0.0.1:8000/redoc. + +* 🎛 🧾 🔜 🎨 🆕 🔢 🔢 & 💪: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### 🌃 + +📄, 👆 📣 **🕐** 🆎 🔢, 💪, ♒️. 🔢 🔢. + +👆 👈 ⏮️ 🐩 🏛 🐍 🆎. + +👆 🚫 ✔️ 💡 🆕 ❕, 👩‍🔬 ⚖️ 🎓 🎯 🗃, ♒️. + +🐩 **🐍 3️⃣.7️⃣ ➕**. + +🖼, `int`: + +```Python +item_id: int +``` + +⚖️ 🌖 🏗 `Item` 🏷: + +```Python +item: Item +``` + +...& ⏮️ 👈 👁 📄 👆 🤚: + +* 👨‍🎨 🐕‍🦺, 🔌: + * 🛠️. + * 🆎 ✅. +* 🔬 💽: + * 🏧 & 🆑 ❌ 🕐❔ 📊 ❌. + * 🔬 🙇 🐦 🎻 🎚. +* 🛠️ 🔢 💽: 👟 ⚪️➡️ 🕸 🐍 💽 & 🆎. 👂 ⚪️➡️: + * 🎻. + * ➡ 🔢. + * 🔢 🔢. + * 🍪. + * 🎚. + * 📨. + * 📁. +* 🛠️ 🔢 📊: 🗜 ⚪️➡️ 🐍 💽 & 🆎 🕸 💽 (🎻): + * 🗜 🐍 🆎 (`str`, `int`, `float`, `bool`, `list`, ♒️). + * `datetime` 🎚. + * `UUID` 🎚. + * 💽 🏷. + * ...& 📚 🌖. +* 🏧 🎓 🛠️ 🧾, 🔌 2️⃣ 🎛 👩‍💻 🔢: + * 🦁 🎚. + * 📄. + +--- + +👟 🔙 ⏮️ 📟 🖼, **FastAPI** 🔜: + +* ✔ 👈 📤 `item_id` ➡ `GET` & `PUT` 📨. +* ✔ 👈 `item_id` 🆎 `int` `GET` & `PUT` 📨. + * 🚥 ⚫️ 🚫, 👩‍💻 🔜 👀 ⚠, 🆑 ❌. +* ✅ 🚥 📤 📦 🔢 🔢 📛 `q` ( `http://127.0.0.1:8000/items/foo?q=somequery`) `GET` 📨. + * `q` 🔢 📣 ⏮️ `= None`, ⚫️ 📦. + * 🍵 `None` ⚫️ 🔜 🚚 (💪 💼 ⏮️ `PUT`). +* `PUT` 📨 `/items/{item_id}`, ✍ 💪 🎻: + * ✅ 👈 ⚫️ ✔️ ✔ 🔢 `name` 👈 🔜 `str`. + * ✅ 👈 ⚫️ ✔️ ✔ 🔢 `price` 👈 ✔️ `float`. + * ✅ 👈 ⚫️ ✔️ 📦 🔢 `is_offer`, 👈 🔜 `bool`, 🚥 🎁. + * 🌐 👉 🔜 👷 🙇 🐦 🎻 🎚. +* 🗜 ⚪️➡️ & 🎻 🔁. +* 📄 🌐 ⏮️ 🗄, 👈 💪 ⚙️: + * 🎓 🧾 ⚙️. + * 🏧 👩‍💻 📟 ⚡ ⚙️, 📚 🇪🇸. +* 🚚 2️⃣ 🎓 🧾 🕸 🔢 🔗. + +--- + +👥 🖌 🧽, ✋️ 👆 ⏪ 🤚 💭 ❔ ⚫️ 🌐 👷. + +🔄 🔀 ⏸ ⏮️: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...⚪️➡️: + +```Python + ... "item_name": item.name ... +``` + +...: + +```Python + ... "item_price": item.price ... +``` + +...& 👀 ❔ 👆 👨‍🎨 🔜 🚘-🏁 🔢 & 💭 👫 🆎: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +🌅 🏁 🖼 🔌 🌅 ⚒, 👀 🔰 - 👩‍💻 🦮. + +**🚘 🚨**: 🔰 - 👩‍💻 🦮 🔌: + +* 📄 **🔢** ⚪️➡️ 🎏 🎏 🥉: **🎚**, **🍪**, **📨 🏑** & **📁**. +* ❔ ⚒ **🔬 ⚛** `maximum_length` ⚖️ `regex`. +* 📶 🏋️ & ⏩ ⚙️ **🔗 💉** ⚙️. +* 💂‍♂ & 🤝, ✅ 🐕‍🦺 **Oauth2️⃣** ⏮️ **🥙 🤝** & **🇺🇸🔍 🔰** 🔐. +* 🌅 🏧 (✋️ 😨 ⏩) ⚒ 📣 **🙇 🐦 🎻 🏷** (👏 Pydantic). +* **🕹** 🛠️ ⏮️ 🍓 & 🎏 🗃. +* 📚 ➕ ⚒ (👏 💃): + * ** *️⃣ ** + * 📶 ⏩ 💯 ⚓️ 🔛 🇸🇲 & `pytest` + * **⚜** + * **🍪 🎉** + * ...& 🌖. + +## 🎭 + +🔬 🇸🇲 📇 🎦 **FastAPI** 🈸 🏃‍♂ 🔽 Uvicorn 1️⃣ ⏩ 🐍 🛠️ 💪, 🕴 🔛 💃 & Uvicorn 👫 (⚙️ 🔘 FastAPI). (*) + +🤔 🌖 🔃 ⚫️, 👀 📄 📇. + +## 📦 🔗 + +⚙️ Pydantic: + +* ujson - ⏩ 🎻 "🎻". +* email_validator - 📧 🔬. + +⚙️ 💃: + +* httpx - ✔ 🚥 👆 💚 ⚙️ `TestClient`. +* jinja2 - ✔ 🚥 👆 💚 ⚙️ 🔢 📄 📳. +* python-multipart - ✔ 🚥 👆 💚 🐕‍🦺 📨 "✍", ⏮️ `request.form()`. +* itsdangerous - ✔ `SessionMiddleware` 🐕‍🦺. +* pyyaml - ✔ 💃 `SchemaGenerator` 🐕‍🦺 (👆 🎲 🚫 💪 ⚫️ ⏮️ FastAPI). +* ujson - ✔ 🚥 👆 💚 ⚙️ `UJSONResponse`. + +⚙️ FastAPI / 💃: + +* uvicorn - 💽 👈 📐 & 🍦 👆 🈸. +* orjson - ✔ 🚥 👆 💚 ⚙️ `ORJSONResponse`. + +👆 💪 ❎ 🌐 👫 ⏮️ `pip install "fastapi[all]"`. + +## 🛂 + +👉 🏗 ® 🔽 ⚖ 🇩🇪 🛂. diff --git a/docs/em/docs/project-generation.md b/docs/em/docs/project-generation.md new file mode 100644 index 0000000000..ae959e1d5c --- /dev/null +++ b/docs/em/docs/project-generation.md @@ -0,0 +1,84 @@ +# 🏗 ⚡ - 📄 + +👆 💪 ⚙️ 🏗 🚂 🤚 ▶️, ⚫️ 🔌 📚 ▶️ ⚒ 🆙, 💂‍♂, 💽 & 🛠️ 🔗 ⏪ ⌛ 👆. + +🏗 🚂 🔜 🕧 ✔️ 📶 🙃 🖥 👈 👆 🔜 ℹ & 🛠️ 👆 👍 💪, ✋️ ⚫️ 💪 👍 ▶️ ☝ 👆 🏗. + +## 🌕 📚 FastAPI ✳ + +📂: https://github.com/tiangolo/full-stack-fastapi-postgresql + +### 🌕 📚 FastAPI ✳ - ⚒ + +* 🌕 **☁** 🛠️ (☁ 🧢). +* ☁ 🐝 📳 🛠️. +* **☁ ✍** 🛠️ & 🛠️ 🇧🇿 🛠️. +* **🏭 🔜** 🐍 🕸 💽 ⚙️ Uvicorn & 🐁. +* 🐍 **FastAPI** 👩‍💻: + * **⏩**: 📶 ↕ 🎭, 🔛 🇷🇪 ⏮️ **✳** & **🚶** (👏 💃 & Pydantic). + * **🏋️**: 👑 👨‍🎨 🐕‍🦺. 🛠️ 🌐. 🌘 🕰 🛠️. + * **⏩**: 🔧 ⏩ ⚙️ & 💡. 🌘 🕰 👂 🩺. + * **📏**: 📉 📟 ❎. 💗 ⚒ ⚪️➡️ 🔠 🔢 📄. + * **🏋️**: 🤚 🏭-🔜 📟. ⏮️ 🏧 🎓 🧾. + * **🐩-⚓️**: ⚓️ 🔛 (& 🍕 🔗 ⏮️) 📂 🐩 🔗: 🗄 & 🎻 🔗. + * **📚 🎏 ⚒** 🔌 🏧 🔬, 🛠️, 🎓 🧾, 🤝 ⏮️ Oauth2️⃣ 🥙 🤝, ♒️. +* **🔐 🔐** 🔁 🔢. +* **🥙 🤝** 🤝. +* **🇸🇲** 🏷 (🔬 🏺 ↔, 👫 💪 ⚙️ ⏮️ 🥒 👨‍🏭 🔗). +* 🔰 ▶️ 🏷 👩‍💻 (🔀 & ❎ 👆 💪). +* **⚗** 🛠️. +* **⚜** (✖️ 🇨🇳 ℹ 🤝). +* **🥒** 👨‍🏭 👈 💪 🗄 & ⚙️ 🏷 & 📟 ⚪️➡️ 🎂 👩‍💻 🍕. +* 🎂 👩‍💻 💯 ⚓️ 🔛 **✳**, 🛠️ ⏮️ ☁, 👆 💪 💯 🌕 🛠️ 🔗, 🔬 🔛 💽. ⚫️ 🏃 ☁, ⚫️ 💪 🏗 🆕 💽 🏪 ⚪️➡️ 🖌 🔠 🕰 (👆 💪 ⚙️ ✳, ✳, ✳, ⚖️ ⚫️❔ 👆 💚, & 💯 👈 🛠️ 👷). +* ⏩ 🐍 🛠️ ⏮️ **📂 💾** 🛰 ⚖️-☁ 🛠️ ⏮️ ↔ 💖 ⚛ ⚗ ⚖️ 🎙 🎙 📟 📂. +* **🎦** 🕸: + * 🏗 ⏮️ 🎦 ✳. + * **🥙 🤝** 🚚. + * 💳 🎑. + * ⏮️ 💳, 👑 🕹 🎑. + * 👑 🕹 ⏮️ 👩‍💻 🏗 & 📕. + * 👤 👩‍💻 📕. + * **🇷🇪**. + * **🎦-📻**. + * **Vuetify** 🌹 🧽 🔧 🦲. + * **📕**. + * ☁ 💽 ⚓️ 🔛 **👌** (📶 🤾 🎆 ⏮️ 🎦-📻). + * ☁ 👁-▶️ 🏗, 👆 🚫 💪 🖊 ⚖️ 💕 ✍ 📟. + * 🕸 💯 🏃 🏗 🕰 (💪 🔕 💁‍♂️). + * ⚒ 🔧 💪, ⚫️ 👷 👅 📦, ✋️ 👆 💪 🏤-🏗 ⏮️ 🎦 ✳ ⚖️ ✍ ⚫️ 👆 💪, & 🏤-⚙️ ⚫️❔ 👆 💚. +* ** *️⃣ ** ✳ 💽, 👆 💪 🔀 ⚫️ ⚙️ 📁 & ✳ 💪. +* **🥀** 🥒 👨‍🏭 ⚖. +* 📐 ⚖ 🖖 🕸 & 👩‍💻 ⏮️ **Traefik**, 👆 💪 ✔️ 👯‍♂️ 🔽 🎏 🆔, 👽 ➡, ✋️ 🍦 🎏 📦. +* Traefik 🛠️, ✅ ➡️ 🗜 **🇺🇸🔍** 📄 🏧 ⚡. +* ✳ **🆑** (🔁 🛠️), 🔌 🕸 & 👩‍💻 🔬. + +## 🌕 📚 FastAPI 🗄 + +📂: https://github.com/tiangolo/full-stack-fastapi-couchbase + +👶 👶 **⚠** 👶 👶 + +🚥 👆 ▶️ 🆕 🏗 ⚪️➡️ 🖌, ✅ 🎛 📥. + +🖼, 🏗 🚂 🌕 📚 FastAPI ✳ 💪 👍 🎛, ⚫️ 🎯 🚧 & ⚙️. & ⚫️ 🔌 🌐 🆕 ⚒ & 📈. + +👆 🆓 ⚙️ 🗄-⚓️ 🚂 🚥 👆 💚, ⚫️ 🔜 🎲 👷 👌, & 🚥 👆 ⏪ ✔️ 🏗 🏗 ⏮️ ⚫️ 👈 👌 👍 (& 👆 🎲 ⏪ ℹ ⚫️ ♣ 👆 💪). + +👆 💪 ✍ 🌅 🔃 ⚫️ 🩺 🏦. + +## 🌕 📚 FastAPI ✳ + +...💪 👟 ⏪, ⚓️ 🔛 👇 🕰 🚚 & 🎏 ⚖. 👶 👶 + +## 🎰 🏫 🏷 ⏮️ 🌈 & FastAPI + +📂: https://github.com/microsoft/cookiecutter-spacy-fastapi + +### 🎰 🏫 🏷 ⏮️ 🌈 & FastAPI - ⚒ + +* **🌈** 🕜 🏷 🛠️. +* **☁ 🧠 🔎** 📨 📁 🏗. +* **🏭 🔜** 🐍 🕸 💽 ⚙️ Uvicorn & 🐁. +* **☁ 👩‍💻** Kubernetes (🦲) 🆑/💿 🛠️ 🏗. +* **🤸‍♂** 💪 ⚒ 1️⃣ 🌈 🏗 🇪🇸 ⏮️ 🏗 🖥. +* **💪 🏧** 🎏 🏷 🛠️ (Pytorch, 🇸🇲), 🚫 🌈. diff --git a/docs/em/docs/python-types.md b/docs/em/docs/python-types.md new file mode 100644 index 0000000000..e079d9039d --- /dev/null +++ b/docs/em/docs/python-types.md @@ -0,0 +1,490 @@ +# 🐍 🆎 🎶 + +🐍 ✔️ 🐕‍🦺 📦 "🆎 🔑". + +👫 **"🆎 🔑"** 🎁 ❕ 👈 ✔ 📣 🆎 🔢. + +📣 🆎 👆 🔢, 👨‍🎨 & 🧰 💪 🤝 👆 👍 🐕‍🦺. + +👉 **⏩ 🔰 / ↗️** 🔃 🐍 🆎 🔑. ⚫️ 📔 🕴 💯 💪 ⚙️ 👫 ⏮️ **FastAPI**... ❔ 🤙 📶 🐥. + +**FastAPI** 🌐 ⚓️ 🔛 👫 🆎 🔑, 👫 🤝 ⚫️ 📚 📈 & 💰. + +✋️ 🚥 👆 🙅 ⚙️ **FastAPI**, 👆 🔜 💰 ⚪️➡️ 🏫 🍖 🔃 👫. + +!!! note + 🚥 👆 🐍 🕴, & 👆 ⏪ 💭 🌐 🔃 🆎 🔑, 🚶 ⏭ 📃. + +## 🎯 + +➡️ ▶️ ⏮️ 🙅 🖼: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +🤙 👉 📋 🔢: + +``` +John Doe +``` + +🔢 🔨 📄: + +* ✊ `first_name` & `last_name`. +* 🗜 🥇 🔤 🔠 1️⃣ ↖ 💼 ⏮️ `title()`. +* 🔢 👫 ⏮️ 🚀 🖕. + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### ✍ ⚫️ + +⚫️ 📶 🙅 📋. + +✋️ 🔜 🌈 👈 👆 ✍ ⚫️ ⚪️➡️ 🖌. + +☝ 👆 🔜 ✔️ ▶️ 🔑 🔢, 👆 ✔️ 🔢 🔜... + +✋️ ⤴️ 👆 ✔️ 🤙 "👈 👩‍🔬 👈 🗜 🥇 🔤 ↖ 💼". + +⚫️ `upper`❓ ⚫️ `uppercase`❓ `first_uppercase`❓ `capitalize`❓ + +⤴️, 👆 🔄 ⏮️ 🗝 👩‍💻 👨‍👧‍👦, 👨‍🎨 ✍. + +👆 🆎 🥇 🔢 🔢, `first_name`, ⤴️ ❣ (`.`) & ⤴️ 🎯 `Ctrl+Space` ⏲ 🛠️. + +✋️, 😞, 👆 🤚 🕳 ⚠: + + + +### 🚮 🆎 + +➡️ 🔀 👁 ⏸ ⚪️➡️ ⏮️ ⏬. + +👥 🔜 🔀 ⚫️❔ 👉 🧬, 🔢 🔢, ⚪️➡️: + +```Python + first_name, last_name +``` + +: + +```Python + first_name: str, last_name: str +``` + +👈 ⚫️. + +👈 "🆎 🔑": + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +👈 🚫 🎏 📣 🔢 💲 💖 🔜 ⏮️: + +```Python + first_name="john", last_name="doe" +``` + +⚫️ 🎏 👜. + +👥 ⚙️ ❤ (`:`), 🚫 🌓 (`=`). + +& ❎ 🆎 🔑 🛎 🚫 🔀 ⚫️❔ 🔨 ⚪️➡️ ⚫️❔ 🔜 🔨 🍵 👫. + +✋️ 🔜, 🌈 👆 🔄 🖕 🏗 👈 🔢, ✋️ ⏮️ 🆎 🔑. + +🎏 ☝, 👆 🔄 ⏲ 📋 ⏮️ `Ctrl+Space` & 👆 👀: + + + +⏮️ 👈, 👆 💪 📜, 👀 🎛, ⏭ 👆 🔎 1️⃣ 👈 "💍 🔔": + + + +## 🌅 🎯 + +✅ 👉 🔢, ⚫️ ⏪ ✔️ 🆎 🔑: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +↩️ 👨‍🎨 💭 🆎 🔢, 👆 🚫 🕴 🤚 🛠️, 👆 🤚 ❌ ✅: + + + +🔜 👆 💭 👈 👆 ✔️ 🔧 ⚫️, 🗜 `age` 🎻 ⏮️ `str(age)`: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## 📣 🆎 + +👆 👀 👑 🥉 📣 🆎 🔑. 🔢 🔢. + +👉 👑 🥉 👆 🔜 ⚙️ 👫 ⏮️ **FastAPI**. + +### 🙅 🆎 + +👆 💪 📣 🌐 🐩 🐍 🆎, 🚫 🕴 `str`. + +👆 💪 ⚙️, 🖼: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### 💊 🆎 ⏮️ 🆎 🔢 + +📤 📊 📊 👈 💪 🔌 🎏 💲, 💖 `dict`, `list`, `set` & `tuple`. & 🔗 💲 💪 ✔️ 👫 👍 🆎 💁‍♂️. + +👉 🆎 👈 ✔️ 🔗 🆎 🤙 "**💊**" 🆎. & ⚫️ 💪 📣 👫, ⏮️ 👫 🔗 🆎. + +📣 👈 🆎 & 🔗 🆎, 👆 💪 ⚙️ 🐩 🐍 🕹 `typing`. ⚫️ 🔀 🎯 🐕‍🦺 👫 🆎 🔑. + +#### 🆕 ⏬ 🐍 + +❕ ⚙️ `typing` **🔗** ⏮️ 🌐 ⏬, ⚪️➡️ 🐍 3️⃣.6️⃣ ⏪ 🕐, ✅ 🐍 3️⃣.9️⃣, 🐍 3️⃣.1️⃣0️⃣, ♒️. + +🐍 🏧, **🆕 ⏬** 👟 ⏮️ 📉 🐕‍🦺 👉 🆎 ✍ & 📚 💼 👆 🏆 🚫 💪 🗄 & ⚙️ `typing` 🕹 📣 🆎 ✍. + +🚥 👆 💪 ⚒ 🌖 ⏮️ ⏬ 🐍 👆 🏗, 👆 🔜 💪 ✊ 📈 👈 ➕ 🦁. 👀 🖼 🔛. + +#### 📇 + +🖼, ➡️ 🔬 🔢 `list` `str`. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ⚪️➡️ `typing`, 🗄 `List` (⏮️ 🔠 `L`): + + ``` Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + + 📣 🔢, ⏮️ 🎏 ❤ (`:`) ❕. + + 🆎, 🚮 `List` 👈 👆 🗄 ⚪️➡️ `typing`. + + 📇 🆎 👈 🔌 🔗 🆎, 👆 🚮 👫 ⬜ 🗜: + + ```Python hl_lines="4" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + 📣 🔢, ⏮️ 🎏 ❤ (`:`) ❕. + + 🆎, 🚮 `list`. + + 📇 🆎 👈 🔌 🔗 🆎, 👆 🚮 👫 ⬜ 🗜: + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006_py39.py!} + ``` + +!!! info + 👈 🔗 🆎 ⬜ 🗜 🤙 "🆎 🔢". + + 👉 💼, `str` 🆎 🔢 🚶‍♀️ `List` (⚖️ `list` 🐍 3️⃣.9️⃣ & 🔛). + +👈 ⛓: "🔢 `items` `list`, & 🔠 🏬 👉 📇 `str`". + +!!! tip + 🚥 👆 ⚙️ 🐍 3️⃣.9️⃣ ⚖️ 🔛, 👆 🚫 ✔️ 🗄 `List` ⚪️➡️ `typing`, 👆 💪 ⚙️ 🎏 🥔 `list` 🆎 ↩️. + +🔨 👈, 👆 👨‍🎨 💪 🚚 🐕‍🦺 ⏪ 🏭 🏬 ⚪️➡️ 📇: + + + +🍵 🆎, 👈 🌖 💪 🏆. + +👀 👈 🔢 `item` 1️⃣ 🔣 📇 `items`. + +& , 👨‍🎨 💭 ⚫️ `str`, & 🚚 🐕‍🦺 👈. + +#### 🔢 & ⚒ + +👆 🔜 🎏 📣 `tuple`Ⓜ & `set`Ⓜ: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial007.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial007_py39.py!} + ``` + +👉 ⛓: + +* 🔢 `items_t` `tuple` ⏮️ 3️⃣ 🏬, `int`, ➕1️⃣ `int`, & `str`. +* 🔢 `items_s` `set`, & 🔠 🚮 🏬 🆎 `bytes`. + +#### #️⃣ + +🔬 `dict`, 👆 🚶‍♀️ 2️⃣ 🆎 🔢, 🎏 ❕. + +🥇 🆎 🔢 🔑 `dict`. + +🥈 🆎 🔢 💲 `dict`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008_py39.py!} + ``` + +👉 ⛓: + +* 🔢 `prices` `dict`: + * 🔑 👉 `dict` 🆎 `str` (➡️ 💬, 📛 🔠 🏬). + * 💲 👉 `dict` 🆎 `float` (➡️ 💬, 🔖 🔠 🏬). + +#### 🇪🇺 + +👆 💪 📣 👈 🔢 💪 🙆 **📚 🆎**, 🖼, `int` ⚖️ `str`. + +🐍 3️⃣.6️⃣ & 🔛 (✅ 🐍 3️⃣.1️⃣0️⃣) 👆 💪 ⚙️ `Union` 🆎 ⚪️➡️ `typing` & 🚮 🔘 ⬜ 🗜 💪 🆎 🚫. + +🐍 3️⃣.1️⃣0️⃣ 📤 **🎛 ❕** 🌐❔ 👆 💪 🚮 💪 🆎 👽 ⏸ ⏸ (`|`). + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008b.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008b_py310.py!} + ``` + +👯‍♂️ 💼 👉 ⛓ 👈 `item` 💪 `int` ⚖️ `str`. + +#### 🎲 `None` + +👆 💪 📣 👈 💲 💪 ✔️ 🆎, 💖 `str`, ✋️ 👈 ⚫️ 💪 `None`. + +🐍 3️⃣.6️⃣ & 🔛 (✅ 🐍 3️⃣.1️⃣0️⃣) 👆 💪 📣 ⚫️ 🏭 & ⚙️ `Optional` ⚪️➡️ `typing` 🕹. + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +⚙️ `Optional[str]` ↩️ `str` 🔜 ➡️ 👨‍🎨 ℹ 👆 🔍 ❌ 🌐❔ 👆 💪 🤔 👈 💲 🕧 `str`, 🕐❔ ⚫️ 💪 🤙 `None` 💁‍♂️. + +`Optional[Something]` 🤙 ⌨ `Union[Something, None]`, 👫 🌓. + +👉 ⛓ 👈 🐍 3️⃣.1️⃣0️⃣, 👆 💪 ⚙️ `Something | None`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009.py!} + ``` + +=== "🐍 3️⃣.6️⃣ & 🔛 - 🎛" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009b.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial009_py310.py!} + ``` + +#### ⚙️ `Union` ⚖️ `Optional` + +🚥 👆 ⚙️ 🐍 ⏬ 🔛 3️⃣.1️⃣0️⃣, 📥 💁‍♂ ⚪️➡️ 👇 📶 **🤔** ☝ 🎑: + +* 👶 ❎ ⚙️ `Optional[SomeType]` +* ↩️ 👶 **⚙️ `Union[SomeType, None]`** 👶. + +👯‍♂️ 🌓 & 🔘 👫 🎏, ✋️ 👤 🔜 👍 `Union` ↩️ `Optional` ↩️ 🔤 "**📦**" 🔜 😑 🔑 👈 💲 📦, & ⚫️ 🤙 ⛓ "⚫️ 💪 `None`", 🚥 ⚫️ 🚫 📦 & ✔. + +👤 💭 `Union[SomeType, None]` 🌖 🔑 🔃 ⚫️❔ ⚫️ ⛓. + +⚫️ 🔃 🔤 & 📛. ✋️ 👈 🔤 💪 📉 ❔ 👆 & 👆 🤽‍♂ 💭 🔃 📟. + +🖼, ➡️ ✊ 👉 🔢: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c.py!} +``` + +🔢 `name` 🔬 `Optional[str]`, ✋️ ⚫️ **🚫 📦**, 👆 🚫🔜 🤙 🔢 🍵 🔢: + +```Python +say_hi() # Oh, no, this throws an error! 😱 +``` + +`name` 🔢 **✔** (🚫 *📦*) ↩️ ⚫️ 🚫 ✔️ 🔢 💲. , `name` 🚫 `None` 💲: + +```Python +say_hi(name=None) # This works, None is valid 🎉 +``` + +👍 📰, 🕐 👆 🔛 🐍 3️⃣.1️⃣0️⃣ 👆 🏆 🚫 ✔️ 😟 🔃 👈, 👆 🔜 💪 🎯 ⚙️ `|` 🔬 🇪🇺 🆎: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c_py310.py!} +``` + +& ⤴️ 👆 🏆 🚫 ✔️ 😟 🔃 📛 💖 `Optional` & `Union`. 👶 + +#### 💊 🆎 + +👉 🆎 👈 ✊ 🆎 🔢 ⬜ 🗜 🤙 **💊 🆎** ⚖️ **💊**, 🖼: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + * `List` + * `Tuple` + * `Set` + * `Dict` + * `Union` + * `Optional` + * ...& 🎏. + +=== "🐍 3️⃣.9️⃣ & 🔛" + + 👆 💪 ⚙️ 🎏 💽 🆎 💊 (⏮️ ⬜ 🗜 & 🆎 🔘): + + * `list` + * `tuple` + * `set` + * `dict` + + & 🎏 ⏮️ 🐍 3️⃣.6️⃣, ⚪️➡️ `typing` 🕹: + + * `Union` + * `Optional` + * ...& 🎏. + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + 👆 💪 ⚙️ 🎏 💽 🆎 💊 (⏮️ ⬜ 🗜 & 🆎 🔘): + + * `list` + * `tuple` + * `set` + * `dict` + + & 🎏 ⏮️ 🐍 3️⃣.6️⃣, ⚪️➡️ `typing` 🕹: + + * `Union` + * `Optional` (🎏 ⏮️ 🐍 3️⃣.6️⃣) + * ...& 🎏. + + 🐍 3️⃣.1️⃣0️⃣, 🎛 ⚙️ 💊 `Union` & `Optional`, 👆 💪 ⚙️ ⏸ ⏸ (`|`) 📣 🇪🇺 🆎. + +### 🎓 🆎 + +👆 💪 📣 🎓 🆎 🔢. + +➡️ 💬 👆 ✔️ 🎓 `Person`, ⏮️ 📛: + +```Python hl_lines="1-3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +⤴️ 👆 💪 📣 🔢 🆎 `Person`: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +& ⤴️, 🔄, 👆 🤚 🌐 👨‍🎨 🐕‍🦺: + + + +## Pydantic 🏷 + +Pydantic 🐍 🗃 🎭 📊 🔬. + +👆 📣 "💠" 💽 🎓 ⏮️ 🔢. + +& 🔠 🔢 ✔️ 🆎. + +⤴️ 👆 ✍ 👐 👈 🎓 ⏮️ 💲 & ⚫️ 🔜 ✔ 💲, 🗜 👫 ☑ 🆎 (🚥 👈 💼) & 🤝 👆 🎚 ⏮️ 🌐 💽. + +& 👆 🤚 🌐 👨‍🎨 🐕‍🦺 ⏮️ 👈 📉 🎚. + +🖼 ⚪️➡️ 🛂 Pydantic 🩺: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/python_types/tutorial011.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py310.py!} + ``` + +!!! info + 💡 🌖 🔃 Pydantic, ✅ 🚮 🩺. + +**FastAPI** 🌐 ⚓️ 🔛 Pydantic. + +👆 🔜 👀 📚 🌅 🌐 👉 💡 [🔰 - 👩‍💻 🦮](tutorial/index.md){.internal-link target=_blank}. + +!!! tip + Pydantic ✔️ 🎁 🎭 🕐❔ 👆 ⚙️ `Optional` ⚖️ `Union[Something, None]` 🍵 🔢 💲, 👆 💪 ✍ 🌅 🔃 ⚫️ Pydantic 🩺 🔃 ✔ 📦 🏑. + +## 🆎 🔑 **FastAPI** + +**FastAPI** ✊ 📈 👫 🆎 🔑 📚 👜. + +⏮️ **FastAPI** 👆 📣 🔢 ⏮️ 🆎 🔑 & 👆 🤚: + +* **👨‍🎨 🐕‍🦺**. +* **🆎 ✅**. + +...and **FastAPI** uses the same declarations : + +* **🔬 📄**: ⚪️➡️ 📨 ➡ 🔢, 🔢 🔢, 🎚, 💪, 🔗, ♒️. +* **🗜 💽**: ⚪️➡️ 📨 🚚 🆎. +* **✔ 💽**: 👟 ⚪️➡️ 🔠 📨: + * 🏭 **🏧 ❌** 📨 👩‍💻 🕐❔ 📊 ❌. +* **📄** 🛠️ ⚙️ 🗄: + * ❔ ⤴️ ⚙️ 🏧 🎓 🧾 👩‍💻 🔢. + +👉 5️⃣📆 🌐 🔊 📝. 🚫 😟. 👆 🔜 👀 🌐 👉 🎯 [🔰 - 👩‍💻 🦮](tutorial/index.md){.internal-link target=_blank}. + +⚠ 👜 👈 ⚙️ 🐩 🐍 🆎, 👁 🥉 (↩️ ❎ 🌖 🎓, 👨‍🎨, ♒️), **FastAPI** 🔜 📚 👷 👆. + +!!! info + 🚥 👆 ⏪ 🚶 🔘 🌐 🔰 & 👟 🔙 👀 🌅 🔃 🆎, 👍 ℹ "🎮 🎼" ⚪️➡️ `mypy`. diff --git a/docs/em/docs/tutorial/background-tasks.md b/docs/em/docs/tutorial/background-tasks.md new file mode 100644 index 0000000000..e28ead4155 --- /dev/null +++ b/docs/em/docs/tutorial/background-tasks.md @@ -0,0 +1,102 @@ +# 🖥 📋 + +👆 💪 🔬 🖥 📋 🏃 *⏮️* 🛬 📨. + +👉 ⚠ 🛠️ 👈 💪 🔨 ⏮️ 📨, ✋️ 👈 👩‍💻 🚫 🤙 ✔️ ⌛ 🛠️ 🏁 ⏭ 📨 📨. + +👉 🔌, 🖼: + +* 📧 📨 📨 ⏮️ 🎭 🎯: + * 🔗 📧 💽 & 📨 📧 😑 "🐌" (📚 🥈), 👆 💪 📨 📨 ▶️️ ↖️ & 📨 📧 📨 🖥. +* 🏭 💽: + * 🖼, ➡️ 💬 👆 📨 📁 👈 🔜 🚶 🔘 🐌 🛠️, 👆 💪 📨 📨 "🚫" (🇺🇸🔍 2️⃣0️⃣2️⃣) & 🛠️ ⚫️ 🖥. + +## ⚙️ `BackgroundTasks` + +🥇, 🗄 `BackgroundTasks` & 🔬 🔢 👆 *➡ 🛠️ 🔢* ⏮️ 🆎 📄 `BackgroundTasks`: + +```Python hl_lines="1 13" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +**FastAPI** 🔜 ✍ 🎚 🆎 `BackgroundTasks` 👆 & 🚶‍♀️ ⚫️ 👈 🔢. + +## ✍ 📋 🔢 + +✍ 🔢 🏃 🖥 📋. + +⚫️ 🐩 🔢 👈 💪 📨 🔢. + +⚫️ 💪 `async def` ⚖️ 😐 `def` 🔢, **FastAPI** 🔜 💭 ❔ 🍵 ⚫️ ☑. + +👉 💼, 📋 🔢 🔜 ✍ 📁 (⚖ 📨 📧). + +& ✍ 🛠️ 🚫 ⚙️ `async` & `await`, 👥 🔬 🔢 ⏮️ 😐 `def`: + +```Python hl_lines="6-9" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +## 🚮 🖥 📋 + +🔘 👆 *➡ 🛠️ 🔢*, 🚶‍♀️ 👆 📋 🔢 *🖥 📋* 🎚 ⏮️ 👩‍🔬 `.add_task()`: + +```Python hl_lines="14" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +`.add_task()` 📨 ❌: + +* 📋 🔢 🏃 🖥 (`write_notification`). +* 🙆 🔁 ❌ 👈 🔜 🚶‍♀️ 📋 🔢 ✔ (`email`). +* 🙆 🇨🇻 ❌ 👈 🔜 🚶‍♀️ 📋 🔢 (`message="some notification"`). + +## 🔗 💉 + +⚙️ `BackgroundTasks` 👷 ⏮️ 🔗 💉 ⚙️, 👆 💪 📣 🔢 🆎 `BackgroundTasks` 💗 🎚: *➡ 🛠️ 🔢*, 🔗 (☑), 🎧-🔗, ♒️. + +**FastAPI** 💭 ⚫️❔ 🔠 💼 & ❔ 🏤-⚙️ 🎏 🎚, 👈 🌐 🖥 📋 🔗 👯‍♂️ & 🏃 🖥 ⏮️: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="11 13 20 23" + {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} + ``` + +👉 🖼, 📧 🔜 ✍ `log.txt` 📁 *⏮️* 📨 📨. + +🚥 📤 🔢 📨, ⚫️ 🔜 ✍ 🕹 🖥 📋. + +& ⤴️ ➕1️⃣ 🖥 📋 🏗 *➡ 🛠️ 🔢* 🔜 ✍ 📧 ⚙️ `email` ➡ 🔢. + +## 📡 ℹ + +🎓 `BackgroundTasks` 👟 🔗 ⚪️➡️ `starlette.background`. + +⚫️ 🗄/🔌 🔗 🔘 FastAPI 👈 👆 💪 🗄 ⚫️ ⚪️➡️ `fastapi` & ❎ 😫 🗄 🎛 `BackgroundTask` (🍵 `s` 🔚) ⚪️➡️ `starlette.background`. + +🕴 ⚙️ `BackgroundTasks` (& 🚫 `BackgroundTask`), ⚫️ ⤴️ 💪 ⚙️ ⚫️ *➡ 🛠️ 🔢* 🔢 & ✔️ **FastAPI** 🍵 🎂 👆, 💖 🕐❔ ⚙️ `Request` 🎚 🔗. + +⚫️ 💪 ⚙️ `BackgroundTask` 😞 FastAPI, ✋️ 👆 ✔️ ✍ 🎚 👆 📟 & 📨 💃 `Response` 🔌 ⚫️. + +👆 💪 👀 🌖 ℹ 💃 🛂 🩺 🖥 📋. + +## ⚠ + +🚥 👆 💪 🎭 🏋️ 🖥 📊 & 👆 🚫 🎯 💪 ⚫️ 🏃 🎏 🛠️ (🖼, 👆 🚫 💪 💰 💾, 🔢, ♒️), 👆 💪 💰 ⚪️➡️ ⚙️ 🎏 🦏 🧰 💖 🥒. + +👫 😑 🚚 🌖 🏗 📳, 📧/👨‍🏭 📤 👨‍💼, 💖 ✳ ⚖️ ✳, ✋️ 👫 ✔ 👆 🏃 🖥 📋 💗 🛠️, & ✴️, 💗 💽. + +👀 🖼, ✅ [🏗 🚂](../project-generation.md){.internal-link target=_blank}, 👫 🌐 🔌 🥒 ⏪ 📶. + +✋️ 🚥 👆 💪 🔐 🔢 & 🎚 ⚪️➡️ 🎏 **FastAPI** 📱, ⚖️ 👆 💪 🎭 🤪 🖥 📋 (💖 📨 📧 📨), 👆 💪 🎯 ⚙️ `BackgroundTasks`. + +## 🌃 + +🗄 & ⚙️ `BackgroundTasks` ⏮️ 🔢 *➡ 🛠️ 🔢* & 🔗 🚮 🖥 📋. diff --git a/docs/em/docs/tutorial/bigger-applications.md b/docs/em/docs/tutorial/bigger-applications.md new file mode 100644 index 0000000000..c30bba106a --- /dev/null +++ b/docs/em/docs/tutorial/bigger-applications.md @@ -0,0 +1,488 @@ +# 🦏 🈸 - 💗 📁 + +🚥 👆 🏗 🈸 ⚖️ 🕸 🛠️, ⚫️ 🛎 💼 👈 👆 💪 🚮 🌐 🔛 👁 📁. + +**FastAPI** 🚚 🏪 🧰 📊 👆 🈸 ⏪ 🚧 🌐 💪. + +!!! info + 🚥 👆 👟 ⚪️➡️ 🏺, 👉 🔜 🌓 🏺 📗. + +## 🖼 📁 📊 + +➡️ 💬 👆 ✔️ 📁 📊 💖 👉: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   ├── dependencies.py +│   └── routers +│   │ ├── __init__.py +│   │ ├── items.py +│   │ └── users.py +│   └── internal +│   ├── __init__.py +│   └── admin.py +``` + +!!! tip + 📤 📚 `__init__.py` 📁: 1️⃣ 🔠 📁 ⚖️ 📁. + + 👉 ⚫️❔ ✔ 🏭 📟 ⚪️➡️ 1️⃣ 📁 🔘 ➕1️⃣. + + 🖼, `app/main.py` 👆 💪 ✔️ ⏸ 💖: + + ``` + from app.routers import items + ``` + +* `app` 📁 🔌 🌐. & ⚫️ ✔️ 🛁 📁 `app/__init__.py`, ⚫️ "🐍 📦" (🗃 "🐍 🕹"): `app`. +* ⚫️ 🔌 `app/main.py` 📁. ⚫️ 🔘 🐍 📦 (📁 ⏮️ 📁 `__init__.py`), ⚫️ "🕹" 👈 📦: `app.main`. +* 📤 `app/dependencies.py` 📁, 💖 `app/main.py`, ⚫️ "🕹": `app.dependencies`. +* 📤 📁 `app/routers/` ⏮️ ➕1️⃣ 📁 `__init__.py`, ⚫️ "🐍 📦": `app.routers`. +* 📁 `app/routers/items.py` 🔘 📦, `app/routers/`,, ⚫️ 🔁: `app.routers.items`. +* 🎏 ⏮️ `app/routers/users.py`, ⚫️ ➕1️⃣ 🔁: `app.routers.users`. +* 📤 📁 `app/internal/` ⏮️ ➕1️⃣ 📁 `__init__.py`, ⚫️ ➕1️⃣ "🐍 📦": `app.internal`. +* & 📁 `app/internal/admin.py` ➕1️⃣ 🔁: `app.internal.admin`. + + + +🎏 📁 📊 ⏮️ 🏤: + +``` +. +├── app # "app" is a Python package +│   ├── __init__.py # this file makes "app" a "Python package" +│   ├── main.py # "main" module, e.g. import app.main +│   ├── dependencies.py # "dependencies" module, e.g. import app.dependencies +│   └── routers # "routers" is a "Python subpackage" +│   │ ├── __init__.py # makes "routers" a "Python subpackage" +│   │ ├── items.py # "items" submodule, e.g. import app.routers.items +│   │ └── users.py # "users" submodule, e.g. import app.routers.users +│   └── internal # "internal" is a "Python subpackage" +│   ├── __init__.py # makes "internal" a "Python subpackage" +│   └── admin.py # "admin" submodule, e.g. import app.internal.admin +``` + +## `APIRouter` + +➡️ 💬 📁 💡 🚚 👩‍💻 🔁 `/app/routers/users.py`. + +👆 💚 ✔️ *➡ 🛠️* 🔗 👆 👩‍💻 👽 ⚪️➡️ 🎂 📟, 🚧 ⚫️ 🏗. + +✋️ ⚫️ 🍕 🎏 **FastAPI** 🈸/🕸 🛠️ (⚫️ 🍕 🎏 "🐍 📦"). + +👆 💪 ✍ *➡ 🛠️* 👈 🕹 ⚙️ `APIRouter`. + +### 🗄 `APIRouter` + +👆 🗄 ⚫️ & ✍ "👐" 🎏 🌌 👆 🔜 ⏮️ 🎓 `FastAPI`: + +```Python hl_lines="1 3" title="app/routers/users.py" +{!../../../docs_src/bigger_applications/app/routers/users.py!} +``` + +### *➡ 🛠️* ⏮️ `APIRouter` + +& ⤴️ 👆 ⚙️ ⚫️ 📣 👆 *➡ 🛠️*. + +⚙️ ⚫️ 🎏 🌌 👆 🔜 ⚙️ `FastAPI` 🎓: + +```Python hl_lines="6 11 16" title="app/routers/users.py" +{!../../../docs_src/bigger_applications/app/routers/users.py!} +``` + +👆 💪 💭 `APIRouter` "🐩 `FastAPI`" 🎓. + +🌐 🎏 🎛 🐕‍🦺. + +🌐 🎏 `parameters`, `responses`, `dependencies`, `tags`, ♒️. + +!!! tip + 👉 🖼, 🔢 🤙 `router`, ✋️ 👆 💪 📛 ⚫️ 👐 👆 💚. + +👥 🔜 🔌 👉 `APIRouter` 👑 `FastAPI` 📱, ✋️ 🥇, ➡️ ✅ 🔗 & ➕1️⃣ `APIRouter`. + +## 🔗 + +👥 👀 👈 👥 🔜 💪 🔗 ⚙️ 📚 🥉 🈸. + +👥 🚮 👫 👫 👍 `dependencies` 🕹 (`app/dependencies.py`). + +👥 🔜 🔜 ⚙️ 🙅 🔗 ✍ 🛃 `X-Token` 🎚: + +```Python hl_lines="1 4-6" title="app/dependencies.py" +{!../../../docs_src/bigger_applications/app/dependencies.py!} +``` + +!!! tip + 👥 ⚙️ 💭 🎚 📉 👉 🖼. + + ✋️ 🎰 💼 👆 🔜 🤚 👍 🏁 ⚙️ 🛠️ [💂‍♂ 🚙](./security/index.md){.internal-link target=_blank}. + +## ➕1️⃣ 🕹 ⏮️ `APIRouter` + +➡️ 💬 👆 ✔️ 🔗 💡 🚚 "🏬" ⚪️➡️ 👆 🈸 🕹 `app/routers/items.py`. + +👆 ✔️ *➡ 🛠️* : + +* `/items/` +* `/items/{item_id}` + +⚫️ 🌐 🎏 📊 ⏮️ `app/routers/users.py`. + +✋️ 👥 💚 🙃 & 📉 📟 🍖. + +👥 💭 🌐 *➡ 🛠️* 👉 🕹 ✔️ 🎏: + +* ➡ `prefix`: `/items`. +* `tags`: (1️⃣ 🔖: `items`). +* ➕ `responses`. +* `dependencies`: 👫 🌐 💪 👈 `X-Token` 🔗 👥 ✍. + +, ↩️ ❎ 🌐 👈 🔠 *➡ 🛠️*, 👥 💪 🚮 ⚫️ `APIRouter`. + +```Python hl_lines="5-10 16 21" title="app/routers/items.py" +{!../../../docs_src/bigger_applications/app/routers/items.py!} +``` + +➡ 🔠 *➡ 🛠️* ✔️ ▶️ ⏮️ `/`, 💖: + +```Python hl_lines="1" +@router.get("/{item_id}") +async def read_item(item_id: str): + ... +``` + +...🔡 🔜 🚫 🔌 🏁 `/`. + +, 🔡 👉 💼 `/items`. + +👥 💪 🚮 📇 `tags` & ➕ `responses` 👈 🔜 ✔ 🌐 *➡ 🛠️* 🔌 👉 📻. + +& 👥 💪 🚮 📇 `dependencies` 👈 🔜 🚮 🌐 *➡ 🛠️* 📻 & 🔜 🛠️/❎ 🔠 📨 ⚒ 👫. + +!!! tip + 🗒 👈, 🌅 💖 [🔗 *➡ 🛠️ 👨‍🎨*](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, 🙅‍♂ 💲 🔜 🚶‍♀️ 👆 *➡ 🛠️ 🔢*. + +🔚 🏁 👈 🏬 ➡ 🔜: + +* `/items/` +* `/items/{item_id}` + +...👥 🎯. + +* 👫 🔜 ™ ⏮️ 📇 🔖 👈 🔌 👁 🎻 `"items"`. + * 👫 "🔖" ✴️ ⚠ 🏧 🎓 🧾 ⚙️ (⚙️ 🗄). +* 🌐 👫 🔜 🔌 🔁 `responses`. +* 🌐 👫 *➡ 🛠️* 🔜 ✔️ 📇 `dependencies` 🔬/🛠️ ⏭ 👫. + * 🚥 👆 📣 🔗 🎯 *➡ 🛠️*, **👫 🔜 🛠️ 💁‍♂️**. + * 📻 🔗 🛠️ 🥇, ⤴️ [`dependencies` 👨‍🎨](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, & ⤴️ 😐 🔢 🔗. + * 👆 💪 🚮 [`Security` 🔗 ⏮️ `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}. + +!!! tip + ✔️ `dependencies` `APIRouter` 💪 ⚙️, 🖼, 🚚 🤝 🎂 👪 *➡ 🛠️*. 🚥 🔗 🚫 🚮 📦 🔠 1️⃣ 👫. + +!!! check + `prefix`, `tags`, `responses`, & `dependencies` 🔢 (📚 🎏 💼) ⚒ ⚪️➡️ **FastAPI** ℹ 👆 ❎ 📟 ❎. + +### 🗄 🔗 + +👉 📟 👨‍❤‍👨 🕹 `app.routers.items`, 📁 `app/routers/items.py`. + +& 👥 💪 🤚 🔗 🔢 ⚪️➡️ 🕹 `app.dependencies`, 📁 `app/dependencies.py`. + +👥 ⚙️ ⚖ 🗄 ⏮️ `..` 🔗: + +```Python hl_lines="3" title="app/routers/items.py" +{!../../../docs_src/bigger_applications/app/routers/items.py!} +``` + +#### ❔ ⚖ 🗄 👷 + +!!! tip + 🚥 👆 💭 👌 ❔ 🗄 👷, 😣 ⏭ 📄 🔛. + +👁 ❣ `.`, 💖: + +```Python +from .dependencies import get_token_header +``` + +🔜 ⛓: + +* ▶️ 🎏 📦 👈 👉 🕹 (📁 `app/routers/items.py`) 🖖 (📁 `app/routers/`)... +* 🔎 🕹 `dependencies` (👽 📁 `app/routers/dependencies.py`)... +* & ⚪️➡️ ⚫️, 🗄 🔢 `get_token_header`. + +✋️ 👈 📁 🚫 🔀, 👆 🔗 📁 `app/dependencies.py`. + +💭 ❔ 👆 📱/📁 📊 👀 💖: + + + +--- + +2️⃣ ❣ `..`, 💖: + +```Python +from ..dependencies import get_token_header +``` + +⛓: + +* ▶️ 🎏 📦 👈 👉 🕹 (📁 `app/routers/items.py`) 🖖 (📁 `app/routers/`)... +* 🚶 👪 📦 (📁 `app/`)... +* & 📤, 🔎 🕹 `dependencies` (📁 `app/dependencies.py`)... +* & ⚪️➡️ ⚫️, 🗄 🔢 `get_token_header`. + +👈 👷 ☑ ❗ 👶 + +--- + +🎏 🌌, 🚥 👥 ✔️ ⚙️ 3️⃣ ❣ `...`, 💖: + +```Python +from ...dependencies import get_token_header +``` + +that 🔜 ⛓: + +* ▶️ 🎏 📦 👈 👉 🕹 (📁 `app/routers/items.py`) 🖖 (📁 `app/routers/`)... +* 🚶 👪 📦 (📁 `app/`)... +* ⤴️ 🚶 👪 👈 📦 (📤 🙅‍♂ 👪 📦, `app` 🔝 🎚 👶)... +* & 📤, 🔎 🕹 `dependencies` (📁 `app/dependencies.py`)... +* & ⚪️➡️ ⚫️, 🗄 🔢 `get_token_header`. + +👈 🔜 🔗 📦 🔛 `app/`, ⏮️ 🚮 👍 📁 `__init__.py`, ♒️. ✋️ 👥 🚫 ✔️ 👈. , 👈 🔜 🚮 ❌ 👆 🖼. 👶 + +✋️ 🔜 👆 💭 ❔ ⚫️ 👷, 👆 💪 ⚙️ ⚖ 🗄 👆 👍 📱 🙅‍♂ 🤔 ❔ 🏗 👫. 👶 + +### 🚮 🛃 `tags`, `responses`, & `dependencies` + +👥 🚫 ❎ 🔡 `/items` 🚫 `tags=["items"]` 🔠 *➡ 🛠️* ↩️ 👥 🚮 👫 `APIRouter`. + +✋️ 👥 💪 🚮 _🌅_ `tags` 👈 🔜 ✔ 🎯 *➡ 🛠️*, & ➕ `responses` 🎯 👈 *➡ 🛠️*: + +```Python hl_lines="30-31" title="app/routers/items.py" +{!../../../docs_src/bigger_applications/app/routers/items.py!} +``` + +!!! tip + 👉 🏁 ➡ 🛠️ 🔜 ✔️ 🌀 🔖: `["items", "custom"]`. + + & ⚫️ 🔜 ✔️ 👯‍♂️ 📨 🧾, 1️⃣ `404` & 1️⃣ `403`. + +## 👑 `FastAPI` + +🔜, ➡️ 👀 🕹 `app/main.py`. + +📥 🌐❔ 👆 🗄 & ⚙️ 🎓 `FastAPI`. + +👉 🔜 👑 📁 👆 🈸 👈 👔 🌐 👯‍♂️. + +& 🏆 👆 ⚛ 🔜 🔜 🖖 🚮 👍 🎯 🕹, 👑 📁 🔜 🙅. + +### 🗄 `FastAPI` + +👆 🗄 & ✍ `FastAPI` 🎓 🛎. + +& 👥 💪 📣 [🌐 🔗](dependencies/global-dependencies.md){.internal-link target=_blank} 👈 🔜 🌀 ⏮️ 🔗 🔠 `APIRouter`: + +```Python hl_lines="1 3 7" title="app/main.py" +{!../../../docs_src/bigger_applications/app/main.py!} +``` + +### 🗄 `APIRouter` + +🔜 👥 🗄 🎏 🔁 👈 ✔️ `APIRouter`Ⓜ: + +```Python hl_lines="5" title="app/main.py" +{!../../../docs_src/bigger_applications/app/main.py!} +``` + +📁 `app/routers/users.py` & `app/routers/items.py` 🔁 👈 🍕 🎏 🐍 📦 `app`, 👥 💪 ⚙️ 👁 ❣ `.` 🗄 👫 ⚙️ "⚖ 🗄". + +### ❔ 🏭 👷 + +📄: + +```Python +from .routers import items, users +``` + +⛓: + +* ▶️ 🎏 📦 👈 👉 🕹 (📁 `app/main.py`) 🖖 (📁 `app/`)... +* 👀 📦 `routers` (📁 `app/routers/`)... +* & ⚪️➡️ ⚫️, 🗄 🔁 `items` (📁 `app/routers/items.py`) & `users` (📁 `app/routers/users.py`)... + +🕹 `items` 🔜 ✔️ 🔢 `router` (`items.router`). 👉 🎏 1️⃣ 👥 ✍ 📁 `app/routers/items.py`, ⚫️ `APIRouter` 🎚. + +& ⤴️ 👥 🎏 🕹 `users`. + +👥 💪 🗄 👫 💖: + +```Python +from app.routers import items, users +``` + +!!! info + 🥇 ⏬ "⚖ 🗄": + + ```Python + from .routers import items, users + ``` + + 🥈 ⏬ "🎆 🗄": + + ```Python + from app.routers import items, users + ``` + + 💡 🌅 🔃 🐍 📦 & 🕹, ✍ 🛂 🐍 🧾 🔃 🕹. + +### ❎ 📛 💥 + +👥 🏭 🔁 `items` 🔗, ↩️ 🏭 🚮 🔢 `router`. + +👉 ↩️ 👥 ✔️ ➕1️⃣ 🔢 📛 `router` 🔁 `users`. + +🚥 👥 ✔️ 🗄 1️⃣ ⏮️ 🎏, 💖: + +```Python +from .routers.items import router +from .routers.users import router +``` + +`router` ⚪️➡️ `users` 🔜 📁 1️⃣ ⚪️➡️ `items` & 👥 🚫🔜 💪 ⚙️ 👫 🎏 🕰. + +, 💪 ⚙️ 👯‍♂️ 👫 🎏 📁, 👥 🗄 🔁 🔗: + +```Python hl_lines="5" title="app/main.py" +{!../../../docs_src/bigger_applications/app/main.py!} +``` + +### 🔌 `APIRouter`Ⓜ `users` & `items` + +🔜, ➡️ 🔌 `router`Ⓜ ⚪️➡️ 🔁 `users` & `items`: + +```Python hl_lines="10-11" title="app/main.py" +{!../../../docs_src/bigger_applications/app/main.py!} +``` + +!!! info + `users.router` 🔌 `APIRouter` 🔘 📁 `app/routers/users.py`. + + & `items.router` 🔌 `APIRouter` 🔘 📁 `app/routers/items.py`. + +⏮️ `app.include_router()` 👥 💪 🚮 🔠 `APIRouter` 👑 `FastAPI` 🈸. + +⚫️ 🔜 🔌 🌐 🛣 ⚪️➡️ 👈 📻 🍕 ⚫️. + +!!! note "📡 ℹ" + ⚫️ 🔜 🤙 🔘 ✍ *➡ 🛠️* 🔠 *➡ 🛠️* 👈 📣 `APIRouter`. + + , ⛅ 🎑, ⚫️ 🔜 🤙 👷 🚥 🌐 🎏 👁 📱. + +!!! check + 👆 🚫 ✔️ 😟 🔃 🎭 🕐❔ ✅ 📻. + + 👉 🔜 ✊ ⏲ & 🔜 🕴 🔨 🕴. + + ⚫️ 🏆 🚫 📉 🎭. 👶 + +### 🔌 `APIRouter` ⏮️ 🛃 `prefix`, `tags`, `responses`, & `dependencies` + +🔜, ➡️ 🌈 👆 🏢 🤝 👆 `app/internal/admin.py` 📁. + +⚫️ 🔌 `APIRouter` ⏮️ 📡 *➡ 🛠️* 👈 👆 🏢 💰 🖖 📚 🏗. + +👉 🖼 ⚫️ 🔜 💎 🙅. ✋️ ➡️ 💬 👈 ↩️ ⚫️ 💰 ⏮️ 🎏 🏗 🏢, 👥 🚫🔜 🔀 ⚫️ & 🚮 `prefix`, `dependencies`, `tags`, ♒️. 🔗 `APIRouter`: + +```Python hl_lines="3" title="app/internal/admin.py" +{!../../../docs_src/bigger_applications/app/internal/admin.py!} +``` + +✋️ 👥 💚 ⚒ 🛃 `prefix` 🕐❔ ✅ `APIRouter` 👈 🌐 🚮 *➡ 🛠️* ▶️ ⏮️ `/admin`, 👥 💚 🔐 ⚫️ ⏮️ `dependencies` 👥 ⏪ ✔️ 👉 🏗, & 👥 💚 🔌 `tags` & `responses`. + +👥 💪 📣 🌐 👈 🍵 ✔️ 🔀 ⏮️ `APIRouter` 🚶‍♀️ 👈 🔢 `app.include_router()`: + +```Python hl_lines="14-17" title="app/main.py" +{!../../../docs_src/bigger_applications/app/main.py!} +``` + +👈 🌌, ⏮️ `APIRouter` 🔜 🚧 ⚗, 👥 💪 💰 👈 🎏 `app/internal/admin.py` 📁 ⏮️ 🎏 🏗 🏢. + +🏁 👈 👆 📱, 🔠 *➡ 🛠️* ⚪️➡️ `admin` 🕹 🔜 ✔️: + +* 🔡 `/admin`. +* 🔖 `admin`. +* 🔗 `get_token_header`. +* 📨 `418`. 👶 + +✋️ 👈 🔜 🕴 📉 👈 `APIRouter` 👆 📱, 🚫 🙆 🎏 📟 👈 ⚙️ ⚫️. + +, 🖼, 🎏 🏗 💪 ⚙️ 🎏 `APIRouter` ⏮️ 🎏 🤝 👩‍🔬. + +### 🔌 *➡ 🛠️* + +👥 💪 🚮 *➡ 🛠️* 🔗 `FastAPI` 📱. + +📥 👥 ⚫️... 🎦 👈 👥 💪 🤷: + +```Python hl_lines="21-23" title="app/main.py" +{!../../../docs_src/bigger_applications/app/main.py!} +``` + +& ⚫️ 🔜 👷 ☑, 👯‍♂️ ⏮️ 🌐 🎏 *➡ 🛠️* 🚮 ⏮️ `app.include_router()`. + +!!! info "📶 📡 ℹ" + **🗒**: 👉 📶 📡 ℹ 👈 👆 🎲 💪 **🚶**. + + --- + + `APIRouter`Ⓜ 🚫 "🗻", 👫 🚫 👽 ⚪️➡️ 🎂 🈸. + + 👉 ↩️ 👥 💚 🔌 👫 *➡ 🛠️* 🗄 🔗 & 👩‍💻 🔢. + + 👥 🚫🔜 ❎ 👫 & "🗻" 👫 ➡ 🎂, *➡ 🛠️* "🖖" (🏤-✍), 🚫 🔌 🔗. + +## ✅ 🏧 🛠️ 🩺 + +🔜, 🏃 `uvicorn`, ⚙️ 🕹 `app.main` & 🔢 `app`: + +
+ +```console +$ uvicorn app.main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +& 📂 🩺 http://127.0.0.1:8000/docs. + +👆 🔜 👀 🏧 🛠️ 🩺, ✅ ➡ ⚪️➡️ 🌐 🔁, ⚙️ ☑ ➡ (& 🔡) & ☑ 🔖: + + + +## 🔌 🎏 📻 💗 🕰 ⏮️ 🎏 `prefix` + +👆 💪 ⚙️ `.include_router()` 💗 🕰 ⏮️ *🎏* 📻 ⚙️ 🎏 🔡. + +👉 💪 ⚠, 🖼, 🎦 🎏 🛠️ 🔽 🎏 🔡, ✅ `/api/v1` & `/api/latest`. + +👉 🏧 ⚙️ 👈 👆 5️⃣📆 🚫 🤙 💪, ✋️ ⚫️ 📤 💼 👆. + +## 🔌 `APIRouter` ➕1️⃣ + +🎏 🌌 👆 💪 🔌 `APIRouter` `FastAPI` 🈸, 👆 💪 🔌 `APIRouter` ➕1️⃣ `APIRouter` ⚙️: + +```Python +router.include_router(other_router) +``` + +⚒ 💭 👆 ⚫️ ⏭ 🔌 `router` `FastAPI` 📱, 👈 *➡ 🛠️* ⚪️➡️ `other_router` 🔌. diff --git a/docs/em/docs/tutorial/body-fields.md b/docs/em/docs/tutorial/body-fields.md new file mode 100644 index 0000000000..9f2c914f4b --- /dev/null +++ b/docs/em/docs/tutorial/body-fields.md @@ -0,0 +1,68 @@ +# 💪 - 🏑 + +🎏 🌌 👆 💪 📣 🌖 🔬 & 🗃 *➡ 🛠️ 🔢* 🔢 ⏮️ `Query`, `Path` & `Body`, 👆 💪 📣 🔬 & 🗃 🔘 Pydantic 🏷 ⚙️ Pydantic `Field`. + +## 🗄 `Field` + +🥇, 👆 ✔️ 🗄 ⚫️: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="2" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +!!! warning + 👀 👈 `Field` 🗄 🔗 ⚪️➡️ `pydantic`, 🚫 ⚪️➡️ `fastapi` 🌐 🎂 (`Query`, `Path`, `Body`, ♒️). + +## 📣 🏷 🔢 + +👆 💪 ⤴️ ⚙️ `Field` ⏮️ 🏷 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="9-12" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +`Field` 👷 🎏 🌌 `Query`, `Path` & `Body`, ⚫️ ✔️ 🌐 🎏 🔢, ♒️. + +!!! note "📡 ℹ" + 🤙, `Query`, `Path` & 🎏 👆 🔜 👀 ⏭ ✍ 🎚 🏿 ⚠ `Param` 🎓, ❔ ⚫️ 🏿 Pydantic `FieldInfo` 🎓. + + & Pydantic `Field` 📨 👐 `FieldInfo` 👍. + + `Body` 📨 🎚 🏿 `FieldInfo` 🔗. & 📤 🎏 👆 🔜 👀 ⏪ 👈 🏿 `Body` 🎓. + + 💭 👈 🕐❔ 👆 🗄 `Query`, `Path`, & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. + +!!! tip + 👀 ❔ 🔠 🏷 🔢 ⏮️ 🆎, 🔢 💲 & `Field` ✔️ 🎏 📊 *➡ 🛠️ 🔢* 🔢, ⏮️ `Field` ↩️ `Path`, `Query` & `Body`. + +## 🚮 ➕ ℹ + +👆 💪 📣 ➕ ℹ `Field`, `Query`, `Body`, ♒️. & ⚫️ 🔜 🔌 🏗 🎻 🔗. + +👆 🔜 💡 🌅 🔃 ❎ ➕ ℹ ⏪ 🩺, 🕐❔ 🏫 📣 🖼. + +!!! warning + ➕ 🔑 🚶‍♀️ `Field` 🔜 🎁 📉 🗄 🔗 👆 🈸. + 👫 🔑 5️⃣📆 🚫 🎯 🍕 🗄 🔧, 🗄 🧰, 🖼 [🗄 💳](https://validator.swagger.io/), 5️⃣📆 🚫 👷 ⏮️ 👆 🏗 🔗. + +## 🌃 + +👆 💪 ⚙️ Pydantic `Field` 📣 ➕ 🔬 & 🗃 🏷 🔢. + +👆 💪 ⚙️ ➕ 🇨🇻 ❌ 🚶‍♀️ 🌖 🎻 🔗 🗃. diff --git a/docs/em/docs/tutorial/body-multiple-params.md b/docs/em/docs/tutorial/body-multiple-params.md new file mode 100644 index 0000000000..9ada7dee10 --- /dev/null +++ b/docs/em/docs/tutorial/body-multiple-params.md @@ -0,0 +1,213 @@ +# 💪 - 💗 🔢 + +🔜 👈 👥 ✔️ 👀 ❔ ⚙️ `Path` & `Query`, ➡️ 👀 🌅 🏧 ⚙️ 📨 💪 📄. + +## 🌀 `Path`, `Query` & 💪 🔢 + +🥇, ↗️, 👆 💪 🌀 `Path`, `Query` & 📨 💪 🔢 📄 ➡ & **FastAPI** 🔜 💭 ⚫️❔. + +& 👆 💪 📣 💪 🔢 📦, ⚒ 🔢 `None`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17-19" + {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} + ``` + +!!! note + 👀 👈, 👉 💼, `item` 👈 🔜 ✊ ⚪️➡️ 💪 📦. ⚫️ ✔️ `None` 🔢 💲. + +## 💗 💪 🔢 + +⏮️ 🖼, *➡ 🛠️* 🔜 ⌛ 🎻 💪 ⏮️ 🔢 `Item`, 💖: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +✋️ 👆 💪 📣 💗 💪 🔢, ✅ `item` & `user`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} + ``` + +👉 💼, **FastAPI** 🔜 👀 👈 📤 🌅 🌘 1️⃣ 💪 🔢 🔢 (2️⃣ 🔢 👈 Pydantic 🏷). + +, ⚫️ 🔜 ⤴️ ⚙️ 🔢 📛 🔑 (🏑 📛) 💪, & ⌛ 💪 💖: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + } +} +``` + +!!! note + 👀 👈 ✋️ `item` 📣 🎏 🌌 ⏭, ⚫️ 🔜 ⌛ 🔘 💪 ⏮️ 🔑 `item`. + + +**FastAPI** 🔜 🏧 🛠️ ⚪️➡️ 📨, 👈 🔢 `item` 📨 ⚫️ 🎯 🎚 & 🎏 `user`. + +⚫️ 🔜 🎭 🔬 ⚗ 💽, & 🔜 📄 ⚫️ 💖 👈 🗄 🔗 & 🏧 🩺. + +## ⭐ 💲 💪 + +🎏 🌌 📤 `Query` & `Path` 🔬 ➕ 💽 🔢 & ➡ 🔢, **FastAPI** 🚚 🌓 `Body`. + +🖼, ↔ ⏮️ 🏷, 👆 💪 💭 👈 👆 💚 ✔️ ➕1️⃣ 🔑 `importance` 🎏 💪, 🥈 `item` & `user`. + +🚥 👆 📣 ⚫️, ↩️ ⚫️ ⭐ 💲, **FastAPI** 🔜 🤔 👈 ⚫️ 🔢 🔢. + +✋️ 👆 💪 💡 **FastAPI** 😥 ⚫️ ➕1️⃣ 💪 🔑 ⚙️ `Body`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} + ``` + +👉 💼, **FastAPI** 🔜 ⌛ 💪 💖: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + }, + "importance": 5 +} +``` + +🔄, ⚫️ 🔜 🗜 📊 🆎, ✔, 📄, ♒️. + +## 💗 💪 = & 🔢 + +↗️, 👆 💪 📣 🌖 🔢 🔢 🕐❔ 👆 💪, 🌖 🙆 💪 🔢. + +, 🔢, ⭐ 💲 🔬 🔢 🔢, 👆 🚫 ✔️ 🎯 🚮 `Query`, 👆 💪: + +```Python +q: Union[str, None] = None +``` + +⚖️ 🐍 3️⃣.1️⃣0️⃣ & 🔛: + +```Python +q: str | None = None +``` + +🖼: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="26" + {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} + ``` + +!!! info + `Body` ✔️ 🌐 🎏 ➕ 🔬 & 🗃 🔢 `Query`,`Path` & 🎏 👆 🔜 👀 ⏪. + +## ⏯ 👁 💪 🔢 + +➡️ 💬 👆 🕴 ✔️ 👁 `item` 💪 🔢 ⚪️➡️ Pydantic 🏷 `Item`. + +🔢, **FastAPI** 🔜 ⤴️ ⌛ 🚮 💪 🔗. + +✋️ 🚥 👆 💚 ⚫️ ⌛ 🎻 ⏮️ 🔑 `item` & 🔘 ⚫️ 🏷 🎚, ⚫️ 🔨 🕐❔ 👆 📣 ➕ 💪 🔢, 👆 💪 ⚙️ 🎁 `Body` 🔢 `embed`: + +```Python +item: Item = Body(embed=True) +``` + +: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} + ``` + +👉 💼 **FastAPI** 🔜 ⌛ 💪 💖: + +```JSON hl_lines="2" +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + } +} +``` + +↩️: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +## 🌃 + +👆 💪 🚮 💗 💪 🔢 👆 *➡ 🛠️ 🔢*, ✋️ 📨 💪 🕴 ✔️ 👁 💪. + +✋️ **FastAPI** 🔜 🍵 ⚫️, 🤝 👆 ☑ 📊 👆 🔢, & ✔ & 📄 ☑ 🔗 *➡ 🛠️*. + +👆 💪 📣 ⭐ 💲 📨 🍕 💪. + +& 👆 💪 💡 **FastAPI** ⏯ 💪 🔑 🕐❔ 📤 🕴 👁 🔢 📣. diff --git a/docs/em/docs/tutorial/body-nested-models.md b/docs/em/docs/tutorial/body-nested-models.md new file mode 100644 index 0000000000..f4bd50f5cb --- /dev/null +++ b/docs/em/docs/tutorial/body-nested-models.md @@ -0,0 +1,382 @@ +# 💪 - 🔁 🏷 + +⏮️ **FastAPI**, 👆 💪 🔬, ✔, 📄, & ⚙️ 🎲 🙇 🐦 🏷 (👏 Pydantic). + +## 📇 🏑 + +👆 💪 🔬 🔢 🏾. 🖼, 🐍 `list`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} + ``` + +👉 🔜 ⚒ `tags` 📇, 👐 ⚫️ 🚫 📣 🆎 🔣 📇. + +## 📇 🏑 ⏮️ 🆎 🔢 + +✋️ 🐍 ✔️ 🎯 🌌 📣 📇 ⏮️ 🔗 🆎, ⚖️ "🆎 🔢": + +### 🗄 ⌨ `List` + +🐍 3️⃣.9️⃣ & 🔛 👆 💪 ⚙️ 🐩 `list` 📣 👫 🆎 ✍ 👥 🔜 👀 🔛. 👶 + +✋️ 🐍 ⏬ ⏭ 3️⃣.9️⃣ (3️⃣.6️⃣ & 🔛), 👆 🥇 💪 🗄 `List` ⚪️➡️ 🐩 🐍 `typing` 🕹: + +```Python hl_lines="1" +{!> ../../../docs_src/body_nested_models/tutorial002.py!} +``` + +### 📣 `list` ⏮️ 🆎 🔢 + +📣 🆎 👈 ✔️ 🆎 🔢 (🔗 🆎), 💖 `list`, `dict`, `tuple`: + +* 🚥 👆 🐍 ⏬ 🔅 🌘 3️⃣.9️⃣, 🗄 👫 🌓 ⏬ ⚪️➡️ `typing` 🕹 +* 🚶‍♀️ 🔗 🆎(Ⓜ) "🆎 🔢" ⚙️ ⬜ 🗜: `[` & `]` + +🐍 3️⃣.9️⃣ ⚫️ 🔜: + +```Python +my_list: list[str] +``` + +⏬ 🐍 ⏭ 3️⃣.9️⃣, ⚫️ 🔜: + +```Python +from typing import List + +my_list: List[str] +``` + +👈 🌐 🐩 🐍 ❕ 🆎 📄. + +⚙️ 👈 🎏 🐩 ❕ 🏷 🔢 ⏮️ 🔗 🆎. + +, 👆 🖼, 👥 💪 ⚒ `tags` 🎯 "📇 🎻": + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} + ``` + +## ⚒ 🆎 + +✋️ ⤴️ 👥 💭 🔃 ⚫️, & 🤔 👈 🔖 🚫🔜 🚫 🔁, 👫 🔜 🎲 😍 🎻. + +& 🐍 ✔️ 🎁 💽 🆎 ⚒ 😍 🏬, `set`. + +⤴️ 👥 💪 📣 `tags` ⚒ 🎻: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 14" + {!> ../../../docs_src/body_nested_models/tutorial003.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} + ``` + +⏮️ 👉, 🚥 👆 📨 📨 ⏮️ ❎ 📊, ⚫️ 🔜 🗜 ⚒ 😍 🏬. + +& 🕐❔ 👆 🔢 👈 📊, 🚥 ℹ ✔️ ❎, ⚫️ 🔜 🔢 ⚒ 😍 🏬. + +& ⚫️ 🔜 ✍ / 📄 ➡️ 💁‍♂️. + +## 🐦 🏷 + +🔠 🔢 Pydantic 🏷 ✔️ 🆎. + +✋️ 👈 🆎 💪 ⚫️ ➕1️⃣ Pydantic 🏷. + +, 👆 💪 📣 🙇 🐦 🎻 "🎚" ⏮️ 🎯 🔢 📛, 🆎 & 🔬. + +🌐 👈, 🎲 🐦. + +### 🔬 📊 + +🖼, 👥 💪 🔬 `Image` 🏷: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7-9" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +### ⚙️ 📊 🆎 + +& ⤴️ 👥 💪 ⚙️ ⚫️ 🆎 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +👉 🔜 ⛓ 👈 **FastAPI** 🔜 ⌛ 💪 🎏: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": ["rock", "metal", "bar"], + "image": { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + } +} +``` + +🔄, 🤸 👈 📄, ⏮️ **FastAPI** 👆 🤚: + +* 👨‍🎨 🐕‍🦺 (🛠️, ♒️), 🐦 🏷 +* 💽 🛠️ +* 💽 🔬 +* 🏧 🧾 + +## 🎁 🆎 & 🔬 + +↖️ ⚪️➡️ 😐 ⭐ 🆎 💖 `str`, `int`, `float`, ♒️. 👆 💪 ⚙️ 🌅 🏗 ⭐ 🆎 👈 😖 ⚪️➡️ `str`. + +👀 🌐 🎛 👆 ✔️, 🛒 🩺 Pydantic 😍 🆎. 👆 🔜 👀 🖼 ⏭ 📃. + +🖼, `Image` 🏷 👥 ✔️ `url` 🏑, 👥 💪 📣 ⚫️ ↩️ `str`, Pydantic `HttpUrl`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="2 8" + {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} + ``` + +🎻 🔜 ✅ ☑ 📛, & 📄 🎻 🔗 / 🗄 ✅. + +## 🔢 ⏮️ 📇 📊 + +👆 💪 ⚙️ Pydantic 🏷 🏾 `list`, `set`, ♒️: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} + ``` + +👉 🔜 ⌛ (🗜, ✔, 📄, ♒️) 🎻 💪 💖: + +```JSON hl_lines="11" +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": [ + "rock", + "metal", + "bar" + ], + "images": [ + { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + }, + { + "url": "http://example.com/dave.jpg", + "name": "The Baz" + } + ] +} +``` + +!!! info + 👀 ❔ `images` 🔑 🔜 ✔️ 📇 🖼 🎚. + +## 🙇 🐦 🏷 + +👆 💪 🔬 🎲 🙇 🐦 🏷: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7 12 18 21 25" + {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} + ``` + +!!! info + 👀 ❔ `Offer` ✔️ 📇 `Item`Ⓜ, ❔ 🔄 ✔️ 📦 📇 `Image`Ⓜ + +## 💪 😁 📇 + +🚥 🔝 🎚 💲 🎻 💪 👆 ⌛ 🎻 `array` (🐍 `list`), 👆 💪 📣 🆎 🔢 🔢, 🎏 Pydantic 🏷: + +```Python +images: List[Image] +``` + +⚖️ 🐍 3️⃣.9️⃣ & 🔛: + +```Python +images: list[Image] +``` + +: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_nested_models/tutorial008.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="13" + {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} + ``` + +## 👨‍🎨 🐕‍🦺 🌐 + +& 👆 🤚 👨‍🎨 🐕‍🦺 🌐. + +🏬 🔘 📇: + + + +👆 🚫 🚫 🤚 👉 😇 👨‍🎨 🐕‍🦺 🚥 👆 👷 🔗 ⏮️ `dict` ↩️ Pydantic 🏷. + +✋️ 👆 🚫 ✔️ 😟 🔃 👫 👯‍♂️, 📨 #️⃣ 🗜 🔁 & 👆 🔢 🗜 🔁 🎻 💁‍♂️. + +## 💪 ❌ `dict`Ⓜ + +👆 💪 📣 💪 `dict` ⏮️ 🔑 🆎 & 💲 🎏 🆎. + +🍵 ✔️ 💭 ⏪ ⚫️❔ ☑ 🏑/🔢 📛 (🔜 💼 ⏮️ Pydantic 🏷). + +👉 🔜 ⚠ 🚥 👆 💚 📨 🔑 👈 👆 🚫 ⏪ 💭. + +--- + +🎏 ⚠ 💼 🕐❔ 👆 💚 ✔️ 🔑 🎏 🆎, ✅ `int`. + +👈 ⚫️❔ 👥 🔜 👀 📥. + +👉 💼, 👆 🔜 🚫 🙆 `dict` 📏 ⚫️ ✔️ `int` 🔑 ⏮️ `float` 💲: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/body_nested_models/tutorial009.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} + ``` + +!!! tip + ✔️ 🤯 👈 🎻 🕴 🐕‍🦺 `str` 🔑. + + ✋️ Pydantic ✔️ 🏧 💽 🛠️. + + 👉 ⛓ 👈, ✋️ 👆 🛠️ 👩‍💻 💪 🕴 📨 🎻 🔑, 📏 👈 🎻 🔌 😁 🔢, Pydantic 🔜 🗜 👫 & ✔ 👫. + + & `dict` 👆 📨 `weights` 🔜 🤙 ✔️ `int` 🔑 & `float` 💲. + +## 🌃 + +⏮️ **FastAPI** 👆 ✔️ 🔆 💪 🚚 Pydantic 🏷, ⏪ 🚧 👆 📟 🙅, 📏 & 😍. + +✋️ ⏮️ 🌐 💰: + +* 👨‍🎨 🐕‍🦺 (🛠️ 🌐 ❗) +* 💽 🛠️ (.Ⓜ.. ✍ / 🛠️) +* 💽 🔬 +* 🔗 🧾 +* 🏧 🩺 diff --git a/docs/em/docs/tutorial/body-updates.md b/docs/em/docs/tutorial/body-updates.md new file mode 100644 index 0000000000..98058ab526 --- /dev/null +++ b/docs/em/docs/tutorial/body-updates.md @@ -0,0 +1,155 @@ +# 💪 - ℹ + +## ℹ ❎ ⏮️ `PUT` + +ℹ 🏬 👆 💪 ⚙️ 🇺🇸🔍 `PUT` 🛠️. + +👆 💪 ⚙️ `jsonable_encoder` 🗜 🔢 💽 📊 👈 💪 🏪 🎻 (✅ ⏮️ ☁ 💽). 🖼, 🏭 `datetime` `str`. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="30-35" + {!> ../../../docs_src/body_updates/tutorial001.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="30-35" + {!> ../../../docs_src/body_updates/tutorial001_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="28-33" + {!> ../../../docs_src/body_updates/tutorial001_py310.py!} + ``` + +`PUT` ⚙️ 📨 💽 👈 🔜 ❎ ♻ 💽. + +### ⚠ 🔃 ❎ + +👈 ⛓ 👈 🚥 👆 💚 ℹ 🏬 `bar` ⚙️ `PUT` ⏮️ 💪 ⚗: + +```Python +{ + "name": "Barz", + "price": 3, + "description": None, +} +``` + +↩️ ⚫️ 🚫 🔌 ⏪ 🏪 🔢 `"tax": 20.2`, 🔢 🏷 🔜 ✊ 🔢 💲 `"tax": 10.5`. + +& 📊 🔜 🖊 ⏮️ 👈 "🆕" `tax` `10.5`. + +## 🍕 ℹ ⏮️ `PATCH` + +👆 💪 ⚙️ 🇺🇸🔍 `PATCH` 🛠️ *🍕* ℹ 💽. + +👉 ⛓ 👈 👆 💪 📨 🕴 💽 👈 👆 💚 ℹ, 🍂 🎂 🐣. + +!!! Note + `PATCH` 🌘 🛎 ⚙️ & 💭 🌘 `PUT`. + + & 📚 🏉 ⚙️ 🕴 `PUT`, 🍕 ℹ. + + 👆 **🆓** ⚙️ 👫 👐 👆 💚, **FastAPI** 🚫 🚫 🙆 🚫. + + ✋️ 👉 🦮 🎦 👆, 🌖 ⚖️ 🌘, ❔ 👫 🎯 ⚙️. + +### ⚙️ Pydantic `exclude_unset` 🔢 + +🚥 👆 💚 📨 🍕 ℹ, ⚫️ 📶 ⚠ ⚙️ 🔢 `exclude_unset` Pydantic 🏷 `.dict()`. + +💖 `item.dict(exclude_unset=True)`. + +👈 🔜 🏗 `dict` ⏮️ 🕴 💽 👈 ⚒ 🕐❔ 🏗 `item` 🏷, 🚫 🔢 💲. + +⤴️ 👆 💪 ⚙️ 👉 🏗 `dict` ⏮️ 🕴 💽 👈 ⚒ (📨 📨), 🚫 🔢 💲: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="32" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` + +### ⚙️ Pydantic `update` 🔢 + +🔜, 👆 💪 ✍ 📁 ♻ 🏷 ⚙️ `.copy()`, & 🚶‍♀️ `update` 🔢 ⏮️ `dict` ⚗ 💽 ℹ. + +💖 `stored_item_model.copy(update=update_data)`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="33" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` + +### 🍕 ℹ 🌃 + +📄, ✔ 🍕 ℹ 👆 🔜: + +* (⚗) ⚙️ `PATCH` ↩️ `PUT`. +* 🗃 🏪 💽. +* 🚮 👈 💽 Pydantic 🏷. +* 🏗 `dict` 🍵 🔢 💲 ⚪️➡️ 🔢 🏷 (⚙️ `exclude_unset`). + * 👉 🌌 👆 💪 ℹ 🕴 💲 🤙 ⚒ 👩‍💻, ↩️ 🔐 💲 ⏪ 🏪 ⏮️ 🔢 💲 👆 🏷. +* ✍ 📁 🏪 🏷, 🛠️ ⚫️ 🔢 ⏮️ 📨 🍕 ℹ (⚙️ `update` 🔢). +* 🗜 📁 🏷 🕳 👈 💪 🏪 👆 💽 (🖼, ⚙️ `jsonable_encoder`). + * 👉 ⭐ ⚙️ 🏷 `.dict()` 👩‍🔬 🔄, ✋️ ⚫️ ⚒ 💭 (& 🗜) 💲 💽 🆎 👈 💪 🗜 🎻, 🖼, `datetime` `str`. +* 🖊 💽 👆 💽. +* 📨 ℹ 🏷. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="30-37" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="30-37" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="28-35" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` + +!!! tip + 👆 💪 🤙 ⚙️ 👉 🎏 ⚒ ⏮️ 🇺🇸🔍 `PUT` 🛠️. + + ✋️ 🖼 📥 ⚙️ `PATCH` ↩️ ⚫️ ✍ 👫 ⚙️ 💼. + +!!! note + 👀 👈 🔢 🏷 ✔. + + , 🚥 👆 💚 📨 🍕 ℹ 👈 💪 🚫 🌐 🔢, 👆 💪 ✔️ 🏷 ⏮️ 🌐 🔢 ™ 📦 (⏮️ 🔢 💲 ⚖️ `None`). + + 🔬 ⚪️➡️ 🏷 ⏮️ 🌐 📦 💲 **ℹ** & 🏷 ⏮️ ✔ 💲 **🏗**, 👆 💪 ⚙️ 💭 🔬 [➕ 🏷](extra-models.md){.internal-link target=_blank}. diff --git a/docs/em/docs/tutorial/body.md b/docs/em/docs/tutorial/body.md new file mode 100644 index 0000000000..ca2f113bf4 --- /dev/null +++ b/docs/em/docs/tutorial/body.md @@ -0,0 +1,213 @@ +# 📨 💪 + +🕐❔ 👆 💪 📨 📊 ⚪️➡️ 👩‍💻 (➡️ 💬, 🖥) 👆 🛠️, 👆 📨 ⚫️ **📨 💪**. + +**📨** 💪 📊 📨 👩‍💻 👆 🛠️. **📨** 💪 💽 👆 🛠️ 📨 👩‍💻. + +👆 🛠️ 🌖 🕧 ✔️ 📨 **📨** 💪. ✋️ 👩‍💻 🚫 🎯 💪 📨 **📨** 💪 🌐 🕰. + +📣 **📨** 💪, 👆 ⚙️ Pydantic 🏷 ⏮️ 🌐 👫 🏋️ & 💰. + +!!! info + 📨 💽, 👆 🔜 ⚙️ 1️⃣: `POST` (🌅 ⚠), `PUT`, `DELETE` ⚖️ `PATCH`. + + 📨 💪 ⏮️ `GET` 📨 ✔️ ⚠ 🎭 🔧, 👐, ⚫️ 🐕‍🦺 FastAPI, 🕴 📶 🏗/😕 ⚙️ 💼. + + ⚫️ 🚫, 🎓 🩺 ⏮️ 🦁 🎚 🏆 🚫 🎦 🧾 💪 🕐❔ ⚙️ `GET`, & 🗳 🖕 💪 🚫 🐕‍🦺 ⚫️. + +## 🗄 Pydantic `BaseModel` + +🥇, 👆 💪 🗄 `BaseModel` ⚪️➡️ `pydantic`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="4" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="2" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +## ✍ 👆 💽 🏷 + +⤴️ 👆 📣 👆 💽 🏷 🎓 👈 😖 ⚪️➡️ `BaseModel`. + +⚙️ 🐩 🐍 🆎 🌐 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="7-11" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="5-9" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +🎏 🕐❔ 📣 🔢 🔢, 🕐❔ 🏷 🔢 ✔️ 🔢 💲, ⚫️ 🚫 ✔. ⏪, ⚫️ ✔. ⚙️ `None` ⚒ ⚫️ 📦. + +🖼, 👉 🏷 🔛 📣 🎻 "`object`" (⚖️ 🐍 `dict`) 💖: + +```JSON +{ + "name": "Foo", + "description": "An optional description", + "price": 45.2, + "tax": 3.5 +} +``` + +... `description` & `tax` 📦 (⏮️ 🔢 💲 `None`), 👉 🎻 "`object`" 🔜 ☑: + +```JSON +{ + "name": "Foo", + "price": 45.2 +} +``` + +## 📣 ⚫️ 🔢 + +🚮 ⚫️ 👆 *➡ 🛠️*, 📣 ⚫️ 🎏 🌌 👆 📣 ➡ & 🔢 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +...& 📣 🚮 🆎 🏷 👆 ✍, `Item`. + +## 🏁 + +⏮️ 👈 🐍 🆎 📄, **FastAPI** 🔜: + +* ✍ 💪 📨 🎻. +* 🗜 🔗 🆎 (🚥 💪). +* ✔ 💽. + * 🚥 💽 ❌, ⚫️ 🔜 📨 👌 & 🆑 ❌, ☠️ ⚫️❔ 🌐❔ & ⚫️❔ ❌ 📊. +* 🤝 👆 📨 📊 🔢 `item`. + * 👆 📣 ⚫️ 🔢 🆎 `Item`, 👆 🔜 ✔️ 🌐 👨‍🎨 🐕‍🦺 (🛠️, ♒️) 🌐 🔢 & 👫 🆎. +* 🏗 🎻 🔗 🔑 👆 🏷, 👆 💪 ⚙️ 👫 🙆 🙆 👆 💖 🚥 ⚫️ ⚒ 🔑 👆 🏗. +* 👈 🔗 🔜 🍕 🏗 🗄 🔗, & ⚙️ 🏧 🧾 . + +## 🏧 🩺 + +🎻 🔗 👆 🏷 🔜 🍕 👆 🗄 🏗 🔗, & 🔜 🎦 🎓 🛠️ 🩺: + + + +& 🔜 ⚙️ 🛠️ 🩺 🔘 🔠 *➡ 🛠️* 👈 💪 👫: + + + +## 👨‍🎨 🐕‍🦺 + +👆 👨‍🎨, 🔘 👆 🔢 👆 🔜 🤚 🆎 🔑 & 🛠️ 🌐 (👉 🚫🔜 🔨 🚥 👆 📨 `dict` ↩️ Pydantic 🏷): + + + +👆 🤚 ❌ ✅ ❌ 🆎 🛠️: + + + +👉 🚫 🤞, 🎂 🛠️ 🏗 🤭 👈 🔧. + +& ⚫️ 🙇 💯 🔧 🌓, ⏭ 🙆 🛠️, 🚚 ⚫️ 🔜 👷 ⏮️ 🌐 👨‍🎨. + +📤 🔀 Pydantic ⚫️ 🐕‍🦺 👉. + +⏮️ 🖼 ✊ ⏮️ 🎙 🎙 📟. + +✋️ 👆 🔜 🤚 🎏 👨‍🎨 🐕‍🦺 ⏮️ 🗒 & 🌅 🎏 🐍 👨‍🎨: + + + +!!! tip + 🚥 👆 ⚙️ 🗒 👆 👨‍🎨, 👆 💪 ⚙️ Pydantic 🗒 📁. + + ⚫️ 📉 👨‍🎨 🐕‍🦺 Pydantic 🏷, ⏮️: + + * 🚘-🛠️ + * 🆎 ✅ + * 🛠️ + * 🔎 + * 🔬 + +## ⚙️ 🏷 + +🔘 🔢, 👆 💪 🔐 🌐 🔢 🏷 🎚 🔗: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="21" + {!> ../../../docs_src/body/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="19" + {!> ../../../docs_src/body/tutorial002_py310.py!} + ``` + +## 📨 💪 ➕ ➡ 🔢 + +👆 💪 📣 ➡ 🔢 & 📨 💪 🎏 🕰. + +**FastAPI** 🔜 🤔 👈 🔢 🔢 👈 🏏 ➡ 🔢 🔜 **✊ ⚪️➡️ ➡**, & 👈 🔢 🔢 👈 📣 Pydantic 🏷 🔜 **✊ ⚪️➡️ 📨 💪**. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="15-16" + {!> ../../../docs_src/body/tutorial003_py310.py!} + ``` + +## 📨 💪 ➕ ➡ ➕ 🔢 🔢 + +👆 💪 📣 **💪**, **➡** & **🔢** 🔢, 🌐 🎏 🕰. + +**FastAPI** 🔜 🤔 🔠 👫 & ✊ 📊 ⚪️➡️ ☑ 🥉. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial004_py310.py!} + ``` + +🔢 🔢 🔜 🤔 ⏩: + +* 🚥 🔢 📣 **➡**, ⚫️ 🔜 ⚙️ ➡ 🔢. +* 🚥 🔢 **⭐ 🆎** (💖 `int`, `float`, `str`, `bool`, ♒️) ⚫️ 🔜 🔬 **🔢** 🔢. +* 🚥 🔢 📣 🆎 **Pydantic 🏷**, ⚫️ 🔜 🔬 📨 **💪**. + +!!! note + FastAPI 🔜 💭 👈 💲 `q` 🚫 ✔ ↩️ 🔢 💲 `= None`. + + `Union` `Union[str, None]` 🚫 ⚙️ FastAPI, ✋️ 🔜 ✔ 👆 👨‍🎨 🤝 👆 👍 🐕‍🦺 & 🔍 ❌. + +## 🍵 Pydantic + +🚥 👆 🚫 💚 ⚙️ Pydantic 🏷, 👆 💪 ⚙️ **💪** 🔢. 👀 🩺 [💪 - 💗 🔢: ⭐ 💲 💪](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. diff --git a/docs/em/docs/tutorial/cookie-params.md b/docs/em/docs/tutorial/cookie-params.md new file mode 100644 index 0000000000..47f4a62f5c --- /dev/null +++ b/docs/em/docs/tutorial/cookie-params.md @@ -0,0 +1,49 @@ +# 🍪 🔢 + +👆 💪 🔬 🍪 🔢 🎏 🌌 👆 🔬 `Query` & `Path` 🔢. + +## 🗄 `Cookie` + +🥇 🗄 `Cookie`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +## 📣 `Cookie` 🔢 + +⤴️ 📣 🍪 🔢 ⚙️ 🎏 📊 ⏮️ `Path` & `Query`. + +🥇 💲 🔢 💲, 👆 💪 🚶‍♀️ 🌐 ➕ 🔬 ⚖️ ✍ 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +!!! note "📡 ℹ" + `Cookie` "👭" 🎓 `Path` & `Query`. ⚫️ 😖 ⚪️➡️ 🎏 ⚠ `Param` 🎓. + + ✋️ 💭 👈 🕐❔ 👆 🗄 `Query`, `Path`, `Cookie` & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. + +!!! info + 📣 🍪, 👆 💪 ⚙️ `Cookie`, ↩️ ⏪ 🔢 🔜 🔬 🔢 🔢. + +## 🌃 + +📣 🍪 ⏮️ `Cookie`, ⚙️ 🎏 ⚠ ⚓ `Query` & `Path`. diff --git a/docs/em/docs/tutorial/cors.md b/docs/em/docs/tutorial/cors.md new file mode 100644 index 0000000000..8c5e33ed7b --- /dev/null +++ b/docs/em/docs/tutorial/cors.md @@ -0,0 +1,84 @@ +# ⚜ (✖️-🇨🇳 ℹ 🤝) + +⚜ ⚖️ "✖️-🇨🇳 ℹ 🤝" 🔗 ⚠ 🕐❔ 🕸 🏃‍♂ 🖥 ✔️ 🕸 📟 👈 🔗 ⏮️ 👩‍💻, & 👩‍💻 🎏 "🇨🇳" 🌘 🕸. + +## 🇨🇳 + +🇨🇳 🌀 🛠️ (`http`, `https`), 🆔 (`myapp.com`, `localhost`, `localhost.tiangolo.com`), & ⛴ (`80`, `443`, `8080`). + +, 🌐 👫 🎏 🇨🇳: + +* `http://localhost` +* `https://localhost` +* `http://localhost:8080` + +🚥 👫 🌐 `localhost`, 👫 ⚙️ 🎏 🛠️ ⚖️ ⛴,, 👫 🎏 "🇨🇳". + +## 🔁 + +, ➡️ 💬 👆 ✔️ 🕸 🏃 👆 🖥 `http://localhost:8080`, & 🚮 🕸 🔄 🔗 ⏮️ 👩‍💻 🏃 `http://localhost` (↩️ 👥 🚫 ✔ ⛴, 🖥 🔜 🤔 🔢 ⛴ `80`). + +⤴️, 🖥 🔜 📨 🇺🇸🔍 `OPTIONS` 📨 👩‍💻, & 🚥 👩‍💻 📨 ☑ 🎚 ✔ 📻 ⚪️➡️ 👉 🎏 🇨🇳 (`http://localhost:8080`) ⤴️ 🖥 🔜 ➡️ 🕸 🕸 📨 🚮 📨 👩‍💻. + +🏆 👉, 👩‍💻 🔜 ✔️ 📇 "✔ 🇨🇳". + +👉 💼, ⚫️ 🔜 ✔️ 🔌 `http://localhost:8080` 🕸 👷 ☑. + +## 🃏 + +⚫️ 💪 📣 📇 `"*"` ("🃏") 💬 👈 🌐 ✔. + +✋️ 👈 🔜 🕴 ✔ 🎯 🆎 📻, 🚫 🌐 👈 🔌 🎓: 🍪, ✔ 🎚 💖 📚 ⚙️ ⏮️ 📨 🤝, ♒️. + +, 🌐 👷 ☑, ⚫️ 👻 ✔ 🎯 ✔ 🇨🇳. + +## ⚙️ `CORSMiddleware` + +👆 💪 🔗 ⚫️ 👆 **FastAPI** 🈸 ⚙️ `CORSMiddleware`. + +* 🗄 `CORSMiddleware`. +* ✍ 📇 ✔ 🇨🇳 (🎻). +* 🚮 ⚫️ "🛠️" 👆 **FastAPI** 🈸. + +👆 💪 ✔ 🚥 👆 👩‍💻 ✔: + +* 🎓 (✔ 🎚, 🍪, ♒️). +* 🎯 🇺🇸🔍 👩‍🔬 (`POST`, `PUT`) ⚖️ 🌐 👫 ⏮️ 🃏 `"*"`. +* 🎯 🇺🇸🔍 🎚 ⚖️ 🌐 👫 ⏮️ 🃏 `"*"`. + +```Python hl_lines="2 6-11 13-19" +{!../../../docs_src/cors/tutorial001.py!} +``` + +🔢 🔢 ⚙️ `CORSMiddleware` 🛠️ 🚫 🔢, 👆 🔜 💪 🎯 🛠️ 🎯 🇨🇳, 👩‍🔬, ⚖️ 🎚, ✔ 🖥 ✔ ⚙️ 👫 ✖️-🆔 🔑. + +📄 ❌ 🐕‍🦺: + +* `allow_origins` - 📇 🇨🇳 👈 🔜 ✔ ⚒ ✖️-🇨🇳 📨. 🤶 Ⓜ. `['https://example.org', 'https://www.example.org']`. 👆 💪 ⚙️ `['*']` ✔ 🙆 🇨🇳. +* `allow_origin_regex` - 🎻 🎻 🏏 🛡 🇨🇳 👈 🔜 ✔ ⚒ ✖️-🇨🇳 📨. ✅ `'https://.*\.example\.org'`. +* `allow_methods` - 📇 🇺🇸🔍 👩‍🔬 👈 🔜 ✔ ✖️-🇨🇳 📨. 🔢 `['GET']`. 👆 💪 ⚙️ `['*']` ✔ 🌐 🐩 👩‍🔬. +* `allow_headers` - 📇 🇺🇸🔍 📨 🎚 👈 🔜 🐕‍🦺 ✖️-🇨🇳 📨. 🔢 `[]`. 👆 💪 ⚙️ `['*']` ✔ 🌐 🎚. `Accept`, `Accept-Language`, `Content-Language` & `Content-Type` 🎚 🕧 ✔ 🙅 ⚜ 📨. +* `allow_credentials` - 🎦 👈 🍪 🔜 🐕‍🦺 ✖️-🇨🇳 📨. 🔢 `False`. , `allow_origins` 🚫🔜 ⚒ `['*']` 🎓 ✔, 🇨🇳 🔜 ✔. +* `expose_headers` - 🎦 🙆 📨 🎚 👈 🔜 ⚒ ♿ 🖥. 🔢 `[]`. +* `max_age` - ⚒ 🔆 🕰 🥈 🖥 💾 ⚜ 📨. 🔢 `600`. + +🛠️ 📨 2️⃣ 🎯 🆎 🇺🇸🔍 📨... + +### ⚜ 🛫 📨 + +👉 🙆 `OPTIONS` 📨 ⏮️ `Origin` & `Access-Control-Request-Method` 🎚. + +👉 💼 🛠️ 🔜 🆘 📨 📨 & 📨 ⏮️ ☑ ⚜ 🎚, & 👯‍♂️ `200` ⚖️ `400` 📨 🎓 🎯. + +### 🙅 📨 + +🙆 📨 ⏮️ `Origin` 🎚. 👉 💼 🛠️ 🔜 🚶‍♀️ 📨 🔘 😐, ✋️ 🔜 🔌 ☑ ⚜ 🎚 🔛 📨. + +## 🌅 ℹ + +🌖 ℹ 🔃 , ✅ 🦎 ⚜ 🧾. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.middleware.cors import CORSMiddleware`. + + **FastAPI** 🚚 📚 🛠️ `fastapi.middleware` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 🛠️ 👟 🔗 ⚪️➡️ 💃. diff --git a/docs/em/docs/tutorial/debugging.md b/docs/em/docs/tutorial/debugging.md new file mode 100644 index 0000000000..c7c11b5cec --- /dev/null +++ b/docs/em/docs/tutorial/debugging.md @@ -0,0 +1,112 @@ +# 🛠️ + +👆 💪 🔗 🕹 👆 👨‍🎨, 🖼 ⏮️ 🎙 🎙 📟 ⚖️ 🗒. + +## 🤙 `uvicorn` + +👆 FastAPI 🈸, 🗄 & 🏃 `uvicorn` 🔗: + +```Python hl_lines="1 15" +{!../../../docs_src/debugging/tutorial001.py!} +``` + +### 🔃 `__name__ == "__main__"` + +👑 🎯 `__name__ == "__main__"` ✔️ 📟 👈 🛠️ 🕐❔ 👆 📁 🤙 ⏮️: + +
+ +```console +$ python myapp.py +``` + +
+ +✋️ 🚫 🤙 🕐❔ ➕1️⃣ 📁 🗄 ⚫️, 💖: + +```Python +from myapp import app +``` + +#### 🌅 ℹ + +➡️ 💬 👆 📁 🌟 `myapp.py`. + +🚥 👆 🏃 ⚫️ ⏮️: + +
+ +```console +$ python myapp.py +``` + +
+ +⤴️ 🔗 🔢 `__name__` 👆 📁, ✍ 🔁 🐍, 🔜 ✔️ 💲 🎻 `"__main__"`. + +, 📄: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +🔜 🏃. + +--- + +👉 🏆 🚫 🔨 🚥 👆 🗄 👈 🕹 (📁). + +, 🚥 👆 ✔️ ➕1️⃣ 📁 `importer.py` ⏮️: + +```Python +from myapp import app + +# Some more code +``` + +👈 💼, 🏧 🔢 🔘 `myapp.py` 🔜 🚫 ✔️ 🔢 `__name__` ⏮️ 💲 `"__main__"`. + +, ⏸: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +🔜 🚫 🛠️. + +!!! info + 🌅 ℹ, ✅ 🛂 🐍 🩺. + +## 🏃 👆 📟 ⏮️ 👆 🕹 + +↩️ 👆 🏃 Uvicorn 💽 🔗 ⚪️➡️ 👆 📟, 👆 💪 🤙 👆 🐍 📋 (👆 FastAPI 🈸) 🔗 ⚪️➡️ 🕹. + +--- + +🖼, 🎙 🎙 📟, 👆 💪: + +* 🚶 "ℹ" 🎛. +* "🚮 📳...". +* 🖊 "🐍" +* 🏃 🕹 ⏮️ 🎛 "`Python: Current File (Integrated Terminal)`". + +⚫️ 🔜 ⤴️ ▶️ 💽 ⏮️ 👆 **FastAPI** 📟, ⛔️ 👆 0️⃣, ♒️. + +📥 ❔ ⚫️ 💪 👀: + + + +--- + +🚥 👆 ⚙️ 🗒, 👆 💪: + +* 📂 "🏃" 🍣. +* 🖊 🎛 "ℹ...". +* ⤴️ 🔑 🍣 🎦 🆙. +* 🖊 📁 ℹ (👉 💼, `main.py`). + +⚫️ 🔜 ⤴️ ▶️ 💽 ⏮️ 👆 **FastAPI** 📟, ⛔️ 👆 0️⃣, ♒️. + +📥 ❔ ⚫️ 💪 👀: + + diff --git a/docs/em/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/em/docs/tutorial/dependencies/classes-as-dependencies.md new file mode 100644 index 0000000000..e2d2686d34 --- /dev/null +++ b/docs/em/docs/tutorial/dependencies/classes-as-dependencies.md @@ -0,0 +1,247 @@ +# 🎓 🔗 + +⏭ 🤿 ⏬ 🔘 **🔗 💉** ⚙️, ➡️ ♻ ⏮️ 🖼. + +## `dict` ⚪️➡️ ⏮️ 🖼 + +⏮️ 🖼, 👥 🛬 `dict` ⚪️➡️ 👆 🔗 ("☑"): + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +✋️ ⤴️ 👥 🤚 `dict` 🔢 `commons` *➡ 🛠️ 🔢*. + +& 👥 💭 👈 👨‍🎨 💪 🚫 🚚 📚 🐕‍🦺 (💖 🛠️) `dict`Ⓜ, ↩️ 👫 💪 🚫 💭 👫 🔑 & 💲 🆎. + +👥 💪 👍... + +## ⚫️❔ ⚒ 🔗 + +🆙 🔜 👆 ✔️ 👀 🔗 📣 🔢. + +✋️ 👈 🚫 🕴 🌌 📣 🔗 (👐 ⚫️ 🔜 🎲 🌖 ⚠). + +🔑 ⚖ 👈 🔗 🔜 "🇧🇲". + +"**🇧🇲**" 🐍 🕳 👈 🐍 💪 "🤙" 💖 🔢. + +, 🚥 👆 ✔️ 🎚 `something` (👈 💪 _🚫_ 🔢) & 👆 💪 "🤙" ⚫️ (🛠️ ⚫️) 💖: + +```Python +something() +``` + +⚖️ + +```Python +something(some_argument, some_keyword_argument="foo") +``` + +⤴️ ⚫️ "🇧🇲". + +## 🎓 🔗 + +👆 5️⃣📆 👀 👈 ✍ 👐 🐍 🎓, 👆 ⚙️ 👈 🎏 ❕. + +🖼: + +```Python +class Cat: + def __init__(self, name: str): + self.name = name + + +fluffy = Cat(name="Mr Fluffy") +``` + +👉 💼, `fluffy` 👐 🎓 `Cat`. + +& ✍ `fluffy`, 👆 "🤙" `Cat`. + +, 🐍 🎓 **🇧🇲**. + +⤴️, **FastAPI**, 👆 💪 ⚙️ 🐍 🎓 🔗. + +⚫️❔ FastAPI 🤙 ✅ 👈 ⚫️ "🇧🇲" (🔢, 🎓 ⚖️ 🕳 🙆) & 🔢 🔬. + +🚥 👆 🚶‍♀️ "🇧🇲" 🔗 **FastAPI**, ⚫️ 🔜 🔬 🔢 👈 "🇧🇲", & 🛠️ 👫 🎏 🌌 🔢 *➡ 🛠️ 🔢*. ✅ 🎧-🔗. + +👈 ✔ 🇧🇲 ⏮️ 🙅‍♂ 🔢 🌐. 🎏 ⚫️ 🔜 *➡ 🛠️ 🔢* ⏮️ 🙅‍♂ 🔢. + +⤴️, 👥 💪 🔀 🔗 "☑" `common_parameters` ⚪️➡️ 🔛 🎓 `CommonQueryParams`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="9-13" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +💸 🙋 `__init__` 👩‍🔬 ⚙️ ✍ 👐 🎓: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +...⚫️ ✔️ 🎏 🔢 👆 ⏮️ `common_parameters`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="6" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +📚 🔢 ⚫️❔ **FastAPI** 🔜 ⚙️ "❎" 🔗. + +👯‍♂️ 💼, ⚫️ 🔜 ✔️: + +* 📦 `q` 🔢 🔢 👈 `str`. +* `skip` 🔢 🔢 👈 `int`, ⏮️ 🔢 `0`. +* `limit` 🔢 🔢 👈 `int`, ⏮️ 🔢 `100`. + +👯‍♂️ 💼 💽 🔜 🗜, ✔, 📄 🔛 🗄 🔗, ♒️. + +## ⚙️ ⚫️ + +🔜 👆 💪 📣 👆 🔗 ⚙️ 👉 🎓. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +**FastAPI** 🤙 `CommonQueryParams` 🎓. 👉 ✍ "👐" 👈 🎓 & 👐 🔜 🚶‍♀️ 🔢 `commons` 👆 🔢. + +## 🆎 ✍ 🆚 `Depends` + +👀 ❔ 👥 ✍ `CommonQueryParams` 🕐 🔛 📟: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +🏁 `CommonQueryParams`,: + +```Python +... = Depends(CommonQueryParams) +``` + +...⚫️❔ **FastAPI** 🔜 🤙 ⚙️ 💭 ⚫️❔ 🔗. + +⚪️➡️ ⚫️ 👈 FastAPI 🔜 ⚗ 📣 🔢 & 👈 ⚫️❔ FastAPI 🔜 🤙 🤙. + +--- + +👉 💼, 🥇 `CommonQueryParams`,: + +```Python +commons: CommonQueryParams ... +``` + +...🚫 ✔️ 🙆 🎁 🔑 **FastAPI**. FastAPI 🏆 🚫 ⚙️ ⚫️ 💽 🛠️, 🔬, ♒️. (⚫️ ⚙️ `= Depends(CommonQueryParams)` 👈). + +👆 💪 🤙 ✍: + +```Python +commons = Depends(CommonQueryParams) +``` + +...: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial003_py310.py!} + ``` + +✋️ 📣 🆎 💡 👈 🌌 👆 👨‍🎨 🔜 💭 ⚫️❔ 🔜 🚶‍♀️ 🔢 `commons`, & ⤴️ ⚫️ 💪 ℹ 👆 ⏮️ 📟 🛠️, 🆎 ✅, ♒️: + + + +## ⌨ + +✋️ 👆 👀 👈 👥 ✔️ 📟 🔁 📥, ✍ `CommonQueryParams` 🕐: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +**FastAPI** 🚚 ⌨ 👫 💼, 🌐❔ 🔗 *🎯* 🎓 👈 **FastAPI** 🔜 "🤙" ✍ 👐 🎓 ⚫️. + +📚 🎯 💼, 👆 💪 📄: + +↩️ ✍: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +...👆 ✍: + +```Python +commons: CommonQueryParams = Depends() +``` + +👆 📣 🔗 🆎 🔢, & 👆 ⚙️ `Depends()` 🚮 "🔢" 💲 (👈 ⏮️ `=`) 👈 🔢 🔢, 🍵 🙆 🔢 `Depends()`, ↩️ ✔️ ✍ 🌕 🎓 *🔄* 🔘 `Depends(CommonQueryParams)`. + +🎏 🖼 🔜 ⤴️ 👀 💖: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial004_py310.py!} + ``` + +...& **FastAPI** 🔜 💭 ⚫️❔. + +!!! tip + 🚥 👈 😑 🌅 😨 🌘 👍, 🤷‍♂ ⚫️, 👆 🚫 *💪* ⚫️. + + ⚫️ ⌨. ↩️ **FastAPI** 💅 🔃 🤝 👆 📉 📟 🔁. diff --git a/docs/em/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/em/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md new file mode 100644 index 0000000000..4d54b91c77 --- /dev/null +++ b/docs/em/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -0,0 +1,71 @@ +# 🔗 ➡ 🛠️ 👨‍🎨 + +💼 👆 🚫 🤙 💪 📨 💲 🔗 🔘 👆 *➡ 🛠️ 🔢*. + +⚖️ 🔗 🚫 📨 💲. + +✋️ 👆 💪 ⚫️ 🛠️/❎. + +📚 💼, ↩️ 📣 *➡ 🛠️ 🔢* 🔢 ⏮️ `Depends`, 👆 💪 🚮 `list` `dependencies` *➡ 🛠️ 👨‍🎨*. + +## 🚮 `dependencies` *➡ 🛠️ 👨‍🎨* + +*➡ 🛠️ 👨‍🎨* 📨 📦 ❌ `dependencies`. + +⚫️ 🔜 `list` `Depends()`: + +```Python hl_lines="17" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +👉 🔗 🔜 🛠️/❎ 🎏 🌌 😐 🔗. ✋️ 👫 💲 (🚥 👫 📨 🙆) 🏆 🚫 🚶‍♀️ 👆 *➡ 🛠️ 🔢*. + +!!! tip + 👨‍🎨 ✅ ♻ 🔢 🔢, & 🎦 👫 ❌. + + ⚙️ 👉 `dependencies` *➡ 🛠️ 👨‍🎨* 👆 💪 ⚒ 💭 👫 🛠️ ⏪ ❎ 👨‍🎨/🏭 ❌. + + ⚫️ 💪 ℹ ❎ 😨 🆕 👩‍💻 👈 👀 ♻ 🔢 👆 📟 & 💪 💭 ⚫️ 🙃. + +!!! info + 👉 🖼 👥 ⚙️ 💭 🛃 🎚 `X-Key` & `X-Token`. + + ✋️ 🎰 💼, 🕐❔ 🛠️ 💂‍♂, 👆 🔜 🤚 🌖 💰 ⚪️➡️ ⚙️ 🛠️ [💂‍♂ 🚙 (⏭ 📃)](../security/index.md){.internal-link target=_blank}. + +## 🔗 ❌ & 📨 💲 + +👆 💪 ⚙️ 🎏 🔗 *🔢* 👆 ⚙️ 🛎. + +### 🔗 📄 + +👫 💪 📣 📨 📄 (💖 🎚) ⚖️ 🎏 🎧-🔗: + +```Python hl_lines="6 11" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +### 🤚 ⚠ + +👫 🔗 💪 `raise` ⚠, 🎏 😐 🔗: + +```Python hl_lines="8 13" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +### 📨 💲 + +& 👫 💪 📨 💲 ⚖️ 🚫, 💲 🏆 🚫 ⚙️. + +, 👆 💪 🏤-⚙️ 😐 🔗 (👈 📨 💲) 👆 ⏪ ⚙️ 👱 🙆, & ✋️ 💲 🏆 🚫 ⚙️, 🔗 🔜 🛠️: + +```Python hl_lines="9 14" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +## 🔗 👪 *➡ 🛠️* + +⏪, 🕐❔ 👂 🔃 ❔ 📊 🦏 🈸 ([🦏 🈸 - 💗 📁](../../tutorial/bigger-applications.md){.internal-link target=_blank}), 🎲 ⏮️ 💗 📁, 👆 🔜 💡 ❔ 📣 👁 `dependencies` 🔢 👪 *➡ 🛠️*. + +## 🌐 🔗 + +⏭ 👥 🔜 👀 ❔ 🚮 🔗 🎂 `FastAPI` 🈸, 👈 👫 ✔ 🔠 *➡ 🛠️*. diff --git a/docs/em/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/em/docs/tutorial/dependencies/dependencies-with-yield.md new file mode 100644 index 0000000000..9617667f43 --- /dev/null +++ b/docs/em/docs/tutorial/dependencies/dependencies-with-yield.md @@ -0,0 +1,219 @@ +# 🔗 ⏮️ 🌾 + +FastAPI 🐕‍🦺 🔗 👈 ➕ 🔁 ⏮️ 🏁. + +👉, ⚙️ `yield` ↩️ `return`, & ✍ ➕ 🔁 ⏮️. + +!!! tip + ⚒ 💭 ⚙️ `yield` 1️⃣ 👁 🕰. + +!!! note "📡 ℹ" + 🙆 🔢 👈 ☑ ⚙️ ⏮️: + + * `@contextlib.contextmanager` ⚖️ + * `@contextlib.asynccontextmanager` + + 🔜 ☑ ⚙️ **FastAPI** 🔗. + + 👐, FastAPI ⚙️ 📚 2️⃣ 👨‍🎨 🔘. + +## 💽 🔗 ⏮️ `yield` + +🖼, 👆 💪 ⚙️ 👉 ✍ 💽 🎉 & 🔐 ⚫️ ⏮️ 🏁. + +🕴 📟 ⏭ & 🔌 `yield` 📄 🛠️ ⏭ 📨 📨: + +```Python hl_lines="2-4" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +🌾 💲 ⚫️❔ 💉 🔘 *➡ 🛠️* & 🎏 🔗: + +```Python hl_lines="4" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +📟 📄 `yield` 📄 🛠️ ⏮️ 📨 ✔️ 🚚: + +```Python hl_lines="5-6" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +!!! tip + 👆 💪 ⚙️ `async` ⚖️ 😐 🔢. + + **FastAPI** 🔜 ▶️️ 👜 ⏮️ 🔠, 🎏 ⏮️ 😐 🔗. + +## 🔗 ⏮️ `yield` & `try` + +🚥 👆 ⚙️ `try` 🍫 🔗 ⏮️ `yield`, 👆 🔜 📨 🙆 ⚠ 👈 🚮 🕐❔ ⚙️ 🔗. + +🖼, 🚥 📟 ☝ 🖕, ➕1️⃣ 🔗 ⚖️ *➡ 🛠️*, ⚒ 💽 💵 "💾" ⚖️ ✍ 🙆 🎏 ❌, 👆 🔜 📨 ⚠ 👆 🔗. + +, 👆 💪 👀 👈 🎯 ⚠ 🔘 🔗 ⏮️ `except SomeException`. + +🎏 🌌, 👆 💪 ⚙️ `finally` ⚒ 💭 🚪 📶 🛠️, 🙅‍♂ 🤔 🚥 📤 ⚠ ⚖️ 🚫. + +```Python hl_lines="3 5" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +## 🎧-🔗 ⏮️ `yield` + +👆 💪 ✔️ 🎧-🔗 & "🌲" 🎧-🔗 🙆 📐 & 💠, & 🙆 ⚖️ 🌐 👫 💪 ⚙️ `yield`. + +**FastAPI** 🔜 ⚒ 💭 👈 "🚪 📟" 🔠 🔗 ⏮️ `yield` 🏃 ☑ ✔. + +🖼, `dependency_c` 💪 ✔️ 🔗 🔛 `dependency_b`, & `dependency_b` 🔛 `dependency_a`: + +```Python hl_lines="4 12 20" +{!../../../docs_src/dependencies/tutorial008.py!} +``` + +& 🌐 👫 💪 ⚙️ `yield`. + +👉 💼 `dependency_c`, 🛠️ 🚮 🚪 📟, 💪 💲 ⚪️➡️ `dependency_b` (📥 📛 `dep_b`) 💪. + +& , 🔄, `dependency_b` 💪 💲 ⚪️➡️ `dependency_a` (📥 📛 `dep_a`) 💪 🚮 🚪 📟. + +```Python hl_lines="16-17 24-25" +{!../../../docs_src/dependencies/tutorial008.py!} +``` + +🎏 🌌, 👆 💪 ✔️ 🔗 ⏮️ `yield` & `return` 🌀. + +& 👆 💪 ✔️ 👁 🔗 👈 🚚 📚 🎏 🔗 ⏮️ `yield`, ♒️. + +👆 💪 ✔️ 🙆 🌀 🔗 👈 👆 💚. + +**FastAPI** 🔜 ⚒ 💭 🌐 🏃 ☑ ✔. + +!!! note "📡 ℹ" + 👉 👷 👏 🐍 🔑 👨‍💼. + + **FastAPI** ⚙️ 👫 🔘 🏆 👉. + +## 🔗 ⏮️ `yield` & `HTTPException` + +👆 👀 👈 👆 💪 ⚙️ 🔗 ⏮️ `yield` & ✔️ `try` 🍫 👈 ✊ ⚠. + +⚫️ 5️⃣📆 😋 🤚 `HTTPException` ⚖️ 🎏 🚪 📟, ⏮️ `yield`. ✋️ **⚫️ 🏆 🚫 👷**. + +🚪 📟 🔗 ⏮️ `yield` 🛠️ *⏮️* 📨 📨, [⚠ 🐕‍🦺](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} 🔜 ✔️ ⏪ 🏃. 📤 🕳 😽 ⚠ 🚮 👆 🔗 🚪 📟 (⏮️ `yield`). + +, 🚥 👆 🤚 `HTTPException` ⏮️ `yield`, 🔢 (⚖️ 🙆 🛃) ⚠ 🐕‍🦺 👈 ✊ `HTTPException`Ⓜ & 📨 🇺🇸🔍 4️⃣0️⃣0️⃣ 📨 🏆 🚫 📤 ✊ 👈 ⚠ 🚫🔜. + +👉 ⚫️❔ ✔ 🕳 ⚒ 🔗 (✅ 💽 🎉), 🖼, ⚙️ 🖥 📋. + +🖥 📋 🏃 *⏮️* 📨 ✔️ 📨. 📤 🙅‍♂ 🌌 🤚 `HTTPException` ↩️ 📤 🚫 🌌 🔀 📨 👈 *⏪ 📨*. + +✋️ 🚥 🖥 📋 ✍ 💽 ❌, 🌘 👆 💪 💾 ⚖️ 😬 🔐 🎉 🔗 ⏮️ `yield`, & 🎲 🕹 ❌ ⚖️ 📄 ⚫️ 🛰 🕵 ⚙️. + +🚥 👆 ✔️ 📟 👈 👆 💭 💪 🤚 ⚠, 🏆 😐/"🙃" 👜 & 🚮 `try` 🍫 👈 📄 📟. + +🚥 👆 ✔️ 🛃 ⚠ 👈 👆 🔜 💖 🍵 *⏭* 🛬 📨 & 🎲 ❎ 📨, 🎲 🙋‍♀ `HTTPException`, ✍ [🛃 ⚠ 🐕‍🦺](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. + +!!! tip + 👆 💪 🤚 ⚠ 🔌 `HTTPException` *⏭* `yield`. ✋️ 🚫 ⏮️. + +🔁 🛠️ 🌅 ⚖️ 🌘 💖 👉 📊. 🕰 💧 ⚪️➡️ 🔝 🔝. & 🔠 🏓 1️⃣ 🍕 🔗 ⚖️ 🛠️ 📟. + +```mermaid +sequenceDiagram + +participant client as Client +participant handler as Exception handler +participant dep as Dep with yield +participant operation as Path Operation +participant tasks as Background tasks + + Note over client,tasks: Can raise exception for dependency, handled after response is sent + Note over client,operation: Can raise HTTPException and can change the response + client ->> dep: Start request + Note over dep: Run code up to yield + opt raise + dep -->> handler: Raise HTTPException + handler -->> client: HTTP error response + dep -->> dep: Raise other exception + end + dep ->> operation: Run dependency, e.g. DB session + opt raise + operation -->> dep: Raise HTTPException + dep -->> handler: Auto forward exception + handler -->> client: HTTP error response + operation -->> dep: Raise other exception + dep -->> handler: Auto forward exception + end + operation ->> client: Return response to client + Note over client,operation: Response is already sent, can't change it anymore + opt Tasks + operation -->> tasks: Send background tasks + end + opt Raise other exception + tasks -->> dep: Raise other exception + end + Note over dep: After yield + opt Handle other exception + dep -->> dep: Handle exception, can't change response. E.g. close DB session. + end +``` + +!!! info + 🕴 **1️⃣ 📨** 🔜 📨 👩‍💻. ⚫️ 💪 1️⃣ ❌ 📨 ⚖️ ⚫️ 🔜 📨 ⚪️➡️ *➡ 🛠️*. + + ⏮️ 1️⃣ 📚 📨 📨, 🙅‍♂ 🎏 📨 💪 📨. + +!!! tip + 👉 📊 🎦 `HTTPException`, ✋️ 👆 💪 🤚 🙆 🎏 ⚠ ❔ 👆 ✍ [🛃 ⚠ 🐕‍🦺](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. + + 🚥 👆 🤚 🙆 ⚠, ⚫️ 🔜 🚶‍♀️ 🔗 ⏮️ 🌾, 🔌 `HTTPException`, & ⤴️ **🔄** ⚠ 🐕‍🦺. 🚥 📤 🙅‍♂ ⚠ 🐕‍🦺 👈 ⚠, ⚫️ 🔜 ⤴️ 🍵 🔢 🔗 `ServerErrorMiddleware`, 🛬 5️⃣0️⃣0️⃣ 🇺🇸🔍 👔 📟, ➡️ 👩‍💻 💭 👈 📤 ❌ 💽. + +## 🔑 👨‍💼 + +### ⚫️❔ "🔑 👨‍💼" + +"🔑 👨‍💼" 🙆 👈 🐍 🎚 👈 👆 💪 ⚙️ `with` 📄. + +🖼, 👆 💪 ⚙️ `with` ✍ 📁: + +```Python +with open("./somefile.txt") as f: + contents = f.read() + print(contents) +``` + +🔘, `open("./somefile.txt")` ✍ 🎚 👈 🤙 "🔑 👨‍💼". + +🕐❔ `with` 🍫 🏁, ⚫️ ⚒ 💭 🔐 📁, 🚥 📤 ⚠. + +🕐❔ 👆 ✍ 🔗 ⏮️ `yield`, **FastAPI** 🔜 🔘 🗜 ⚫️ 🔑 👨‍💼, & 🌀 ⚫️ ⏮️ 🎏 🔗 🧰. + +### ⚙️ 🔑 👨‍💼 🔗 ⏮️ `yield` + +!!! warning + 👉, 🌅 ⚖️ 🌘, "🏧" 💭. + + 🚥 👆 ▶️ ⏮️ **FastAPI** 👆 💪 💚 🚶 ⚫️ 🔜. + +🐍, 👆 💪 ✍ 🔑 👨‍💼 🏗 🎓 ⏮️ 2️⃣ 👩‍🔬: `__enter__()` & `__exit__()`. + +👆 💪 ⚙️ 👫 🔘 **FastAPI** 🔗 ⏮️ `yield` ⚙️ +`with` ⚖️ `async with` 📄 🔘 🔗 🔢: + +```Python hl_lines="1-9 13" +{!../../../docs_src/dependencies/tutorial010.py!} +``` + +!!! tip + ➕1️⃣ 🌌 ✍ 🔑 👨‍💼 ⏮️: + + * `@contextlib.contextmanager` ⚖️ + * `@contextlib.asynccontextmanager` + + ⚙️ 👫 🎀 🔢 ⏮️ 👁 `yield`. + + 👈 ⚫️❔ **FastAPI** ⚙️ 🔘 🔗 ⏮️ `yield`. + + ✋️ 👆 🚫 ✔️ ⚙️ 👨‍🎨 FastAPI 🔗 (& 👆 🚫🔜 🚫). + + FastAPI 🔜 ⚫️ 👆 🔘. diff --git a/docs/em/docs/tutorial/dependencies/global-dependencies.md b/docs/em/docs/tutorial/dependencies/global-dependencies.md new file mode 100644 index 0000000000..81759d0e83 --- /dev/null +++ b/docs/em/docs/tutorial/dependencies/global-dependencies.md @@ -0,0 +1,17 @@ +# 🌐 🔗 + +🆎 🈸 👆 💪 💚 🚮 🔗 🎂 🈸. + +🎏 🌌 👆 💪 [🚮 `dependencies` *➡ 🛠️ 👨‍🎨*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, 👆 💪 🚮 👫 `FastAPI` 🈸. + +👈 💼, 👫 🔜 ✔ 🌐 *➡ 🛠️* 🈸: + +```Python hl_lines="15" +{!../../../docs_src/dependencies/tutorial012.py!} +``` + +& 🌐 💭 📄 🔃 [❎ `dependencies` *➡ 🛠️ 👨‍🎨*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} ✔, ✋️ 👉 💼, 🌐 *➡ 🛠️* 📱. + +## 🔗 👪 *➡ 🛠️* + +⏪, 🕐❔ 👂 🔃 ❔ 📊 🦏 🈸 ([🦏 🈸 - 💗 📁](../../tutorial/bigger-applications.md){.internal-link target=_blank}), 🎲 ⏮️ 💗 📁, 👆 🔜 💡 ❔ 📣 👁 `dependencies` 🔢 👪 *➡ 🛠️*. diff --git a/docs/em/docs/tutorial/dependencies/index.md b/docs/em/docs/tutorial/dependencies/index.md new file mode 100644 index 0000000000..ffd38d7168 --- /dev/null +++ b/docs/em/docs/tutorial/dependencies/index.md @@ -0,0 +1,233 @@ +# 🔗 + +**FastAPI** ✔️ 📶 🏋️ ✋️ 🏋️ **🔗 💉** ⚙️. + +⚫️ 🏗 📶 🙅 ⚙️, & ⚒ ⚫️ 📶 ⏩ 🙆 👩‍💻 🛠️ 🎏 🦲 ⏮️ **FastAPI**. + +## ⚫️❔ "🔗 💉" + +**"🔗 💉"** ⛓, 📋, 👈 📤 🌌 👆 📟 (👉 💼, 👆 *➡ 🛠️ 🔢*) 📣 👜 👈 ⚫️ 🚚 👷 & ⚙️: "🔗". + +& ⤴️, 👈 ⚙️ (👉 💼 **FastAPI**) 🔜 ✊ 💅 🔨 ⚫️❔ 💪 🚚 👆 📟 ⏮️ 📚 💪 🔗 ("💉" 🔗). + +👉 📶 ⚠ 🕐❔ 👆 💪: + +* ✔️ 💰 ⚛ (🎏 📟 ⚛ 🔄 & 🔄). +* 💰 💽 🔗. +* 🛠️ 💂‍♂, 🤝, 🔑 📄, ♒️. +* & 📚 🎏 👜... + +🌐 👫, ⏪ 📉 📟 🔁. + +## 🥇 🔁 + +➡️ 👀 📶 🙅 🖼. ⚫️ 🔜 🙅 👈 ⚫️ 🚫 📶 ⚠, 🔜. + +✋️ 👉 🌌 👥 💪 🎯 🔛 ❔ **🔗 💉** ⚙️ 👷. + +### ✍ 🔗, ⚖️ "☑" + +➡️ 🥇 🎯 🔛 🔗. + +⚫️ 🔢 👈 💪 ✊ 🌐 🎏 🔢 👈 *➡ 🛠️ 🔢* 💪 ✊: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="8-11" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="6-7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +👈 ⚫️. + +**2️⃣ ⏸**. + +& ⚫️ ✔️ 🎏 💠 & 📊 👈 🌐 👆 *➡ 🛠️ 🔢* ✔️. + +👆 💪 💭 ⚫️ *➡ 🛠️ 🔢* 🍵 "👨‍🎨" (🍵 `@app.get("/some-path")`). + +& ⚫️ 💪 📨 🕳 👆 💚. + +👉 💼, 👉 🔗 ⌛: + +* 📦 🔢 🔢 `q` 👈 `str`. +* 📦 🔢 🔢 `skip` 👈 `int`, & 🔢 `0`. +* 📦 🔢 🔢 `limit` 👈 `int`, & 🔢 `100`. + +& ⤴️ ⚫️ 📨 `dict` ⚗ 📚 💲. + +### 🗄 `Depends` + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +### 📣 🔗, "⚓️" + +🎏 🌌 👆 ⚙️ `Body`, `Query`, ♒️. ⏮️ 👆 *➡ 🛠️ 🔢* 🔢, ⚙️ `Depends` ⏮️ 🆕 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="15 20" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="11 16" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +👐 👆 ⚙️ `Depends` 🔢 👆 🔢 🎏 🌌 👆 ⚙️ `Body`, `Query`, ♒️, `Depends` 👷 👄 🎏. + +👆 🕴 🤝 `Depends` 👁 🔢. + +👉 🔢 🔜 🕳 💖 🔢. + +& 👈 🔢 ✊ 🔢 🎏 🌌 👈 *➡ 🛠️ 🔢* . + +!!! tip + 👆 🔜 👀 ⚫️❔ 🎏 "👜", ↖️ ⚪️➡️ 🔢, 💪 ⚙️ 🔗 ⏭ 📃. + +🕐❔ 🆕 📨 🛬, **FastAPI** 🔜 ✊ 💅: + +* 🤙 👆 🔗 ("☑") 🔢 ⏮️ ☑ 🔢. +* 🤚 🏁 ⚪️➡️ 👆 🔢. +* 🛠️ 👈 🏁 🔢 👆 *➡ 🛠️ 🔢*. + +```mermaid +graph TB + +common_parameters(["common_parameters"]) +read_items["/items/"] +read_users["/users/"] + +common_parameters --> read_items +common_parameters --> read_users +``` + +👉 🌌 👆 ✍ 🔗 📟 🕐 & **FastAPI** ✊ 💅 🤙 ⚫️ 👆 *➡ 🛠️*. + +!!! check + 👀 👈 👆 🚫 ✔️ ✍ 🎁 🎓 & 🚶‍♀️ ⚫️ 👱 **FastAPI** "®" ⚫️ ⚖️ 🕳 🎏. + + 👆 🚶‍♀️ ⚫️ `Depends` & **FastAPI** 💭 ❔ 🎂. + +## `async` ⚖️ 🚫 `async` + +🔗 🔜 🤙 **FastAPI** (🎏 👆 *➡ 🛠️ 🔢*), 🎏 🚫 ✔ ⏪ 🔬 👆 🔢. + +👆 💪 ⚙️ `async def` ⚖️ 😐 `def`. + +& 👆 💪 📣 🔗 ⏮️ `async def` 🔘 😐 `def` *➡ 🛠️ 🔢*, ⚖️ `def` 🔗 🔘 `async def` *➡ 🛠️ 🔢*, ♒️. + +⚫️ 🚫 🤔. **FastAPI** 🔜 💭 ⚫️❔. + +!!! note + 🚥 👆 🚫 💭, ✅ [🔁: *"🏃 ❓" *](../../async.md){.internal-link target=_blank} 📄 🔃 `async` & `await` 🩺. + +## 🛠️ ⏮️ 🗄 + +🌐 📨 📄, 🔬 & 📄 👆 🔗 (& 🎧-🔗) 🔜 🛠️ 🎏 🗄 🔗. + +, 🎓 🩺 🔜 ✔️ 🌐 ℹ ⚪️➡️ 👫 🔗 💁‍♂️: + + + +## 🙅 ⚙️ + +🚥 👆 👀 ⚫️, *➡ 🛠️ 🔢* 📣 ⚙️ 🕐❔ *➡* & *🛠️* 🏏, & ⤴️ **FastAPI** ✊ 💅 🤙 🔢 ⏮️ ☑ 🔢, ❎ 📊 ⚪️➡️ 📨. + +🤙, 🌐 (⚖️ 🏆) 🕸 🛠️ 👷 👉 🎏 🌌. + +👆 🙅 🤙 👈 🔢 🔗. 👫 🤙 👆 🛠️ (👉 💼, **FastAPI**). + +⏮️ 🔗 💉 ⚙️, 👆 💪 💬 **FastAPI** 👈 👆 *➡ 🛠️ 🔢* "🪀" 🔛 🕳 🙆 👈 🔜 🛠️ ⏭ 👆 *➡ 🛠️ 🔢*, & **FastAPI** 🔜 ✊ 💅 🛠️ ⚫️ & "💉" 🏁. + +🎏 ⚠ ⚖ 👉 🎏 💭 "🔗 💉": + +* ℹ +* 🐕‍🦺 +* 🐕‍🦺 +* 💉 +* 🦲 + +## **FastAPI** 🔌-🔌 + +🛠️ & "🔌-"Ⓜ 💪 🏗 ⚙️ **🔗 💉** ⚙️. ✋️ 👐, 📤 🤙 **🙅‍♂ 💪 ✍ "🔌-🔌"**, ⚙️ 🔗 ⚫️ 💪 📣 ♾ 🔢 🛠️ & 🔗 👈 ▶️️ 💪 👆 *➡ 🛠️ 🔢*. + +& 🔗 💪 ✍ 📶 🙅 & 🏋️ 🌌 👈 ✔ 👆 🗄 🐍 📦 👆 💪, & 🛠️ 👫 ⏮️ 👆 🛠️ 🔢 👩‍❤‍👨 ⏸ 📟, *🌖*. + +👆 🔜 👀 🖼 👉 ⏭ 📃, 🔃 🔗 & ☁ 💽, 💂‍♂, ♒️. + +## **FastAPI** 🔗 + +🦁 🔗 💉 ⚙️ ⚒ **FastAPI** 🔗 ⏮️: + +* 🌐 🔗 💽 +* ☁ 💽 +* 🔢 📦 +* 🔢 🔗 +* 🤝 & ✔ ⚙️ +* 🛠️ ⚙️ ⚖ ⚙️ +* 📨 💽 💉 ⚙️ +* ♒️. + +## 🙅 & 🏋️ + +👐 🔗 🔗 💉 ⚙️ 📶 🙅 🔬 & ⚙️, ⚫️ 📶 🏋️. + +👆 💪 🔬 🔗 👈 🔄 💪 🔬 🔗 👫. + +🔚, 🔗 🌲 🔗 🏗, & **🔗 💉** ⚙️ ✊ 💅 🔬 🌐 👉 🔗 👆 (& 👫 🎧-🔗) & 🚚 (💉) 🏁 🔠 🔁. + +🖼, ➡️ 💬 👆 ✔️ 4️⃣ 🛠️ 🔗 (*➡ 🛠️*): + +* `/items/public/` +* `/items/private/` +* `/users/{user_id}/activate` +* `/items/pro/` + +⤴️ 👆 💪 🚮 🎏 ✔ 📄 🔠 👫 ⏮️ 🔗 & 🎧-🔗: + +```mermaid +graph TB + +current_user(["current_user"]) +active_user(["active_user"]) +admin_user(["admin_user"]) +paying_user(["paying_user"]) + +public["/items/public/"] +private["/items/private/"] +activate_user["/users/{user_id}/activate"] +pro_items["/items/pro/"] + +current_user --> active_user +active_user --> admin_user +active_user --> paying_user + +current_user --> public +active_user --> private +admin_user --> activate_user +paying_user --> pro_items +``` + +## 🛠️ ⏮️ **🗄** + +🌐 👫 🔗, ⏪ 📣 👫 📄, 🚮 🔢, 🔬, ♒️. 👆 *➡ 🛠️*. + +**FastAPI** 🔜 ✊ 💅 🚮 ⚫️ 🌐 🗄 🔗, 👈 ⚫️ 🎦 🎓 🧾 ⚙️. diff --git a/docs/em/docs/tutorial/dependencies/sub-dependencies.md b/docs/em/docs/tutorial/dependencies/sub-dependencies.md new file mode 100644 index 0000000000..454ff51298 --- /dev/null +++ b/docs/em/docs/tutorial/dependencies/sub-dependencies.md @@ -0,0 +1,110 @@ +# 🎧-🔗 + +👆 💪 ✍ 🔗 👈 ✔️ **🎧-🔗**. + +👫 💪 **⏬** 👆 💪 👫. + +**FastAPI** 🔜 ✊ 💅 🔬 👫. + +## 🥇 🔗 "☑" + +👆 💪 ✍ 🥇 🔗 ("☑") 💖: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="6-7" + {!> ../../../docs_src/dependencies/tutorial005_py310.py!} + ``` + +⚫️ 📣 📦 🔢 🔢 `q` `str`, & ⤴️ ⚫️ 📨 ⚫️. + +👉 🙅 (🚫 📶 ⚠), ✋️ 🔜 ℹ 👥 🎯 🔛 ❔ 🎧-🔗 👷. + +## 🥈 🔗, "☑" & "⚓️" + +⤴️ 👆 💪 ✍ ➕1️⃣ 🔗 🔢 ("☑") 👈 🎏 🕰 📣 🔗 🚮 👍 (⚫️ "⚓️" 💁‍♂️): + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial005_py310.py!} + ``` + +➡️ 🎯 🔛 🔢 📣: + +* ✋️ 👉 🔢 🔗 ("☑") ⚫️, ⚫️ 📣 ➕1️⃣ 🔗 (⚫️ "🪀" 🔛 🕳 🙆). + * ⚫️ 🪀 🔛 `query_extractor`, & 🛠️ 💲 📨 ⚫️ 🔢 `q`. +* ⚫️ 📣 📦 `last_query` 🍪, `str`. + * 🚥 👩‍💻 🚫 🚚 🙆 🔢 `q`, 👥 ⚙️ 🏁 🔢 ⚙️, ❔ 👥 🖊 🍪 ⏭. + +## ⚙️ 🔗 + +⤴️ 👥 💪 ⚙️ 🔗 ⏮️: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="22" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial005_py310.py!} + ``` + +!!! info + 👀 👈 👥 🕴 📣 1️⃣ 🔗 *➡ 🛠️ 🔢*, `query_or_cookie_extractor`. + + ✋️ **FastAPI** 🔜 💭 👈 ⚫️ ✔️ ❎ `query_extractor` 🥇, 🚶‍♀️ 🏁 👈 `query_or_cookie_extractor` ⏪ 🤙 ⚫️. + +```mermaid +graph TB + +query_extractor(["query_extractor"]) +query_or_cookie_extractor(["query_or_cookie_extractor"]) + +read_query["/items/"] + +query_extractor --> query_or_cookie_extractor --> read_query +``` + +## ⚙️ 🎏 🔗 💗 🕰 + +🚥 1️⃣ 👆 🔗 📣 💗 🕰 🎏 *➡ 🛠️*, 🖼, 💗 🔗 ✔️ ⚠ 🎧-🔗, **FastAPI** 🔜 💭 🤙 👈 🎧-🔗 🕴 🕐 📍 📨. + +& ⚫️ 🔜 🖊 📨 💲 "💾" & 🚶‍♀️ ⚫️ 🌐 "⚓️" 👈 💪 ⚫️ 👈 🎯 📨, ↩️ 🤙 🔗 💗 🕰 🎏 📨. + +🏧 😐 🌐❔ 👆 💭 👆 💪 🔗 🤙 🔠 🔁 (🎲 💗 🕰) 🎏 📨 ↩️ ⚙️ "💾" 💲, 👆 💪 ⚒ 🔢 `use_cache=False` 🕐❔ ⚙️ `Depends`: + +```Python hl_lines="1" +async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): + return {"fresh_value": fresh_value} +``` + +## 🌃 + +↖️ ⚪️➡️ 🌐 🎀 🔤 ⚙️ 📥, **🔗 💉** ⚙️ 🙅. + +🔢 👈 👀 🎏 *➡ 🛠️ 🔢*. + +✋️, ⚫️ 📶 🏋️, & ✔ 👆 📣 🎲 🙇 🐦 🔗 "📊" (🌲). + +!!! tip + 🌐 👉 💪 🚫 😑 ⚠ ⏮️ 👫 🙅 🖼. + + ✋️ 👆 🔜 👀 ❔ ⚠ ⚫️ 📃 🔃 **💂‍♂**. + + & 👆 🔜 👀 💸 📟 ⚫️ 🔜 🖊 👆. diff --git a/docs/em/docs/tutorial/encoder.md b/docs/em/docs/tutorial/encoder.md new file mode 100644 index 0000000000..75ca3824da --- /dev/null +++ b/docs/em/docs/tutorial/encoder.md @@ -0,0 +1,42 @@ +# 🎻 🔗 🔢 + +📤 💼 🌐❔ 👆 5️⃣📆 💪 🗜 💽 🆎 (💖 Pydantic 🏷) 🕳 🔗 ⏮️ 🎻 (💖 `dict`, `list`, ♒️). + +🖼, 🚥 👆 💪 🏪 ⚫️ 💽. + +👈, **FastAPI** 🚚 `jsonable_encoder()` 🔢. + +## ⚙️ `jsonable_encoder` + +➡️ 🌈 👈 👆 ✔️ 💽 `fake_db` 👈 🕴 📨 🎻 🔗 💽. + +🖼, ⚫️ 🚫 📨 `datetime` 🎚, 👈 🚫 🔗 ⏮️ 🎻. + +, `datetime` 🎚 🔜 ✔️ 🗜 `str` ⚗ 💽 💾 📁. + +🎏 🌌, 👉 💽 🚫🔜 📨 Pydantic 🏷 (🎚 ⏮️ 🔢), 🕴 `dict`. + +👆 💪 ⚙️ `jsonable_encoder` 👈. + +⚫️ 📨 🎚, 💖 Pydantic 🏷, & 📨 🎻 🔗 ⏬: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="4 21" + {!> ../../../docs_src/encoder/tutorial001_py310.py!} + ``` + +👉 🖼, ⚫️ 🔜 🗜 Pydantic 🏷 `dict`, & `datetime` `str`. + +🏁 🤙 ⚫️ 🕳 👈 💪 🗜 ⏮️ 🐍 🐩 `json.dumps()`. + +⚫️ 🚫 📨 ⭕ `str` ⚗ 💽 🎻 📁 (🎻). ⚫️ 📨 🐍 🐩 💽 📊 (✅ `dict`) ⏮️ 💲 & 🎧-💲 👈 🌐 🔗 ⏮️ 🎻. + +!!! note + `jsonable_encoder` 🤙 ⚙️ **FastAPI** 🔘 🗜 💽. ✋️ ⚫️ ⚠ 📚 🎏 😐. diff --git a/docs/em/docs/tutorial/extra-data-types.md b/docs/em/docs/tutorial/extra-data-types.md new file mode 100644 index 0000000000..dfdf6141b8 --- /dev/null +++ b/docs/em/docs/tutorial/extra-data-types.md @@ -0,0 +1,82 @@ +# ➕ 💽 🆎 + +🆙 🔜, 👆 ✔️ ⚙️ ⚠ 📊 🆎, 💖: + +* `int` +* `float` +* `str` +* `bool` + +✋️ 👆 💪 ⚙️ 🌅 🏗 📊 🆎. + +& 👆 🔜 ✔️ 🎏 ⚒ 👀 🆙 🔜: + +* 👑 👨‍🎨 🐕‍🦺. +* 💽 🛠️ ⚪️➡️ 📨 📨. +* 💽 🛠️ 📨 💽. +* 💽 🔬. +* 🏧 ✍ & 🧾. + +## 🎏 💽 🆎 + +📥 🌖 📊 🆎 👆 💪 ⚙️: + +* `UUID`: + * 🐩 "⭐ 😍 🆔", ⚠ 🆔 📚 💽 & ⚙️. + * 📨 & 📨 🔜 🎨 `str`. +* `datetime.datetime`: + * 🐍 `datetime.datetime`. + * 📨 & 📨 🔜 🎨 `str` 💾 8️⃣6️⃣0️⃣1️⃣ 📁, 💖: `2008-09-15T15:53:00+05:00`. +* `datetime.date`: + * 🐍 `datetime.date`. + * 📨 & 📨 🔜 🎨 `str` 💾 8️⃣6️⃣0️⃣1️⃣ 📁, 💖: `2008-09-15`. +* `datetime.time`: + * 🐍 `datetime.time`. + * 📨 & 📨 🔜 🎨 `str` 💾 8️⃣6️⃣0️⃣1️⃣ 📁, 💖: `14:23:55.003`. +* `datetime.timedelta`: + * 🐍 `datetime.timedelta`. + * 📨 & 📨 🔜 🎨 `float` 🌐 🥈. + * Pydantic ✔ 🎦 ⚫️ "💾 8️⃣6️⃣0️⃣1️⃣ 🕰 ➕ 🔢", 👀 🩺 🌅 ℹ. +* `frozenset`: + * 📨 & 📨, 😥 🎏 `set`: + * 📨, 📇 🔜 ✍, ❎ ❎ & 🏭 ⚫️ `set`. + * 📨, `set` 🔜 🗜 `list`. + * 🏗 🔗 🔜 ✔ 👈 `set` 💲 😍 (⚙️ 🎻 🔗 `uniqueItems`). +* `bytes`: + * 🐩 🐍 `bytes`. + * 📨 & 📨 🔜 😥 `str`. + * 🏗 🔗 🔜 ✔ 👈 ⚫️ `str` ⏮️ `binary` "📁". +* `Decimal`: + * 🐩 🐍 `Decimal`. + * 📨 & 📨, 🍵 🎏 `float`. +* 👆 💪 ✅ 🌐 ☑ Pydantic 📊 🆎 📥: Pydantic 📊 🆎. + +## 🖼 + +📥 🖼 *➡ 🛠️* ⏮️ 🔢 ⚙️ 🔛 🆎. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1 2 11-15" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` + +🗒 👈 🔢 🔘 🔢 ✔️ 👫 🐠 💽 🆎, & 👆 💪, 🖼, 🎭 😐 📅 🎭, 💖: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` diff --git a/docs/em/docs/tutorial/extra-models.md b/docs/em/docs/tutorial/extra-models.md new file mode 100644 index 0000000000..06c36285d3 --- /dev/null +++ b/docs/em/docs/tutorial/extra-models.md @@ -0,0 +1,252 @@ +# ➕ 🏷 + +▶️ ⏮️ ⏮️ 🖼, ⚫️ 🔜 ⚠ ✔️ 🌅 🌘 1️⃣ 🔗 🏷. + +👉 ✴️ 💼 👩‍💻 🏷, ↩️: + +* **🔢 🏷** 💪 💪 ✔️ 🔐. +* **🔢 🏷** 🔜 🚫 ✔️ 🔐. +* **💽 🏷** 🔜 🎲 💪 ✔️ #️⃣ 🔐. + +!!! danger + 🙅 🏪 👩‍💻 🔢 🔐. 🕧 🏪 "🔐 #️⃣" 👈 👆 💪 ⤴️ ✔. + + 🚥 👆 🚫 💭, 👆 🔜 💡 ⚫️❔ "🔐#️⃣" [💂‍♂ 📃](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}. + +## 💗 🏷 + +📥 🏢 💭 ❔ 🏷 💪 👀 💖 ⏮️ 👫 🔐 🏑 & 🥉 🌐❔ 👫 ⚙️: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" + {!> ../../../docs_src/extra_models/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" + {!> ../../../docs_src/extra_models/tutorial001_py310.py!} + ``` + +### 🔃 `**user_in.dict()` + +#### Pydantic `.dict()` + +`user_in` Pydantic 🏷 🎓 `UserIn`. + +Pydantic 🏷 ✔️ `.dict()` 👩‍🔬 👈 📨 `dict` ⏮️ 🏷 💽. + +, 🚥 👥 ✍ Pydantic 🎚 `user_in` 💖: + +```Python +user_in = UserIn(username="john", password="secret", email="john.doe@example.com") +``` + +& ⤴️ 👥 🤙: + +```Python +user_dict = user_in.dict() +``` + +👥 🔜 ✔️ `dict` ⏮️ 💽 🔢 `user_dict` (⚫️ `dict` ↩️ Pydantic 🏷 🎚). + +& 🚥 👥 🤙: + +```Python +print(user_dict) +``` + +👥 🔜 🤚 🐍 `dict` ⏮️: + +```Python +{ + 'username': 'john', + 'password': 'secret', + 'email': 'john.doe@example.com', + 'full_name': None, +} +``` + +#### 🎁 `dict` + +🚥 👥 ✊ `dict` 💖 `user_dict` & 🚶‍♀️ ⚫️ 🔢 (⚖️ 🎓) ⏮️ `**user_dict`, 🐍 🔜 "🎁" ⚫️. ⚫️ 🔜 🚶‍♀️ 🔑 & 💲 `user_dict` 🔗 🔑-💲 ❌. + +, ▶️ ⏮️ `user_dict` ⚪️➡️ 🔛, ✍: + +```Python +UserInDB(**user_dict) +``` + +🔜 🏁 🕳 🌓: + +```Python +UserInDB( + username="john", + password="secret", + email="john.doe@example.com", + full_name=None, +) +``` + +⚖️ 🌅 ⚫️❔, ⚙️ `user_dict` 🔗, ⏮️ ⚫️❔ 🎚 ⚫️ 💪 ✔️ 🔮: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], +) +``` + +#### Pydantic 🏷 ⚪️➡️ 🎚 ➕1️⃣ + +🖼 🔛 👥 🤚 `user_dict` ⚪️➡️ `user_in.dict()`, 👉 📟: + +```Python +user_dict = user_in.dict() +UserInDB(**user_dict) +``` + +🔜 🌓: + +```Python +UserInDB(**user_in.dict()) +``` + +...↩️ `user_in.dict()` `dict`, & ⤴️ 👥 ⚒ 🐍 "🎁" ⚫️ 🚶‍♀️ ⚫️ `UserInDB` 🔠 ⏮️ `**`. + +, 👥 🤚 Pydantic 🏷 ⚪️➡️ 💽 ➕1️⃣ Pydantic 🏷. + +#### 🎁 `dict` & ➕ 🇨🇻 + +& ⤴️ ❎ ➕ 🇨🇻 ❌ `hashed_password=hashed_password`, 💖: + +```Python +UserInDB(**user_in.dict(), hashed_password=hashed_password) +``` + +...🔚 🆙 💆‍♂ 💖: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], + hashed_password = hashed_password, +) +``` + +!!! warning + 🔗 🌖 🔢 🤖 💪 💧 💽, ✋️ 👫 ↗️ 🚫 🚚 🙆 🎰 💂‍♂. + +## 📉 ❎ + +📉 📟 ❎ 1️⃣ 🐚 💭 **FastAPI**. + +📟 ❎ 📈 🤞 🐛, 💂‍♂ ❔, 📟 🔁 ❔ (🕐❔ 👆 ℹ 1️⃣ 🥉 ✋️ 🚫 🎏), ♒️. + +& 👉 🏷 🌐 🤝 📚 💽 & ❎ 🔢 📛 & 🆎. + +👥 💪 👻. + +👥 💪 📣 `UserBase` 🏷 👈 🍦 🧢 👆 🎏 🏷. & ⤴️ 👥 💪 ⚒ 🏿 👈 🏷 👈 😖 🚮 🔢 (🆎 📄, 🔬, ♒️). + +🌐 💽 🛠️, 🔬, 🧾, ♒️. 🔜 👷 🛎. + +👈 🌌, 👥 💪 📣 🔺 🖖 🏷 (⏮️ 🔢 `password`, ⏮️ `hashed_password` & 🍵 🔐): + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7 13-14 17-18 21-22" + {!> ../../../docs_src/extra_models/tutorial002_py310.py!} + ``` + +## `Union` ⚖️ `anyOf` + +👆 💪 📣 📨 `Union` 2️⃣ 🆎, 👈 ⛓, 👈 📨 🔜 🙆 2️⃣. + +⚫️ 🔜 🔬 🗄 ⏮️ `anyOf`. + +👈, ⚙️ 🐩 🐍 🆎 🔑 `typing.Union`: + +!!! note + 🕐❔ ⚖ `Union`, 🔌 🏆 🎯 🆎 🥇, ⏩ 🌘 🎯 🆎. 🖼 🔛, 🌖 🎯 `PlaneItem` 👟 ⏭ `CarItem` `Union[PlaneItem, CarItem]`. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003_py310.py!} + ``` + +### `Union` 🐍 3️⃣.1️⃣0️⃣ + +👉 🖼 👥 🚶‍♀️ `Union[PlaneItem, CarItem]` 💲 ❌ `response_model`. + +↩️ 👥 🚶‍♀️ ⚫️ **💲 ❌** ↩️ 🚮 ⚫️ **🆎 ✍**, 👥 ✔️ ⚙️ `Union` 🐍 3️⃣.1️⃣0️⃣. + +🚥 ⚫️ 🆎 ✍ 👥 💪 ✔️ ⚙️ ⏸ ⏸,: + +```Python +some_variable: PlaneItem | CarItem +``` + +✋️ 🚥 👥 🚮 👈 `response_model=PlaneItem | CarItem` 👥 🔜 🤚 ❌, ↩️ 🐍 🔜 🔄 🎭 **❌ 🛠️** 🖖 `PlaneItem` & `CarItem` ↩️ 🔬 👈 🆎 ✍. + +## 📇 🏷 + +🎏 🌌, 👆 💪 📣 📨 📇 🎚. + +👈, ⚙️ 🐩 🐍 `typing.List` (⚖️ `list` 🐍 3️⃣.9️⃣ & 🔛): + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/extra_models/tutorial004_py39.py!} + ``` + +## 📨 ⏮️ ❌ `dict` + +👆 💪 📣 📨 ⚙️ ✅ ❌ `dict`, 📣 🆎 🔑 & 💲, 🍵 ⚙️ Pydantic 🏷. + +👉 ⚠ 🚥 👆 🚫 💭 ☑ 🏑/🔢 📛 (👈 🔜 💪 Pydantic 🏷) ⏪. + +👉 💼, 👆 💪 ⚙️ `typing.Dict` (⚖️ `dict` 🐍 3️⃣.9️⃣ & 🔛): + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="6" + {!> ../../../docs_src/extra_models/tutorial005_py39.py!} + ``` + +## 🌃 + +⚙️ 💗 Pydantic 🏷 & 😖 ➡ 🔠 💼. + +👆 🚫 💪 ✔️ 👁 💽 🏷 📍 👨‍💼 🚥 👈 👨‍💼 🔜 💪 ✔️ 🎏 "🇵🇸". 💼 ⏮️ 👩‍💻 "👨‍💼" ⏮️ 🇵🇸 ✅ `password`, `password_hash` & 🙅‍♂ 🔐. diff --git a/docs/em/docs/tutorial/first-steps.md b/docs/em/docs/tutorial/first-steps.md new file mode 100644 index 0000000000..252e769f41 --- /dev/null +++ b/docs/em/docs/tutorial/first-steps.md @@ -0,0 +1,333 @@ +# 🥇 🔁 + +🙅 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` (🐍 "🕹"). + * `app`: 🎚 ✍ 🔘 `main.py` ⏮️ ⏸ `app = FastAPI()`. + * `--reload`: ⚒ 💽 ⏏ ⏮️ 📟 🔀. 🕴 ⚙️ 🛠️. + +🔢, 📤 ⏸ ⏮️ 🕳 💖: + +```hl_lines="4" +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +👈 ⏸ 🎦 📛 🌐❔ 👆 📱 ➖ 🍦, 👆 🇧🇿 🎰. + +### ✅ ⚫️ + +📂 👆 🖥 http://127.0.0.1:8000. + +👆 🔜 👀 🎻 📨: + +```JSON +{"message": "Hello World"} +``` + +### 🎓 🛠️ 🩺 + +🔜 🚶 http://127.0.0.1:8000/docs. + +👆 🔜 👀 🏧 🎓 🛠️ 🧾 (🚚 🦁 🎚): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### 🎛 🛠️ 🩺 + +& 🔜, 🚶 http://127.0.0.1:8000/redoc. + +👆 🔜 👀 🎛 🏧 🧾 (🚚 📄): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +### 🗄 + +**FastAPI** 🏗 "🔗" ⏮️ 🌐 👆 🛠️ ⚙️ **🗄** 🐩 ⚖ 🔗. + +#### "🔗" + +"🔗" 🔑 ⚖️ 📛 🕳. 🚫 📟 👈 🛠️ ⚫️, ✋️ 📝 📛. + +#### 🛠️ "🔗" + +👉 💼, 🗄 🔧 👈 🤔 ❔ 🔬 🔗 👆 🛠️. + +👉 🔗 🔑 🔌 👆 🛠️ ➡, 💪 🔢 👫 ✊, ♒️. + +#### 💽 "🔗" + +⚖ "🔗" 💪 🔗 💠 💽, 💖 🎻 🎚. + +👈 💼, ⚫️ 🔜 ⛓ 🎻 🔢, & 📊 🆎 👫 ✔️, ♒️. + +#### 🗄 & 🎻 🔗 + +🗄 🔬 🛠️ 🔗 👆 🛠️. & 👈 🔗 🔌 🔑 (⚖️ "🔗") 📊 📨 & 📨 👆 🛠️ ⚙️ **🎻 🔗**, 🐩 🎻 📊 🔗. + +#### ✅ `openapi.json` + +🚥 👆 😟 🔃 ❔ 🍣 🗄 🔗 👀 💖, FastAPI 🔁 🏗 🎻 (🔗) ⏮️ 📛 🌐 👆 🛠️. + +👆 💪 👀 ⚫️ 🔗: http://127.0.0.1:8000/openapi.json. + +⚫️ 🔜 🎦 🎻 ▶️ ⏮️ 🕳 💖: + +```JSON +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + + + +... +``` + +#### ⚫️❔ 🗄 + +🗄 🔗 ⚫️❔ 🏋️ 2️⃣ 🎓 🧾 ⚙️ 🔌. + +& 📤 💯 🎛, 🌐 ⚓️ 🔛 🗄. 👆 💪 💪 🚮 🙆 📚 🎛 👆 🈸 🏗 ⏮️ **FastAPI**. + +👆 💪 ⚙️ ⚫️ 🏗 📟 🔁, 👩‍💻 👈 🔗 ⏮️ 👆 🛠️. 🖼, 🕸, 📱 ⚖️ ☁ 🈸. + +## 🌃, 🔁 🔁 + +### 🔁 1️⃣: 🗄 `FastAPI` + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI` 🐍 🎓 👈 🚚 🌐 🛠️ 👆 🛠️. + +!!! note "📡 ℹ" + `FastAPI` 🎓 👈 😖 🔗 ⚪️➡️ `Starlette`. + + 👆 💪 ⚙️ 🌐 💃 🛠️ ⏮️ `FastAPI` 💁‍♂️. + +### 🔁 2️⃣: ✍ `FastAPI` "👐" + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +📥 `app` 🔢 🔜 "👐" 🎓 `FastAPI`. + +👉 🔜 👑 ☝ 🔗 ✍ 🌐 👆 🛠️. + +👉 `app` 🎏 1️⃣ 🔗 `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️⃣: ✍ *➡ 🛠️* + +#### ➡ + +"➡" 📥 🔗 🏁 🍕 📛 ▶️ ⚪️➡️ 🥇 `/`. + +, 📛 💖: + +``` +https://example.com/items/foo +``` + +...➡ 🔜: + +``` +/items/foo +``` + +!!! info + "➡" 🛎 🤙 "🔗" ⚖️ "🛣". + +⏪ 🏗 🛠️, "➡" 👑 🌌 🎏 "⚠" & "ℹ". + +#### 🛠️ + +"🛠️" 📥 🔗 1️⃣ 🇺🇸🔍 "👩‍🔬". + +1️⃣: + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +...& 🌅 😍 🕐: + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +🇺🇸🔍 🛠️, 👆 💪 🔗 🔠 ➡ ⚙️ 1️⃣ (⚖️ 🌅) 👫 "👩‍🔬". + +--- + +🕐❔ 🏗 🔗, 👆 🛎 ⚙️ 👫 🎯 🇺🇸🔍 👩‍🔬 🎭 🎯 🎯. + +🛎 👆 ⚙️: + +* `POST`: ✍ 💽. +* `GET`: ✍ 💽. +* `PUT`: ℹ 💽. +* `DELETE`: ❎ 💽. + +, 🗄, 🔠 🇺🇸🔍 👩‍🔬 🤙 "🛠️". + +👥 🔜 🤙 👫 "**🛠️**" 💁‍♂️. + +#### 🔬 *➡ 🛠️ 👨‍🎨* + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`@app.get("/")` 💬 **FastAPI** 👈 🔢 ▶️️ 🔛 🈚 🚚 📨 👈 🚶: + +* ➡ `/` +* ⚙️ get 🛠️ + +!!! info "`@decorator` ℹ" + 👈 `@something` ❕ 🐍 🤙 "👨‍🎨". + + 👆 🚮 ⚫️ 🔛 🔝 🔢. 💖 📶 📔 👒 (👤 💭 👈 🌐❔ ⚖ 👟 ⚪️➡️). + + "👨‍🎨" ✊ 🔢 🔛 & 🔨 🕳 ⏮️ ⚫️. + + 👆 💼, 👉 👨‍🎨 💬 **FastAPI** 👈 🔢 🔛 🔗 **➡** `/` ⏮️ **🛠️** `get`. + + ⚫️ "**➡ 🛠️ 👨‍🎨**". + +👆 💪 ⚙️ 🎏 🛠️: + +* `@app.post()` +* `@app.put()` +* `@app.delete()` + +& 🌅 😍 🕐: + +* `@app.options()` +* `@app.head()` +* `@app.patch()` +* `@app.trace()` + +!!! tip + 👆 🆓 ⚙️ 🔠 🛠️ (🇺🇸🔍 👩‍🔬) 👆 🎋. + + **FastAPI** 🚫 🛠️ 🙆 🎯 🔑. + + ℹ 📥 🎁 📄, 🚫 📄. + + 🖼, 🕐❔ ⚙️ 🕹 👆 🛎 🎭 🌐 🎯 ⚙️ 🕴 `POST` 🛠️. + +### 🔁 4️⃣: 🔬 **➡ 🛠️ 🔢** + +👉 👆 "**➡ 🛠️ 🔢**": + +* **➡**: `/`. +* **🛠️**: `get`. +* **🔢**: 🔢 🔛 "👨‍🎨" (🔛 `@app.get("/")`). + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +👉 🐍 🔢. + +⚫️ 🔜 🤙 **FastAPI** 🕐❔ ⚫️ 📨 📨 📛 "`/`" ⚙️ `GET` 🛠️. + +👉 💼, ⚫️ `async` 🔢. + +--- + +👆 💪 🔬 ⚫️ 😐 🔢 ↩️ `async def`: + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note + 🚥 👆 🚫 💭 🔺, ✅ [🔁: *"🏃 ❓"*](../async.md#in-a-hurry){.internal-link target=_blank}. + +### 🔁 5️⃣: 📨 🎚 + +```Python hl_lines="8" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +👆 💪 📨 `dict`, `list`, ⭐ 💲 `str`, `int`, ♒️. + +👆 💪 📨 Pydantic 🏷 (👆 🔜 👀 🌅 🔃 👈 ⏪). + +📤 📚 🎏 🎚 & 🏷 👈 🔜 🔁 🗜 🎻 (🔌 🐜, ♒️). 🔄 ⚙️ 👆 💕 🕐, ⚫️ 🏆 🎲 👈 👫 ⏪ 🐕‍🦺. + +## 🌃 + +* 🗄 `FastAPI`. +* ✍ `app` 👐. +* ✍ **➡ 🛠️ 👨‍🎨** (💖 `@app.get("/")`). +* ✍ **➡ 🛠️ 🔢** (💖 `def root(): ...` 🔛). +* 🏃 🛠️ 💽 (💖 `uvicorn main:app --reload`). diff --git a/docs/em/docs/tutorial/handling-errors.md b/docs/em/docs/tutorial/handling-errors.md new file mode 100644 index 0000000000..ef7bbfa651 --- /dev/null +++ b/docs/em/docs/tutorial/handling-errors.md @@ -0,0 +1,261 @@ +# 🚚 ❌ + +📤 📚 ⚠ 🌐❔ 👆 💪 🚨 ❌ 👩‍💻 👈 ⚙️ 👆 🛠️. + +👉 👩‍💻 💪 🖥 ⏮️ 🕸, 📟 ⚪️➡️ 👱 🙆, ☁ 📳, ♒️. + +👆 💪 💪 💬 👩‍💻 👈: + +* 👩‍💻 🚫 ✔️ 🥃 😌 👈 🛠️. +* 👩‍💻 🚫 ✔️ 🔐 👈 ℹ. +* 🏬 👩‍💻 🔄 🔐 🚫 🔀. +* ♒️. + +👫 💼, 👆 🔜 🛎 📨 **🇺🇸🔍 👔 📟** ↔ **4️⃣0️⃣0️⃣** (⚪️➡️ 4️⃣0️⃣0️⃣ 4️⃣9️⃣9️⃣). + +👉 🎏 2️⃣0️⃣0️⃣ 🇺🇸🔍 👔 📟 (⚪️➡️ 2️⃣0️⃣0️⃣ 2️⃣9️⃣9️⃣). 👈 "2️⃣0️⃣0️⃣" 👔 📟 ⛓ 👈 😫 📤 "🏆" 📨. + +👔 📟 4️⃣0️⃣0️⃣ ↔ ⛓ 👈 📤 ❌ ⚪️➡️ 👩‍💻. + +💭 🌐 👈 **"4️⃣0️⃣4️⃣ 🚫 🔎"** ❌ (& 🤣) ❓ + +## ⚙️ `HTTPException` + +📨 🇺🇸🔍 📨 ⏮️ ❌ 👩‍💻 👆 ⚙️ `HTTPException`. + +### 🗄 `HTTPException` + +```Python hl_lines="1" +{!../../../docs_src/handling_errors/tutorial001.py!} +``` + +### 🤚 `HTTPException` 👆 📟 + +`HTTPException` 😐 🐍 ⚠ ⏮️ 🌖 📊 🔗 🔗. + +↩️ ⚫️ 🐍 ⚠, 👆 🚫 `return` ⚫️, 👆 `raise` ⚫️. + +👉 ⛓ 👈 🚥 👆 🔘 🚙 🔢 👈 👆 🤙 🔘 👆 *➡ 🛠️ 🔢*, & 👆 🤚 `HTTPException` ⚪️➡️ 🔘 👈 🚙 🔢, ⚫️ 🏆 🚫 🏃 🎂 📟 *➡ 🛠️ 🔢*, ⚫️ 🔜 ❎ 👈 📨 ▶️️ ↖️ & 📨 🇺🇸🔍 ❌ ⚪️➡️ `HTTPException` 👩‍💻. + +💰 🙋‍♀ ⚠ 🤭 `return`😅 💲 🔜 🌖 ⭐ 📄 🔃 🔗 & 💂‍♂. + +👉 🖼, 🕐❔ 👩‍💻 📨 🏬 🆔 👈 🚫 🔀, 🤚 ⚠ ⏮️ 👔 📟 `404`: + +```Python hl_lines="11" +{!../../../docs_src/handling_errors/tutorial001.py!} +``` + +### 📉 📨 + +🚥 👩‍💻 📨 `http://example.com/items/foo` ( `item_id` `"foo"`), 👈 👩‍💻 🔜 📨 🇺🇸🔍 👔 📟 2️⃣0️⃣0️⃣, & 🎻 📨: + +```JSON +{ + "item": "The Foo Wrestlers" +} +``` + +✋️ 🚥 👩‍💻 📨 `http://example.com/items/bar` (🚫-🚫 `item_id` `"bar"`), 👈 👩‍💻 🔜 📨 🇺🇸🔍 👔 📟 4️⃣0️⃣4️⃣ ("🚫 🔎" ❌), & 🎻 📨: + +```JSON +{ + "detail": "Item not found" +} +``` + +!!! tip + 🕐❔ 🙋‍♀ `HTTPException`, 👆 💪 🚶‍♀️ 🙆 💲 👈 💪 🗜 🎻 🔢 `detail`, 🚫 🕴 `str`. + + 👆 💪 🚶‍♀️ `dict`, `list`, ♒️. + + 👫 🍵 🔁 **FastAPI** & 🗜 🎻. + +## 🚮 🛃 🎚 + +📤 ⚠ 🌐❔ ⚫️ ⚠ 💪 🚮 🛃 🎚 🇺🇸🔍 ❌. 🖼, 🆎 💂‍♂. + +👆 🎲 🏆 🚫 💪 ⚙️ ⚫️ 🔗 👆 📟. + +✋️ 💼 👆 💪 ⚫️ 🏧 😐, 👆 💪 🚮 🛃 🎚: + +```Python hl_lines="14" +{!../../../docs_src/handling_errors/tutorial002.py!} +``` + +## ❎ 🛃 ⚠ 🐕‍🦺 + +👆 💪 🚮 🛃 ⚠ 🐕‍🦺 ⏮️ 🎏 ⚠ 🚙 ⚪️➡️ 💃. + +➡️ 💬 👆 ✔️ 🛃 ⚠ `UnicornException` 👈 👆 (⚖️ 🗃 👆 ⚙️) 💪 `raise`. + +& 👆 💚 🍵 👉 ⚠ 🌐 ⏮️ FastAPI. + +👆 💪 🚮 🛃 ⚠ 🐕‍🦺 ⏮️ `@app.exception_handler()`: + +```Python hl_lines="5-7 13-18 24" +{!../../../docs_src/handling_errors/tutorial003.py!} +``` + +📥, 🚥 👆 📨 `/unicorns/yolo`, *➡ 🛠️* 🔜 `raise` `UnicornException`. + +✋️ ⚫️ 🔜 🍵 `unicorn_exception_handler`. + +, 👆 🔜 📨 🧹 ❌, ⏮️ 🇺🇸🔍 👔 📟 `418` & 🎻 🎚: + +```JSON +{"message": "Oops! yolo did something. There goes a rainbow..."} +``` + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.requests import Request` & `from starlette.responses import JSONResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. 🎏 ⏮️ `Request`. + +## 🔐 🔢 ⚠ 🐕‍🦺 + +**FastAPI** ✔️ 🔢 ⚠ 🐕‍🦺. + +👫 🐕‍🦺 🈚 🛬 🔢 🎻 📨 🕐❔ 👆 `raise` `HTTPException` & 🕐❔ 📨 ✔️ ❌ 💽. + +👆 💪 🔐 👫 ⚠ 🐕‍🦺 ⏮️ 👆 👍. + +### 🔐 📨 🔬 ⚠ + +🕐❔ 📨 🔌 ❌ 📊, **FastAPI** 🔘 🤚 `RequestValidationError`. + +& ⚫️ 🔌 🔢 ⚠ 🐕‍🦺 ⚫️. + +🔐 ⚫️, 🗄 `RequestValidationError` & ⚙️ ⚫️ ⏮️ `@app.exception_handler(RequestValidationError)` 🎀 ⚠ 🐕‍🦺. + +⚠ 🐕‍🦺 🔜 📨 `Request` & ⚠. + +```Python hl_lines="2 14-16" +{!../../../docs_src/handling_errors/tutorial004.py!} +``` + +🔜, 🚥 👆 🚶 `/items/foo`, ↩️ 💆‍♂ 🔢 🎻 ❌ ⏮️: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +👆 🔜 🤚 ✍ ⏬, ⏮️: + +``` +1 validation error +path -> item_id + value is not a valid integer (type=type_error.integer) +``` + +#### `RequestValidationError` 🆚 `ValidationError` + +!!! warning + 👫 📡 ℹ 👈 👆 💪 🚶 🚥 ⚫️ 🚫 ⚠ 👆 🔜. + +`RequestValidationError` 🎧-🎓 Pydantic `ValidationError`. + +**FastAPI** ⚙️ ⚫️ 👈, 🚥 👆 ⚙️ Pydantic 🏷 `response_model`, & 👆 💽 ✔️ ❌, 👆 🔜 👀 ❌ 👆 🕹. + +✋️ 👩‍💻/👩‍💻 🔜 🚫 👀 ⚫️. ↩️, 👩‍💻 🔜 📨 "🔗 💽 ❌" ⏮️ 🇺🇸🔍 👔 📟 `500`. + +⚫️ 🔜 👉 🌌 ↩️ 🚥 👆 ✔️ Pydantic `ValidationError` 👆 *📨* ⚖️ 🙆 👆 📟 (🚫 👩‍💻 *📨*), ⚫️ 🤙 🐛 👆 📟. + +& ⏪ 👆 🔧 ⚫️, 👆 👩‍💻/👩‍💻 🚫🔜 🚫 ✔️ 🔐 🔗 ℹ 🔃 ❌, 👈 💪 🎦 💂‍♂ ⚠. + +### 🔐 `HTTPException` ❌ 🐕‍🦺 + +🎏 🌌, 👆 💪 🔐 `HTTPException` 🐕‍🦺. + +🖼, 👆 💪 💚 📨 ✅ ✍ 📨 ↩️ 🎻 👫 ❌: + +```Python hl_lines="3-4 9-11 22" +{!../../../docs_src/handling_errors/tutorial004.py!} +``` + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import PlainTextResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. + +### ⚙️ `RequestValidationError` 💪 + +`RequestValidationError` 🔌 `body` ⚫️ 📨 ⏮️ ❌ 💽. + +👆 💪 ⚙️ ⚫️ ⏪ 🛠️ 👆 📱 🕹 💪 & ℹ ⚫️, 📨 ⚫️ 👩‍💻, ♒️. + +```Python hl_lines="14" +{!../../../docs_src/handling_errors/tutorial005.py!} +``` + +🔜 🔄 📨 ❌ 🏬 💖: + +```JSON +{ + "title": "towel", + "size": "XL" +} +``` + +👆 🔜 📨 📨 💬 👆 👈 💽 ❌ ⚗ 📨 💪: + +```JSON hl_lines="12-15" +{ + "detail": [ + { + "loc": [ + "body", + "size" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ], + "body": { + "title": "towel", + "size": "XL" + } +} +``` + +#### FastAPI `HTTPException` 🆚 💃 `HTTPException` + +**FastAPI** ✔️ 🚮 👍 `HTTPException`. + +& **FastAPI**'Ⓜ `HTTPException` ❌ 🎓 😖 ⚪️➡️ 💃 `HTTPException` ❌ 🎓. + +🕴 🔺, 👈 **FastAPI**'Ⓜ `HTTPException` ✔ 👆 🚮 🎚 🔌 📨. + +👉 💪/⚙️ 🔘 ✳ 2️⃣.0️⃣ & 💂‍♂ 🚙. + +, 👆 💪 🚧 🙋‍♀ **FastAPI**'Ⓜ `HTTPException` 🛎 👆 📟. + +✋️ 🕐❔ 👆 ® ⚠ 🐕‍🦺, 👆 🔜 ® ⚫️ 💃 `HTTPException`. + +👉 🌌, 🚥 🙆 🍕 💃 🔗 📟, ⚖️ 💃 ↔ ⚖️ 🔌 -, 🤚 💃 `HTTPException`, 👆 🐕‍🦺 🔜 💪 ✊ & 🍵 ⚫️. + +👉 🖼, 💪 ✔️ 👯‍♂️ `HTTPException`Ⓜ 🎏 📟, 💃 ⚠ 📁 `StarletteHTTPException`: + +```Python +from starlette.exceptions import HTTPException as StarletteHTTPException +``` + +### 🏤-⚙️ **FastAPI**'Ⓜ ⚠ 🐕‍🦺 + +🚥 👆 💚 ⚙️ ⚠ ⤴️ ⏮️ 🎏 🔢 ⚠ 🐕‍🦺 ⚪️➡️ **FastAPI**, 👆 💪 🗄 & 🏤-⚙️ 🔢 ⚠ 🐕‍🦺 ⚪️➡️ `fastapi.exception_handlers`: + +```Python hl_lines="2-5 15 21" +{!../../../docs_src/handling_errors/tutorial006.py!} +``` + +👉 🖼 👆 `print`😅 ❌ ⏮️ 📶 🎨 📧, ✋️ 👆 🤚 💭. 👆 💪 ⚙️ ⚠ & ⤴️ 🏤-⚙️ 🔢 ⚠ 🐕‍🦺. diff --git a/docs/em/docs/tutorial/header-params.md b/docs/em/docs/tutorial/header-params.md new file mode 100644 index 0000000000..0f33a17747 --- /dev/null +++ b/docs/em/docs/tutorial/header-params.md @@ -0,0 +1,128 @@ +# 🎚 🔢 + +👆 💪 🔬 🎚 🔢 🎏 🌌 👆 🔬 `Query`, `Path` & `Cookie` 🔢. + +## 🗄 `Header` + +🥇 🗄 `Header`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` + +## 📣 `Header` 🔢 + +⤴️ 📣 🎚 🔢 ⚙️ 🎏 📊 ⏮️ `Path`, `Query` & `Cookie`. + +🥇 💲 🔢 💲, 👆 💪 🚶‍♀️ 🌐 ➕ 🔬 ⚖️ ✍ 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` + +!!! note "📡 ℹ" + `Header` "👭" 🎓 `Path`, `Query` & `Cookie`. ⚫️ 😖 ⚪️➡️ 🎏 ⚠ `Param` 🎓. + + ✋️ 💭 👈 🕐❔ 👆 🗄 `Query`, `Path`, `Header`, & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. + +!!! info + 📣 🎚, 👆 💪 ⚙️ `Header`, ↩️ ⏪ 🔢 🔜 🔬 🔢 🔢. + +## 🏧 🛠️ + +`Header` ✔️ 🐥 ➕ 🛠️ 🔛 🔝 ⚫️❔ `Path`, `Query` & `Cookie` 🚚. + +🌅 🐩 🎚 🎏 "🔠" 🦹, 💭 "➖ 🔣" (`-`). + +✋️ 🔢 💖 `user-agent` ❌ 🐍. + +, 🔢, `Header` 🔜 🗜 🔢 📛 🦹 ⚪️➡️ 🎦 (`_`) 🔠 (`-`) ⚗ & 📄 🎚. + +, 🇺🇸🔍 🎚 💼-😛,, 👆 💪 📣 👫 ⏮️ 🐩 🐍 👗 (💭 "🔡"). + +, 👆 💪 ⚙️ `user_agent` 👆 🛎 🔜 🐍 📟, ↩️ 💆‍♂ 🎯 🥇 🔤 `User_Agent` ⚖️ 🕳 🎏. + +🚥 🤔 👆 💪 ❎ 🏧 🛠️ 🎦 🔠, ⚒ 🔢 `convert_underscores` `Header` `False`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="8" + {!> ../../../docs_src/header_params/tutorial002_py310.py!} + ``` + +!!! warning + ⏭ ⚒ `convert_underscores` `False`, 🐻 🤯 👈 🇺🇸🔍 🗳 & 💽 / ⚙️ 🎚 ⏮️ 🎦. + +## ❎ 🎚 + +⚫️ 💪 📨 ❎ 🎚. 👈 ⛓, 🎏 🎚 ⏮️ 💗 💲. + +👆 💪 🔬 👈 💼 ⚙️ 📇 🆎 📄. + +👆 🔜 📨 🌐 💲 ⚪️➡️ ❎ 🎚 🐍 `list`. + +🖼, 📣 🎚 `X-Token` 👈 💪 😑 🌅 🌘 🕐, 👆 💪 ✍: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial003_py310.py!} + ``` + +🚥 👆 🔗 ⏮️ 👈 *➡ 🛠️* 📨 2️⃣ 🇺🇸🔍 🎚 💖: + +``` +X-Token: foo +X-Token: bar +``` + +📨 🔜 💖: + +```JSON +{ + "X-Token values": [ + "bar", + "foo" + ] +} +``` + +## 🌃 + +📣 🎚 ⏮️ `Header`, ⚙️ 🎏 ⚠ ⚓ `Query`, `Path` & `Cookie`. + +& 🚫 😟 🔃 🎦 👆 🔢, **FastAPI** 🔜 ✊ 💅 🏭 👫. diff --git a/docs/em/docs/tutorial/index.md b/docs/em/docs/tutorial/index.md new file mode 100644 index 0000000000..26b4c1913a --- /dev/null +++ b/docs/em/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# 🔰 - 👩‍💻 🦮 + +👉 🔰 🎦 👆 ❔ ⚙️ **FastAPI** ⏮️ 🌅 🚮 ⚒, 🔁 🔁. + +🔠 📄 📉 🏗 🔛 ⏮️ 🕐, ✋️ ⚫️ 🏗 🎏 ❔, 👈 👆 💪 🚶 🔗 🙆 🎯 1️⃣ ❎ 👆 🎯 🛠️ 💪. + +⚫️ 🏗 👷 🔮 🔗. + +👆 💪 👟 🔙 & 👀 ⚫️❔ ⚫️❔ 👆 💪. + +## 🏃 📟 + +🌐 📟 🍫 💪 📁 & ⚙️ 🔗 (👫 🤙 💯 🐍 📁). + +🏃 🙆 🖼, 📁 📟 📁 `main.py`, & ▶️ `uvicorn` ⏮️: + +
+ +```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. +``` + +
+ +⚫️ **🏆 💡** 👈 👆 ✍ ⚖️ 📁 📟, ✍ ⚫️ & 🏃 ⚫️ 🌐. + +⚙️ ⚫️ 👆 👨‍🎨 ⚫️❔ 🤙 🎦 👆 💰 FastAPI, 👀 ❔ 🐥 📟 👆 ✔️ ✍, 🌐 🆎 ✅, ✍, ♒️. + +--- + +## ❎ FastAPI + +🥇 🔁 ❎ FastAPI. + +🔰, 👆 💪 💚 ❎ ⚫️ ⏮️ 🌐 📦 🔗 & ⚒: + +
+ +```console +$ pip install "fastapi[all]" + +---> 100% +``` + +
+ +...👈 🔌 `uvicorn`, 👈 👆 💪 ⚙️ 💽 👈 🏃 👆 📟. + +!!! note + 👆 💪 ❎ ⚫️ 🍕 🍕. + + 👉 ⚫️❔ 👆 🔜 🎲 🕐 👆 💚 🛠️ 👆 🈸 🏭: + + ``` + pip install fastapi + ``` + + ❎ `uvicorn` 👷 💽: + + ``` + pip install "uvicorn[standard]" + ``` + + & 🎏 🔠 📦 🔗 👈 👆 💚 ⚙️. + +## 🏧 👩‍💻 🦮 + +📤 **🏧 👩‍💻 🦮** 👈 👆 💪 ✍ ⏪ ⏮️ 👉 **🔰 - 👩‍💻 🦮**. + +**🏧 👩‍💻 🦮**, 🏗 🔛 👉, ⚙️ 🎏 🔧, & 💡 👆 ➕ ⚒. + +✋️ 👆 🔜 🥇 ✍ **🔰 - 👩‍💻 🦮** (⚫️❔ 👆 👂 ▶️️ 🔜). + +⚫️ 🔧 👈 👆 💪 🏗 🏁 🈸 ⏮️ **🔰 - 👩‍💻 🦮**, & ⤴️ ↔ ⚫️ 🎏 🌌, ⚓️ 🔛 👆 💪, ⚙️ 🌖 💭 ⚪️➡️ **🏧 👩‍💻 🦮**. diff --git a/docs/em/docs/tutorial/metadata.md b/docs/em/docs/tutorial/metadata.md new file mode 100644 index 0000000000..00098cdf59 --- /dev/null +++ b/docs/em/docs/tutorial/metadata.md @@ -0,0 +1,112 @@ +# 🗃 & 🩺 📛 + +👆 💪 🛃 📚 🗃 📳 👆 **FastAPI** 🈸. + +## 🗃 🛠️ + +👆 💪 ⚒ 📄 🏑 👈 ⚙️ 🗄 🔧 & 🏧 🛠️ 🩺 ⚜: + +| 🔢 | 🆎 | 📛 | +|------------|------|-------------| +| `title` | `str` | 📛 🛠️. | +| `description` | `str` | 📏 📛 🛠️. ⚫️ 💪 ⚙️ ✍. | +| `version` | `string` | ⏬ 🛠️. 👉 ⏬ 👆 👍 🈸, 🚫 🗄. 🖼 `2.5.0`. | +| `terms_of_service` | `str` | 📛 ⚖ 🐕‍🦺 🛠️. 🚥 🚚, 👉 ✔️ 📛. | +| `contact` | `dict` | 📧 ℹ 🎦 🛠️. ⚫️ 💪 🔌 📚 🏑.
contact 🏑
🔢🆎📛
namestr⚖ 📛 📧 👨‍💼/🏢.
urlstr📛 ☝ 📧 ℹ. 🔜 📁 📛.
emailstr📧 📢 📧 👨‍💼/🏢. 🔜 📁 📧 📢.
| +| `license_info` | `dict` | 🛂 ℹ 🎦 🛠️. ⚫️ 💪 🔌 📚 🏑.
license_info 🏑
🔢🆎📛
namestr🚚 (🚥 license_info ⚒). 🛂 📛 ⚙️ 🛠️.
urlstr📛 🛂 ⚙️ 🛠️. 🔜 📁 📛.
| + +👆 💪 ⚒ 👫 ⏩: + +```Python hl_lines="3-16 19-31" +{!../../../docs_src/metadata/tutorial001.py!} +``` + +!!! tip + 👆 💪 ✍ ✍ `description` 🏑 & ⚫️ 🔜 ✍ 🔢. + +⏮️ 👉 📳, 🏧 🛠️ 🩺 🔜 👀 💖: + + + +## 🗃 🔖 + +👆 💪 🚮 🌖 🗃 🎏 🔖 ⚙️ 👪 👆 ➡ 🛠️ ⏮️ 🔢 `openapi_tags`. + +⚫️ ✊ 📇 ⚗ 1️⃣ 📖 🔠 🔖. + +🔠 📖 💪 🔌: + +* `name` (**✔**): `str` ⏮️ 🎏 📛 👆 ⚙️ `tags` 🔢 👆 *➡ 🛠️* & `APIRouter`Ⓜ. +* `description`: `str` ⏮️ 📏 📛 🔖. ⚫️ 💪 ✔️ ✍ & 🔜 🎦 🩺 🎚. +* `externalDocs`: `dict` 🔬 🔢 🧾 ⏮️: + * `description`: `str` ⏮️ 📏 📛 🔢 🩺. + * `url` (**✔**): `str` ⏮️ 📛 🔢 🧾. + +### ✍ 🗃 🔖 + +➡️ 🔄 👈 🖼 ⏮️ 🔖 `users` & `items`. + +✍ 🗃 👆 🔖 & 🚶‍♀️ ⚫️ `openapi_tags` 🔢: + +```Python hl_lines="3-16 18" +{!../../../docs_src/metadata/tutorial004.py!} +``` + +👀 👈 👆 💪 ⚙️ ✍ 🔘 📛, 🖼 "💳" 🔜 🎦 🦁 (**💳**) & "🎀" 🔜 🎦 ❕ (_🎀_). + +!!! tip + 👆 🚫 ✔️ 🚮 🗃 🌐 🔖 👈 👆 ⚙️. + +### ⚙️ 👆 🔖 + +⚙️ `tags` 🔢 ⏮️ 👆 *➡ 🛠️* (& `APIRouter`Ⓜ) 🛠️ 👫 🎏 🔖: + +```Python hl_lines="21 26" +{!../../../docs_src/metadata/tutorial004.py!} +``` + +!!! info + ✍ 🌅 🔃 🔖 [➡ 🛠️ 📳](../path-operation-configuration/#tags){.internal-link target=_blank}. + +### ✅ 🩺 + +🔜, 🚥 👆 ✅ 🩺, 👫 🔜 🎦 🌐 🌖 🗃: + + + +### ✔ 🔖 + +✔ 🔠 🔖 🗃 📖 🔬 ✔ 🎦 🩺 🎚. + +🖼, ✋️ `users` 🔜 🚶 ⏮️ `items` 🔤 ✔, ⚫️ 🎦 ⏭ 👫, ↩️ 👥 🚮 👫 🗃 🥇 📖 📇. + +## 🗄 📛 + +🔢, 🗄 🔗 🍦 `/openapi.json`. + +✋️ 👆 💪 🔗 ⚫️ ⏮️ 🔢 `openapi_url`. + +🖼, ⚒ ⚫️ 🍦 `/api/v1/openapi.json`: + +```Python hl_lines="3" +{!../../../docs_src/metadata/tutorial002.py!} +``` + +🚥 👆 💚 ❎ 🗄 🔗 🍕 👆 💪 ⚒ `openapi_url=None`, 👈 🔜 ❎ 🧾 👩‍💻 🔢 👈 ⚙️ ⚫️. + +## 🩺 📛 + +👆 💪 🔗 2️⃣ 🧾 👩‍💻 🔢 🔌: + +* **🦁 🎚**: 🍦 `/docs`. + * 👆 💪 ⚒ 🚮 📛 ⏮️ 🔢 `docs_url`. + * 👆 💪 ❎ ⚫️ ⚒ `docs_url=None`. +* **📄**: 🍦 `/redoc`. + * 👆 💪 ⚒ 🚮 📛 ⏮️ 🔢 `redoc_url`. + * 👆 💪 ❎ ⚫️ ⚒ `redoc_url=None`. + +🖼, ⚒ 🦁 🎚 🍦 `/documentation` & ❎ 📄: + +```Python hl_lines="3" +{!../../../docs_src/metadata/tutorial003.py!} +``` diff --git a/docs/em/docs/tutorial/middleware.md b/docs/em/docs/tutorial/middleware.md new file mode 100644 index 0000000000..644b4690c1 --- /dev/null +++ b/docs/em/docs/tutorial/middleware.md @@ -0,0 +1,61 @@ +# 🛠️ + +👆 💪 🚮 🛠️ **FastAPI** 🈸. + +"🛠️" 🔢 👈 👷 ⏮️ 🔠 **📨** ⏭ ⚫️ 🛠️ 🙆 🎯 *➡ 🛠️*. & ⏮️ 🔠 **📨** ⏭ 🛬 ⚫️. + +* ⚫️ ✊ 🔠 **📨** 👈 👟 👆 🈸. +* ⚫️ 💪 ⤴️ 🕳 👈 **📨** ⚖️ 🏃 🙆 💪 📟. +* ⤴️ ⚫️ 🚶‍♀️ **📨** 🛠️ 🎂 🈸 ( *➡ 🛠️*). +* ⚫️ ⤴️ ✊ **📨** 🏗 🈸 ( *➡ 🛠️*). +* ⚫️ 💪 🕳 👈 **📨** ⚖️ 🏃 🙆 💪 📟. +* ⤴️ ⚫️ 📨 **📨**. + +!!! note "📡 ℹ" + 🚥 👆 ✔️ 🔗 ⏮️ `yield`, 🚪 📟 🔜 🏃 *⏮️* 🛠️. + + 🚥 📤 🙆 🖥 📋 (📄 ⏪), 👫 🔜 🏃 *⏮️* 🌐 🛠️. + +## ✍ 🛠️ + +✍ 🛠️ 👆 ⚙️ 👨‍🎨 `@app.middleware("http")` 🔛 🔝 🔢. + +🛠️ 🔢 📨: + +* `request`. +* 🔢 `call_next` 👈 🔜 📨 `request` 🔢. + * 👉 🔢 🔜 🚶‍♀️ `request` 🔗 *➡ 🛠️*. + * ⤴️ ⚫️ 📨 `response` 🏗 🔗 *➡ 🛠️*. +* 👆 💪 ⤴️ 🔀 🌅 `response` ⏭ 🛬 ⚫️. + +```Python hl_lines="8-9 11 14" +{!../../../docs_src/middleware/tutorial001.py!} +``` + +!!! tip + ✔️ 🤯 👈 🛃 © 🎚 💪 🚮 ⚙️ '✖-' 🔡. + + ✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 ([⚜ (✖️-🇨🇳 ℹ 🤝)](cors.md){.internal-link target=_blank}) ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.requests import Request`. + + **FastAPI** 🚚 ⚫️ 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. + +### ⏭ & ⏮️ `response` + +👆 💪 🚮 📟 🏃 ⏮️ `request`, ⏭ 🙆 *➡ 🛠️* 📨 ⚫️. + +& ⏮️ `response` 🏗, ⏭ 🛬 ⚫️. + +🖼, 👆 💪 🚮 🛃 🎚 `X-Process-Time` ⚗ 🕰 🥈 👈 ⚫️ ✊ 🛠️ 📨 & 🏗 📨: + +```Python hl_lines="10 12-13" +{!../../../docs_src/middleware/tutorial001.py!} +``` + +## 🎏 🛠️ + +👆 💪 ⏪ ✍ 🌖 🔃 🎏 🛠️ [🏧 👩‍💻 🦮: 🏧 🛠️](../advanced/middleware.md){.internal-link target=_blank}. + +👆 🔜 ✍ 🔃 ❔ 🍵 ⏮️ 🛠️ ⏭ 📄. diff --git a/docs/em/docs/tutorial/path-operation-configuration.md b/docs/em/docs/tutorial/path-operation-configuration.md new file mode 100644 index 0000000000..916529258d --- /dev/null +++ b/docs/em/docs/tutorial/path-operation-configuration.md @@ -0,0 +1,179 @@ +# ➡ 🛠️ 📳 + +📤 📚 🔢 👈 👆 💪 🚶‍♀️ 👆 *➡ 🛠️ 👨‍🎨* 🔗 ⚫️. + +!!! warning + 👀 👈 👫 🔢 🚶‍♀️ 🔗 *➡ 🛠️ 👨‍🎨*, 🚫 👆 *➡ 🛠️ 🔢*. + +## 📨 👔 📟 + +👆 💪 🔬 (🇺🇸🔍) `status_code` ⚙️ 📨 👆 *➡ 🛠️*. + +👆 💪 🚶‍♀️ 🔗 `int` 📟, 💖 `404`. + +✋️ 🚥 👆 🚫 💭 ⚫️❔ 🔠 🔢 📟, 👆 💪 ⚙️ ⌨ 📉 `status`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1 15" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} + ``` + +👈 👔 📟 🔜 ⚙️ 📨 & 🔜 🚮 🗄 🔗. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette import status`. + + **FastAPI** 🚚 🎏 `starlette.status` `fastapi.status` 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. + +## 🔖 + +👆 💪 🚮 🔖 👆 *➡ 🛠️*, 🚶‍♀️ 🔢 `tags` ⏮️ `list` `str` (🛎 1️⃣ `str`): + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="15 20 25" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} + ``` + +👫 🔜 🚮 🗄 🔗 & ⚙️ 🏧 🧾 🔢: + + + +### 🔖 ⏮️ 🔢 + +🚥 👆 ✔️ 🦏 🈸, 👆 5️⃣📆 🔚 🆙 📈 **📚 🔖**, & 👆 🔜 💚 ⚒ 💭 👆 🕧 ⚙️ **🎏 🔖** 🔗 *➡ 🛠️*. + +👫 💼, ⚫️ 💪 ⚒ 🔑 🏪 🔖 `Enum`. + +**FastAPI** 🐕‍🦺 👈 🎏 🌌 ⏮️ ✅ 🎻: + +```Python hl_lines="1 8-10 13 18" +{!../../../docs_src/path_operation_configuration/tutorial002b.py!} +``` + +## 📄 & 📛 + +👆 💪 🚮 `summary` & `description`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} + ``` + +## 📛 ⚪️➡️ #️⃣ + +📛 😑 📏 & 📔 💗 ⏸, 👆 💪 📣 *➡ 🛠️* 📛 🔢 #️⃣ & **FastAPI** 🔜 ✍ ⚫️ ⚪️➡️ 📤. + +👆 💪 ✍ #️⃣ , ⚫️ 🔜 🔬 & 🖥 ☑ (✊ 🔘 🏧 #️⃣ 📐). + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17-25" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} + ``` + +⚫️ 🔜 ⚙️ 🎓 🩺: + + + +## 📨 📛 + +👆 💪 ✔ 📨 📛 ⏮️ 🔢 `response_description`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="19" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} + ``` + +!!! info + 👀 👈 `response_description` 🔗 🎯 📨, `description` 🔗 *➡ 🛠️* 🏢. + +!!! check + 🗄 ✔ 👈 🔠 *➡ 🛠️* 🚚 📨 📛. + + , 🚥 👆 🚫 🚚 1️⃣, **FastAPI** 🔜 🔁 🏗 1️⃣ "🏆 📨". + + + +## 😢 *➡ 🛠️* + +🚥 👆 💪 ™ *➡ 🛠️* 😢, ✋️ 🍵 ❎ ⚫️, 🚶‍♀️ 🔢 `deprecated`: + +```Python hl_lines="16" +{!../../../docs_src/path_operation_configuration/tutorial006.py!} +``` + +⚫️ 🔜 🎯 ™ 😢 🎓 🩺: + + + +✅ ❔ 😢 & 🚫-😢 *➡ 🛠️* 👀 💖: + + + +## 🌃 + +👆 💪 🔗 & 🚮 🗃 👆 *➡ 🛠️* 💪 🚶‍♀️ 🔢 *➡ 🛠️ 👨‍🎨*. diff --git a/docs/em/docs/tutorial/path-params-numeric-validations.md b/docs/em/docs/tutorial/path-params-numeric-validations.md new file mode 100644 index 0000000000..b1ba2670b6 --- /dev/null +++ b/docs/em/docs/tutorial/path-params-numeric-validations.md @@ -0,0 +1,138 @@ +# ➡ 🔢 & 🔢 🔬 + +🎏 🌌 👈 👆 💪 📣 🌅 🔬 & 🗃 🔢 🔢 ⏮️ `Query`, 👆 💪 📣 🎏 🆎 🔬 & 🗃 ➡ 🔢 ⏮️ `Path`. + +## 🗄 ➡ + +🥇, 🗄 `Path` ⚪️➡️ `fastapi`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +## 📣 🗃 + +👆 💪 📣 🌐 🎏 🔢 `Query`. + +🖼, 📣 `title` 🗃 💲 ➡ 🔢 `item_id` 👆 💪 🆎: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +!!! note + ➡ 🔢 🕧 ✔ ⚫️ ✔️ 🍕 ➡. + + , 👆 🔜 📣 ⚫️ ⏮️ `...` ™ ⚫️ ✔. + + 👐, 🚥 👆 📣 ⚫️ ⏮️ `None` ⚖️ ⚒ 🔢 💲, ⚫️ 🔜 🚫 📉 🕳, ⚫️ 🔜 🕧 🚚. + +## ✔ 🔢 👆 💪 + +➡️ 💬 👈 👆 💚 📣 🔢 🔢 `q` ✔ `str`. + +& 👆 🚫 💪 📣 🕳 🙆 👈 🔢, 👆 🚫 🤙 💪 ⚙️ `Query`. + +✋️ 👆 💪 ⚙️ `Path` `item_id` ➡ 🔢. + +🐍 🔜 😭 🚥 👆 🚮 💲 ⏮️ "🔢" ⏭ 💲 👈 🚫 ✔️ "🔢". + +✋️ 👆 💪 🏤-✔ 👫, & ✔️ 💲 🍵 🔢 (🔢 🔢 `q`) 🥇. + +⚫️ 🚫 🤔 **FastAPI**. ⚫️ 🔜 🔍 🔢 👫 📛, 🆎 & 🔢 📄 (`Query`, `Path`, ♒️), ⚫️ 🚫 💅 🔃 ✔. + +, 👆 💪 📣 👆 🔢: + +```Python hl_lines="7" +{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} +``` + +## ✔ 🔢 👆 💪, 🎱 + +🚥 👆 💚 📣 `q` 🔢 🔢 🍵 `Query` 🚫 🙆 🔢 💲, & ➡ 🔢 `item_id` ⚙️ `Path`, & ✔️ 👫 🎏 ✔, 🐍 ✔️ 🐥 🎁 ❕ 👈. + +🚶‍♀️ `*`, 🥇 🔢 🔢. + +🐍 🏆 🚫 🕳 ⏮️ 👈 `*`, ✋️ ⚫️ 🔜 💭 👈 🌐 📄 🔢 🔜 🤙 🇨🇻 ❌ (🔑-💲 👫), 💭 kwargs. 🚥 👫 🚫 ✔️ 🔢 💲. + +```Python hl_lines="7" +{!../../../docs_src/path_params_numeric_validations/tutorial003.py!} +``` + +## 🔢 🔬: 👑 🌘 ⚖️ 🌓 + +⏮️ `Query` & `Path` (& 🎏 👆 🔜 👀 ⏪) 👆 💪 📣 🔢 ⚛. + +📥, ⏮️ `ge=1`, `item_id` 🔜 💪 🔢 🔢 "`g`🅾 🌘 ⚖️ `e`🅾" `1`. + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial004.py!} +``` + +## 🔢 🔬: 🌘 🌘 & 🌘 🌘 ⚖️ 🌓 + +🎏 ✔: + +* `gt`: `g`🅾 `t`👲 +* `le`: `l`👭 🌘 ⚖️ `e`🅾 + +```Python hl_lines="9" +{!../../../docs_src/path_params_numeric_validations/tutorial005.py!} +``` + +## 🔢 🔬: 🎈, 🌘 🌘 & 🌘 🌘 + +🔢 🔬 👷 `float` 💲. + +📥 🌐❔ ⚫️ ▶️️ ⚠ 💪 📣 gt & 🚫 ge. ⏮️ ⚫️ 👆 💪 🚚, 🖼, 👈 💲 🔜 👑 🌘 `0`, 🚥 ⚫️ 🌘 🌘 `1`. + +, `0.5` 🔜 ☑ 💲. ✋️ `0.0` ⚖️ `0` 🔜 🚫. + +& 🎏 lt. + +```Python hl_lines="11" +{!../../../docs_src/path_params_numeric_validations/tutorial006.py!} +``` + +## 🌃 + +⏮️ `Query`, `Path` (& 🎏 👆 🚫 👀) 👆 💪 📣 🗃 & 🎻 🔬 🎏 🌌 ⏮️ [🔢 🔢 & 🎻 🔬](query-params-str-validations.md){.internal-link target=_blank}. + +& 👆 💪 📣 🔢 🔬: + +* `gt`: `g`🅾 `t`👲 +* `ge`: `g`🅾 🌘 ⚖️ `e`🅾 +* `lt`: `l`👭 `t`👲 +* `le`: `l`👭 🌘 ⚖️ `e`🅾 + +!!! info + `Query`, `Path`, & 🎏 🎓 👆 🔜 👀 ⏪ 🏿 ⚠ `Param` 🎓. + + 🌐 👫 💰 🎏 🔢 🌖 🔬 & 🗃 👆 ✔️ 👀. + +!!! note "📡 ℹ" + 🕐❔ 👆 🗄 `Query`, `Path` & 🎏 ⚪️➡️ `fastapi`, 👫 🤙 🔢. + + 👈 🕐❔ 🤙, 📨 👐 🎓 🎏 📛. + + , 👆 🗄 `Query`, ❔ 🔢. & 🕐❔ 👆 🤙 ⚫️, ⚫️ 📨 👐 🎓 🌟 `Query`. + + 👫 🔢 📤 (↩️ ⚙️ 🎓 🔗) 👈 👆 👨‍🎨 🚫 ™ ❌ 🔃 👫 🆎. + + 👈 🌌 👆 💪 ⚙️ 👆 😐 👨‍🎨 & 🛠️ 🧰 🍵 ✔️ 🚮 🛃 📳 🤷‍♂ 📚 ❌. diff --git a/docs/em/docs/tutorial/path-params.md b/docs/em/docs/tutorial/path-params.md new file mode 100644 index 0000000000..ea939b458a --- /dev/null +++ b/docs/em/docs/tutorial/path-params.md @@ -0,0 +1,252 @@ +# ➡ 🔢 + +👆 💪 📣 ➡ "🔢" ⚖️ "🔢" ⏮️ 🎏 ❕ ⚙️ 🐍 📁 🎻: + +```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 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`, 🐍 `int`, 🚫 🎻 `"3"`. + + , ⏮️ 👈 🆎 📄, **FastAPI** 🤝 👆 🏧 📨 "✍". + +## 💽 🔬 + +✋️ 🚥 👆 🚶 🖥 http://127.0.0.1:8000/items/foo, 👆 🔜 👀 👌 🇺🇸🔍 ❌: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +↩️ ➡ 🔢 `item_id` ✔️ 💲 `"foo"`, ❔ 🚫 `int`. + +🎏 ❌ 🔜 😑 🚥 👆 🚚 `float` ↩️ `int`,: http://127.0.0.1:8000/items/4.2 + +!!! check + , ⏮️ 🎏 🐍 🆎 📄, **FastAPI** 🤝 👆 💽 🔬. + + 👀 👈 ❌ 🎯 🇵🇸 ⚫️❔ ☝ 🌐❔ 🔬 🚫 🚶‍♀️. + + 👉 🙃 👍 ⏪ 🛠️ & 🛠️ 📟 👈 🔗 ⏮️ 👆 🛠️. + +## 🧾 + +& 🕐❔ 👆 📂 👆 🖥 http://127.0.0.1:8000/docs, 👆 🔜 👀 🏧, 🎓, 🛠️ 🧾 💖: + + + +!!! check + 🔄, ⏮️ 👈 🎏 🐍 🆎 📄, **FastAPI** 🤝 👆 🏧, 🎓 🧾 (🛠️ 🦁 🎚). + + 👀 👈 ➡ 🔢 📣 🔢. + +## 🐩-⚓️ 💰, 🎛 🧾 + +& ↩️ 🏗 🔗 ⚪️➡️ 🗄 🐩, 📤 📚 🔗 🧰. + +↩️ 👉, **FastAPI** ⚫️ 🚚 🎛 🛠️ 🧾 (⚙️ 📄), ❔ 👆 💪 🔐 http://127.0.0.1:8000/redoc: + + + +🎏 🌌, 📤 📚 🔗 🧰. ✅ 📟 ⚡ 🧰 📚 🇪🇸. + +## Pydantic + +🌐 💽 🔬 🎭 🔽 🚘 Pydantic, 👆 🤚 🌐 💰 ⚪️➡️ ⚫️. & 👆 💭 👆 👍 ✋. + +👆 💪 ⚙️ 🎏 🆎 📄 ⏮️ `str`, `float`, `bool` & 📚 🎏 🏗 📊 🆎. + +📚 👫 🔬 ⏭ 📃 🔰. + +## ✔ 🤔 + +🕐❔ 🏗 *➡ 🛠️*, 👆 💪 🔎 ⚠ 🌐❔ 👆 ✔️ 🔧 ➡. + +💖 `/users/me`, ➡️ 💬 👈 ⚫️ 🤚 📊 🔃 ⏮️ 👩‍💻. + +& ⤴️ 👆 💪 ✔️ ➡ `/users/{user_id}` 🤚 💽 🔃 🎯 👩‍💻 👩‍💻 🆔. + +↩️ *➡ 🛠️* 🔬 ✔, 👆 💪 ⚒ 💭 👈 ➡ `/users/me` 📣 ⏭ 1️⃣ `/users/{user_id}`: + +```Python hl_lines="6 11" +{!../../../docs_src/path_params/tutorial003.py!} +``` + +⏪, ➡ `/users/{user_id}` 🔜 🏏 `/users/me`, "💭" 👈 ⚫️ 📨 🔢 `user_id` ⏮️ 💲 `"me"`. + +➡, 👆 🚫🔜 ↔ ➡ 🛠️: + +```Python hl_lines="6 11" +{!../../../docs_src/path_params/tutorial003b.py!} +``` + +🥇 🕐 🔜 🕧 ⚙️ ↩️ ➡ 🏏 🥇. + +## 🔁 💲 + +🚥 👆 ✔️ *➡ 🛠️* 👈 📨 *➡ 🔢*, ✋️ 👆 💚 💪 ☑ *➡ 🔢* 💲 🔁, 👆 💪 ⚙️ 🐩 🐍 `Enum`. + +### ✍ `Enum` 🎓 + +🗄 `Enum` & ✍ 🎧-🎓 👈 😖 ⚪️➡️ `str` & ⚪️➡️ `Enum`. + +😖 ⚪️➡️ `str` 🛠️ 🩺 🔜 💪 💭 👈 💲 🔜 🆎 `string` & 🔜 💪 ✍ ☑. + +⤴️ ✍ 🎓 🔢 ⏮️ 🔧 💲, ❔ 🔜 💪 ☑ 💲: + +```Python hl_lines="1 6-9" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +!!! info + 🔢 (⚖️ 🔢) 💪 🐍 ↩️ ⏬ 3️⃣.4️⃣. + +!!! tip + 🚥 👆 💭, "📊", "🎓", & "🍏" 📛 🎰 🏫 🏷. + +### 📣 *➡ 🔢* + +⤴️ ✍ *➡ 🔢* ⏮️ 🆎 ✍ ⚙️ 🔢 🎓 👆 ✍ (`ModelName`): + +```Python hl_lines="16" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +### ✅ 🩺 + +↩️ 💪 💲 *➡ 🔢* 🔢, 🎓 🩺 💪 🎦 👫 🎆: + + + +### 👷 ⏮️ 🐍 *🔢* + +💲 *➡ 🔢* 🔜 *🔢 👨‍🎓*. + +#### 🔬 *🔢 👨‍🎓* + +👆 💪 🔬 ⚫️ ⏮️ *🔢 👨‍🎓* 👆 ✍ 🔢 `ModelName`: + +```Python hl_lines="17" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +#### 🤚 *🔢 💲* + +👆 💪 🤚 ☑ 💲 ( `str` 👉 💼) ⚙️ `model_name.value`, ⚖️ 🏢, `your_enum_member.value`: + +```Python hl_lines="20" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +!!! tip + 👆 💪 🔐 💲 `"lenet"` ⏮️ `ModelName.lenet.value`. + +#### 📨 *🔢 👨‍🎓* + +👆 💪 📨 *🔢 👨‍🎓* ⚪️➡️ 👆 *➡ 🛠️*, 🐦 🎻 💪 (✅ `dict`). + +👫 🔜 🗜 👫 🔗 💲 (🎻 👉 💼) ⏭ 🛬 👫 👩‍💻: + +```Python hl_lines="18 21 23" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +👆 👩‍💻 👆 🔜 🤚 🎻 📨 💖: + +```JSON +{ + "model_name": "alexnet", + "message": "Deep Learning FTW!" +} +``` + +## ➡ 🔢 ⚗ ➡ + +➡️ 💬 👆 ✔️ *➡ 🛠️* ⏮️ ➡ `/files/{file_path}`. + +✋️ 👆 💪 `file_path` ⚫️ 🔌 *➡*, 💖 `home/johndoe/myfile.txt`. + +, 📛 👈 📁 🔜 🕳 💖: `/files/home/johndoe/myfile.txt`. + +### 🗄 🐕‍🦺 + +🗄 🚫 🐕‍🦺 🌌 📣 *➡ 🔢* 🔌 *➡* 🔘, 👈 💪 ↘️ 😐 👈 ⚠ 💯 & 🔬. + +👐, 👆 💪 ⚫️ **FastAPI**, ⚙️ 1️⃣ 🔗 🧰 ⚪️➡️ 💃. + +& 🩺 🔜 👷, 👐 🚫 ❎ 🙆 🧾 💬 👈 🔢 🔜 🔌 ➡. + +### ➡ 🔌 + +⚙️ 🎛 🔗 ⚪️➡️ 💃 👆 💪 📣 *➡ 🔢* ⚗ *➡* ⚙️ 📛 💖: + +``` +/files/{file_path:path} +``` + +👉 💼, 📛 🔢 `file_path`, & 🏁 🍕, `:path`, 💬 ⚫️ 👈 🔢 🔜 🏏 🙆 *➡*. + +, 👆 💪 ⚙️ ⚫️ ⏮️: + +```Python hl_lines="6" +{!../../../docs_src/path_params/tutorial004.py!} +``` + +!!! tip + 👆 💪 💪 🔢 🔌 `/home/johndoe/myfile.txt`, ⏮️ 🏁 🔪 (`/`). + + 👈 💼, 📛 🔜: `/files//home/johndoe/myfile.txt`, ⏮️ 2️⃣✖️ 🔪 (`//`) 🖖 `files` & `home`. + +## 🌃 + +⏮️ **FastAPI**, ⚙️ 📏, 🏋️ & 🐩 🐍 🆎 📄, 👆 🤚: + +* 👨‍🎨 🐕‍🦺: ❌ ✅, ✍, ♒️. +* 💽 "" +* 💽 🔬 +* 🛠️ ✍ & 🏧 🧾 + +& 👆 🕴 ✔️ 📣 👫 🕐. + +👈 🎲 👑 ⭐ 📈 **FastAPI** 🔬 🎛 🛠️ (↖️ ⚪️➡️ 🍣 🎭). diff --git a/docs/em/docs/tutorial/query-params-str-validations.md b/docs/em/docs/tutorial/query-params-str-validations.md new file mode 100644 index 0000000000..f0e455abe1 --- /dev/null +++ b/docs/em/docs/tutorial/query-params-str-validations.md @@ -0,0 +1,467 @@ +# 🔢 🔢 & 🎻 🔬 + +**FastAPI** ✔ 👆 📣 🌖 ℹ & 🔬 👆 🔢. + +➡️ ✊ 👉 🈸 🖼: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} + ``` + +🔢 🔢 `q` 🆎 `Union[str, None]` (⚖️ `str | None` 🐍 3️⃣.1️⃣0️⃣), 👈 ⛓ 👈 ⚫️ 🆎 `str` ✋️ 💪 `None`, & 👐, 🔢 💲 `None`, FastAPI 🔜 💭 ⚫️ 🚫 ✔. + +!!! note + FastAPI 🔜 💭 👈 💲 `q` 🚫 ✔ ↩️ 🔢 💲 `= None`. + + `Union` `Union[str, None]` 🔜 ✔ 👆 👨‍🎨 🤝 👆 👍 🐕‍🦺 & 🔍 ❌. + +## 🌖 🔬 + +👥 🔜 🛠️ 👈 ✋️ `q` 📦, 🕐❔ ⚫️ 🚚, **🚮 📐 🚫 📉 5️⃣0️⃣ 🦹**. + +### 🗄 `Query` + +🏆 👈, 🥇 🗄 `Query` ⚪️➡️ `fastapi`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3" + {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + ``` + +## ⚙️ `Query` 🔢 💲 + +& 🔜 ⚙️ ⚫️ 🔢 💲 👆 🔢, ⚒ 🔢 `max_length` 5️⃣0️⃣: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + ``` + +👥 ✔️ ❎ 🔢 💲 `None` 🔢 ⏮️ `Query()`, 👥 💪 🔜 ⚒ 🔢 💲 ⏮️ 🔢 `Query(default=None)`, ⚫️ 🍦 🎏 🎯 ⚖ 👈 🔢 💲. + +: + +```Python +q: Union[str, None] = Query(default=None) +``` + +...⚒ 🔢 📦, 🎏: + +```Python +q: Union[str, None] = None +``` + +& 🐍 3️⃣.1️⃣0️⃣ & 🔛: + +```Python +q: str | None = Query(default=None) +``` + +...⚒ 🔢 📦, 🎏: + +```Python +q: str | None = None +``` + +✋️ ⚫️ 📣 ⚫️ 🎯 💆‍♂ 🔢 🔢. + +!!! info + ✔️ 🤯 👈 🌅 ⚠ 🍕 ⚒ 🔢 📦 🍕: + + ```Python + = None + ``` + + ⚖️: + + ```Python + = Query(default=None) + ``` + + ⚫️ 🔜 ⚙️ 👈 `None` 🔢 💲, & 👈 🌌 ⚒ 🔢 **🚫 ✔**. + + `Union[str, None]` 🍕 ✔ 👆 👨‍🎨 🚚 👻 🐕‍🦺, ✋️ ⚫️ 🚫 ⚫️❔ 💬 FastAPI 👈 👉 🔢 🚫 ✔. + +⤴️, 👥 💪 🚶‍♀️ 🌅 🔢 `Query`. 👉 💼, `max_length` 🔢 👈 ✔ 🎻: + +```Python +q: Union[str, None] = Query(default=None, max_length=50) +``` + +👉 🔜 ✔ 📊, 🎦 🆑 ❌ 🕐❔ 📊 🚫 ☑, & 📄 🔢 🗄 🔗 *➡ 🛠️*. + +## 🚮 🌅 🔬 + +👆 💪 🚮 🔢 `min_length`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} + ``` + +## 🚮 🥔 🧬 + +👆 💪 🔬 🥔 🧬 👈 🔢 🔜 🏏: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!} + ``` + +👉 🎯 🥔 🧬 ✅ 👈 📨 🔢 💲: + +* `^`: ▶️ ⏮️ 📄 🦹, 🚫 ✔️ 🦹 ⏭. +* `fixedquery`: ✔️ ☑ 💲 `fixedquery`. +* `$`: 🔚 📤, 🚫 ✔️ 🙆 🌖 🦹 ⏮️ `fixedquery`. + +🚥 👆 💭 💸 ⏮️ 🌐 👉 **"🥔 🧬"** 💭, 🚫 😟. 👫 🏋️ ❔ 📚 👫👫. 👆 💪 📚 💩 🍵 💆‍♂ 🥔 🧬. + +✋️ 🕐❔ 👆 💪 👫 & 🚶 & 💡 👫, 💭 👈 👆 💪 ⏪ ⚙️ 👫 🔗 **FastAPI**. + +## 🔢 💲 + +🎏 🌌 👈 👆 💪 🚶‍♀️ `None` 💲 `default` 🔢, 👆 💪 🚶‍♀️ 🎏 💲. + +➡️ 💬 👈 👆 💚 📣 `q` 🔢 🔢 ✔️ `min_length` `3`, & ✔️ 🔢 💲 `"fixedquery"`: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial005.py!} +``` + +!!! note + ✔️ 🔢 💲 ⚒ 🔢 📦. + +## ⚒ ⚫️ ✔ + +🕐❔ 👥 🚫 💪 📣 🌅 🔬 ⚖️ 🗃, 👥 💪 ⚒ `q` 🔢 🔢 ✔ 🚫 📣 🔢 💲, 💖: + +```Python +q: str +``` + +↩️: + +```Python +q: Union[str, None] = None +``` + +✋️ 👥 🔜 📣 ⚫️ ⏮️ `Query`, 🖼 💖: + +```Python +q: Union[str, None] = Query(default=None, min_length=3) +``` + +, 🕐❔ 👆 💪 📣 💲 ✔ ⏪ ⚙️ `Query`, 👆 💪 🎯 🚫 📣 🔢 💲: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial006.py!} +``` + +### ✔ ⏮️ ❕ (`...`) + +📤 🎛 🌌 🎯 📣 👈 💲 ✔. 👆 💪 ⚒ `default` 🔢 🔑 💲 `...`: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial006b.py!} +``` + +!!! info + 🚥 👆 🚫 👀 👈 `...` ⏭: ⚫️ 🎁 👁 💲, ⚫️ 🍕 🐍 & 🤙 "❕". + + ⚫️ ⚙️ Pydantic & FastAPI 🎯 📣 👈 💲 ✔. + +👉 🔜 ➡️ **FastAPI** 💭 👈 👉 🔢 ✔. + +### ✔ ⏮️ `None` + +👆 💪 📣 👈 🔢 💪 🚫 `None`, ✋️ 👈 ⚫️ ✔. 👉 🔜 ⚡ 👩‍💻 📨 💲, 🚥 💲 `None`. + +👈, 👆 💪 📣 👈 `None` ☑ 🆎 ✋️ ⚙️ `default=...`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!} + ``` + +!!! tip + Pydantic, ❔ ⚫️❔ 🏋️ 🌐 💽 🔬 & 🛠️ FastAPI, ✔️ 🎁 🎭 🕐❔ 👆 ⚙️ `Optional` ⚖️ `Union[Something, None]` 🍵 🔢 💲, 👆 💪 ✍ 🌅 🔃 ⚫️ Pydantic 🩺 🔃 ✔ 📦 🏑. + +### ⚙️ Pydantic `Required` ↩️ ❕ (`...`) + +🚥 👆 💭 😬 ⚙️ `...`, 👆 💪 🗄 & ⚙️ `Required` ⚪️➡️ Pydantic: + +```Python hl_lines="2 8" +{!../../../docs_src/query_params_str_validations/tutorial006d.py!} +``` + +!!! tip + 💭 👈 🌅 💼, 🕐❔ 🕳 🚚, 👆 💪 🎯 🚫 `default` 🔢, 👆 🛎 🚫 ✔️ ⚙️ `...` 🚫 `Required`. + +## 🔢 🔢 📇 / 💗 💲 + +🕐❔ 👆 🔬 🔢 🔢 🎯 ⏮️ `Query` 👆 💪 📣 ⚫️ 📨 📇 💲, ⚖️ 🙆‍♀ 🎏 🌌, 📨 💗 💲. + +🖼, 📣 🔢 🔢 `q` 👈 💪 😑 💗 🕰 📛, 👆 💪 ✍: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} + ``` + +⤴️, ⏮️ 📛 💖: + +``` +http://localhost:8000/items/?q=foo&q=bar +``` + +👆 🔜 📨 💗 `q` *🔢 🔢'* 💲 (`foo` & `bar`) 🐍 `list` 🔘 👆 *➡ 🛠️ 🔢*, *🔢 🔢* `q`. + +, 📨 👈 📛 🔜: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +!!! tip + 📣 🔢 🔢 ⏮️ 🆎 `list`, 💖 🖼 🔛, 👆 💪 🎯 ⚙️ `Query`, ⏪ ⚫️ 🔜 🔬 📨 💪. + +🎓 🛠️ 🩺 🔜 ℹ ➡️, ✔ 💗 💲: + + + +### 🔢 🔢 📇 / 💗 💲 ⏮️ 🔢 + +& 👆 💪 🔬 🔢 `list` 💲 🚥 👌 🚚: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial012.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} + ``` + +🚥 👆 🚶: + +``` +http://localhost:8000/items/ +``` + +🔢 `q` 🔜: `["foo", "bar"]` & 👆 📨 🔜: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +#### ⚙️ `list` + +👆 💪 ⚙️ `list` 🔗 ↩️ `List[str]` (⚖️ `list[str]` 🐍 3️⃣.9️⃣ ➕): + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial013.py!} +``` + +!!! note + ✔️ 🤯 👈 👉 💼, FastAPI 🏆 🚫 ✅ 🎚 📇. + + 🖼, `List[int]` 🔜 ✅ (& 📄) 👈 🎚 📇 🔢. ✋️ `list` 😞 🚫🔜. + +## 📣 🌅 🗃 + +👆 💪 🚮 🌅 ℹ 🔃 🔢. + +👈 ℹ 🔜 🔌 🏗 🗄 & ⚙️ 🧾 👩‍💻 🔢 & 🔢 🧰. + +!!! note + ✔️ 🤯 👈 🎏 🧰 5️⃣📆 ✔️ 🎏 🎚 🗄 🐕‍🦺. + + 👫 💪 🚫 🎦 🌐 ➕ ℹ 📣, 👐 🌅 💼, ❌ ⚒ ⏪ 📄 🛠️. + +👆 💪 🚮 `title`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} + ``` + +& `description`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="13" + {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} + ``` + +## 📛 🔢 + +🌈 👈 👆 💚 🔢 `item-query`. + +💖: + +``` +http://127.0.0.1:8000/items/?item-query=foobaritems +``` + +✋️ `item-query` 🚫 ☑ 🐍 🔢 📛. + +🔐 🔜 `item_query`. + +✋️ 👆 💪 ⚫️ ⚫️❔ `item-query`... + +⤴️ 👆 💪 📣 `alias`, & 👈 📛 ⚫️❔ 🔜 ⚙️ 🔎 🔢 💲: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} + ``` + +## 😛 🔢 + +🔜 ➡️ 💬 👆 🚫 💖 👉 🔢 🚫🔜. + +👆 ✔️ 👈 ⚫️ 📤 ⏪ ↩️ 📤 👩‍💻 ⚙️ ⚫️, ✋️ 👆 💚 🩺 🎯 🎦 ⚫️ 😢. + +⤴️ 🚶‍♀️ 🔢 `deprecated=True` `Query`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="16" + {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} + ``` + +🩺 🔜 🎦 ⚫️ 💖 👉: + + + +## 🚫 ⚪️➡️ 🗄 + +🚫 🔢 🔢 ⚪️➡️ 🏗 🗄 🔗 (& ➡️, ⚪️➡️ 🏧 🧾 ⚙️), ⚒ 🔢 `include_in_schema` `Query` `False`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} + ``` + +## 🌃 + +👆 💪 📣 🌖 🔬 & 🗃 👆 🔢. + +💊 🔬 & 🗃: + +* `alias` +* `title` +* `description` +* `deprecated` + +🔬 🎯 🎻: + +* `min_length` +* `max_length` +* `regex` + +👫 🖼 👆 👀 ❔ 📣 🔬 `str` 💲. + +👀 ⏭ 📃 👀 ❔ 📣 🔬 🎏 🆎, 💖 🔢. diff --git a/docs/em/docs/tutorial/query-params.md b/docs/em/docs/tutorial/query-params.md new file mode 100644 index 0000000000..ccb235c151 --- /dev/null +++ b/docs/em/docs/tutorial/query-params.md @@ -0,0 +1,225 @@ +# 🔢 🔢 + +🕐❔ 👆 📣 🎏 🔢 🔢 👈 🚫 🍕 ➡ 🔢, 👫 🔁 🔬 "🔢" 🔢. + +```Python hl_lines="9" +{!../../../docs_src/query_params/tutorial001.py!} +``` + +🔢 ⚒ 🔑-💲 👫 👈 🚶 ⏮️ `?` 📛, 🎏 `&` 🦹. + +🖼, 📛: + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +...🔢 🔢: + +* `skip`: ⏮️ 💲 `0` +* `limit`: ⏮️ 💲 `10` + +👫 🍕 📛, 👫 "🛎" 🎻. + +✋️ 🕐❔ 👆 📣 👫 ⏮️ 🐍 🆎 (🖼 🔛, `int`), 👫 🗜 👈 🆎 & ✔ 🛡 ⚫️. + +🌐 🎏 🛠️ 👈 ⚖ ➡ 🔢 ✔ 🔢 🔢: + +* 👨‍🎨 🐕‍🦺 (🎲) +* 💽 "✍" +* 💽 🔬 +* 🏧 🧾 + +## 🔢 + +🔢 🔢 🚫 🔧 🍕 ➡, 👫 💪 📦 & 💪 ✔️ 🔢 💲. + +🖼 🔛 👫 ✔️ 🔢 💲 `skip=0` & `limit=10`. + +, 🔜 📛: + +``` +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`: ↩️ 👆 ⚒ ⚫️ 📛 +* `limit=10`: ↩️ 👈 🔢 💲 + +## 📦 🔢 + +🎏 🌌, 👆 💪 📣 📦 🔢 🔢, ⚒ 👫 🔢 `None`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial002_py310.py!} + ``` + +👉 💼, 🔢 🔢 `q` 🔜 📦, & 🔜 `None` 🔢. + +!!! check + 👀 👈 **FastAPI** 🙃 🥃 👀 👈 ➡ 🔢 `item_id` ➡ 🔢 & `q` 🚫,, ⚫️ 🔢 🔢. + +## 🔢 🔢 🆎 🛠️ + +👆 💪 📣 `bool` 🆎, & 👫 🔜 🗜: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial003_py310.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` ⏮️ `bool` 💲 `True`. ⏪ `False`. + + +## 💗 ➡ & 🔢 🔢 + +👆 💪 📣 💗 ➡ 🔢 & 🔢 🔢 🎏 🕰, **FastAPI** 💭 ❔ ❔. + +& 👆 🚫 ✔️ 📣 👫 🙆 🎯 ✔. + +👫 🔜 🔬 📛: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="6 8" + {!> ../../../docs_src/query_params/tutorial004_py310.py!} + ``` + +## ✔ 🔢 🔢 + +🕐❔ 👆 📣 🔢 💲 🚫-➡ 🔢 (🔜, 👥 ✔️ 🕴 👀 🔢 🔢), ⤴️ ⚫️ 🚫 ✔. + +🚥 👆 🚫 💚 🚮 🎯 💲 ✋️ ⚒ ⚫️ 📦, ⚒ 🔢 `None`. + +✋️ 🕐❔ 👆 💚 ⚒ 🔢 🔢 ✔, 👆 💪 🚫 📣 🙆 🔢 💲: + +```Python hl_lines="6-7" +{!../../../docs_src/query_params/tutorial005.py!} +``` + +📥 🔢 🔢 `needy` ✔ 🔢 🔢 🆎 `str`. + +🚥 👆 📂 👆 🖥 📛 💖: + +``` +http://127.0.0.1:8000/items/foo-item +``` + +...🍵 ❎ ✔ 🔢 `needy`, 👆 🔜 👀 ❌ 💖: + +```JSON +{ + "detail": [ + { + "loc": [ + "query", + "needy" + ], + "msg": "field required", + "type": "value_error.missing" + } + ] +} +``` + +`needy` 🚚 🔢, 👆 🔜 💪 ⚒ ⚫️ 📛: + +``` +http://127.0.0.1:8000/items/foo-item?needy=sooooneedy +``` + +...👉 🔜 👷: + +```JSON +{ + "item_id": "foo-item", + "needy": "sooooneedy" +} +``` + +& ↗️, 👆 💪 🔬 🔢 ✔, ✔️ 🔢 💲, & 🍕 📦: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params/tutorial006.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params/tutorial006_py310.py!} + ``` + +👉 💼, 📤 3️⃣ 🔢 🔢: + +* `needy`, ✔ `str`. +* `skip`, `int` ⏮️ 🔢 💲 `0`. +* `limit`, 📦 `int`. + +!!! tip + 👆 💪 ⚙️ `Enum`Ⓜ 🎏 🌌 ⏮️ [➡ 🔢](path-params.md#predefined-values){.internal-link target=_blank}. diff --git a/docs/em/docs/tutorial/request-files.md b/docs/em/docs/tutorial/request-files.md new file mode 100644 index 0000000000..26631823f2 --- /dev/null +++ b/docs/em/docs/tutorial/request-files.md @@ -0,0 +1,186 @@ +# 📨 📁 + +👆 💪 🔬 📁 📂 👩‍💻 ⚙️ `File`. + +!!! info + 📨 📂 📁, 🥇 ❎ `python-multipart`. + + 🤶 Ⓜ. `pip install python-multipart`. + + 👉 ↩️ 📂 📁 📨 "📨 💽". + +## 🗄 `File` + +🗄 `File` & `UploadFile` ⚪️➡️ `fastapi`: + +```Python hl_lines="1" +{!../../../docs_src/request_files/tutorial001.py!} +``` + +## 🔬 `File` 🔢 + +✍ 📁 🔢 🎏 🌌 👆 🔜 `Body` ⚖️ `Form`: + +```Python hl_lines="7" +{!../../../docs_src/request_files/tutorial001.py!} +``` + +!!! info + `File` 🎓 👈 😖 🔗 ⚪️➡️ `Form`. + + ✋️ 💭 👈 🕐❔ 👆 🗄 `Query`, `Path`, `File` & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. + +!!! tip + 📣 📁 💪, 👆 💪 ⚙️ `File`, ↩️ ⏪ 🔢 🔜 🔬 🔢 🔢 ⚖️ 💪 (🎻) 🔢. + +📁 🔜 📂 "📨 💽". + +🚥 👆 📣 🆎 👆 *➡ 🛠️ 🔢* 🔢 `bytes`, **FastAPI** 🔜 ✍ 📁 👆 & 👆 🔜 📨 🎚 `bytes`. + +✔️ 🤯 👈 👉 ⛓ 👈 🎂 🎚 🔜 🏪 💾. 👉 🔜 👷 👍 🤪 📁. + +✋️ 📤 📚 💼 ❔ 👆 💪 💰 ⚪️➡️ ⚙️ `UploadFile`. + +## 📁 🔢 ⏮️ `UploadFile` + +🔬 📁 🔢 ⏮️ 🆎 `UploadFile`: + +```Python hl_lines="12" +{!../../../docs_src/request_files/tutorial001.py!} +``` + +⚙️ `UploadFile` ✔️ 📚 📈 🤭 `bytes`: + +* 👆 🚫 ✔️ ⚙️ `File()` 🔢 💲 🔢. +* ⚫️ ⚙️ "🧵" 📁: + * 📁 🏪 💾 🆙 🔆 📐 📉, & ⏮️ 🚶‍♀️ 👉 📉 ⚫️ 🔜 🏪 💾. +* 👉 ⛓ 👈 ⚫️ 🔜 👷 👍 ⭕ 📁 💖 🖼, 📹, ⭕ 💱, ♒️. 🍵 😩 🌐 💾. +* 👆 💪 🤚 🗃 ⚪️➡️ 📂 📁. +* ⚫️ ✔️ 📁-💖 `async` 🔢. +* ⚫️ 🎦 ☑ 🐍 `SpooledTemporaryFile` 🎚 👈 👆 💪 🚶‍♀️ 🔗 🎏 🗃 👈 ⌛ 📁-💖 🎚. + +### `UploadFile` + +`UploadFile` ✔️ 📄 🔢: + +* `filename`: `str` ⏮️ ⏮️ 📁 📛 👈 📂 (✅ `myimage.jpg`). +* `content_type`: `str` ⏮️ 🎚 🆎 (📁 🆎 / 📻 🆎) (✅ `image/jpeg`). +* `file`: `SpooledTemporaryFile` ( 📁-💖 🎚). 👉 ☑ 🐍 📁 👈 👆 💪 🚶‍♀️ 🔗 🎏 🔢 ⚖️ 🗃 👈 ⌛ "📁-💖" 🎚. + +`UploadFile` ✔️ 📄 `async` 👩‍🔬. 👫 🌐 🤙 🔗 📁 👩‍🔬 🔘 (⚙️ 🔗 `SpooledTemporaryFile`). + +* `write(data)`: ✍ `data` (`str` ⚖️ `bytes`) 📁. +* `read(size)`: ✍ `size` (`int`) 🔢/🦹 📁. +* `seek(offset)`: 🚶 🔢 🧘 `offset` (`int`) 📁. + * 🤶 Ⓜ., `await myfile.seek(0)` 🔜 🚶 ▶️ 📁. + * 👉 ✴️ ⚠ 🚥 👆 🏃 `await myfile.read()` 🕐 & ⤴️ 💪 ✍ 🎚 🔄. +* `close()`: 🔐 📁. + +🌐 👫 👩‍🔬 `async` 👩‍🔬, 👆 💪 "⌛" 👫. + +🖼, 🔘 `async` *➡ 🛠️ 🔢* 👆 💪 🤚 🎚 ⏮️: + +```Python +contents = await myfile.read() +``` + +🚥 👆 🔘 😐 `def` *➡ 🛠️ 🔢*, 👆 💪 🔐 `UploadFile.file` 🔗, 🖼: + +```Python +contents = myfile.file.read() +``` + +!!! note "`async` 📡 ℹ" + 🕐❔ 👆 ⚙️ `async` 👩‍🔬, **FastAPI** 🏃 📁 👩‍🔬 🧵 & ⌛ 👫. + +!!! note "💃 📡 ℹ" + **FastAPI**'Ⓜ `UploadFile` 😖 🔗 ⚪️➡️ **💃**'Ⓜ `UploadFile`, ✋️ 🚮 💪 🍕 ⚒ ⚫️ 🔗 ⏮️ **Pydantic** & 🎏 🍕 FastAPI. + +## ⚫️❔ "📨 💽" + +🌌 🕸 📨 (`
`) 📨 💽 💽 🛎 ⚙️ "🎁" 🔢 👈 📊, ⚫️ 🎏 ⚪️➡️ 🎻. + +**FastAPI** 🔜 ⚒ 💭 ✍ 👈 📊 ⚪️➡️ ▶️️ 🥉 ↩️ 🎻. + +!!! note "📡 ℹ" + 📊 ⚪️➡️ 📨 🛎 🗜 ⚙️ "📻 🆎" `application/x-www-form-urlencoded` 🕐❔ ⚫️ 🚫 🔌 📁. + + ✋️ 🕐❔ 📨 🔌 📁, ⚫️ 🗜 `multipart/form-data`. 🚥 👆 ⚙️ `File`, **FastAPI** 🔜 💭 ⚫️ ✔️ 🤚 📁 ⚪️➡️ ☑ 🍕 💪. + + 🚥 👆 💚 ✍ 🌖 🔃 👉 🔢 & 📨 🏑, 👳 🏇 🕸 🩺 POST. + +!!! warning + 👆 💪 📣 💗 `File` & `Form` 🔢 *➡ 🛠️*, ✋️ 👆 💪 🚫 📣 `Body` 🏑 👈 👆 ⌛ 📨 🎻, 📨 🔜 ✔️ 💪 🗜 ⚙️ `multipart/form-data` ↩️ `application/json`. + + 👉 🚫 🚫 **FastAPI**, ⚫️ 🍕 🇺🇸🔍 🛠️. + +## 📦 📁 📂 + +👆 💪 ⚒ 📁 📦 ⚙️ 🐩 🆎 ✍ & ⚒ 🔢 💲 `None`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7 14" + {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} + ``` + +## `UploadFile` ⏮️ 🌖 🗃 + +👆 💪 ⚙️ `File()` ⏮️ `UploadFile`, 🖼, ⚒ 🌖 🗃: + +```Python hl_lines="13" +{!../../../docs_src/request_files/tutorial001_03.py!} +``` + +## 💗 📁 📂 + +⚫️ 💪 📂 📚 📁 🎏 🕰. + +👫 🔜 👨‍💼 🎏 "📨 🏑" 📨 ⚙️ "📨 💽". + +⚙️ 👈, 📣 📇 `bytes` ⚖️ `UploadFile`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="8 13" + {!> ../../../docs_src/request_files/tutorial002_py39.py!} + ``` + +👆 🔜 📨, 📣, `list` `bytes` ⚖️ `UploadFile`Ⓜ. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import HTMLResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. + +### 💗 📁 📂 ⏮️ 🌖 🗃 + +& 🎏 🌌 ⏭, 👆 💪 ⚙️ `File()` ⚒ 🌖 🔢, `UploadFile`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="16" + {!> ../../../docs_src/request_files/tutorial003_py39.py!} + ``` + +## 🌃 + +⚙️ `File`, `bytes`, & `UploadFile` 📣 📁 📂 📨, 📨 📨 💽. diff --git a/docs/em/docs/tutorial/request-forms-and-files.md b/docs/em/docs/tutorial/request-forms-and-files.md new file mode 100644 index 0000000000..99aeca0003 --- /dev/null +++ b/docs/em/docs/tutorial/request-forms-and-files.md @@ -0,0 +1,35 @@ +# 📨 📨 & 📁 + +👆 💪 🔬 📁 & 📨 🏑 🎏 🕰 ⚙️ `File` & `Form`. + +!!! info + 📨 📂 📁 & /⚖️ 📨 📊, 🥇 ❎ `python-multipart`. + + 🤶 Ⓜ. `pip install python-multipart`. + +## 🗄 `File` & `Form` + +```Python hl_lines="1" +{!../../../docs_src/request_forms_and_files/tutorial001.py!} +``` + +## 🔬 `File` & `Form` 🔢 + +✍ 📁 & 📨 🔢 🎏 🌌 👆 🔜 `Body` ⚖️ `Query`: + +```Python hl_lines="8" +{!../../../docs_src/request_forms_and_files/tutorial001.py!} +``` + +📁 & 📨 🏑 🔜 📂 📨 📊 & 👆 🔜 📨 📁 & 📨 🏑. + +& 👆 💪 📣 📁 `bytes` & `UploadFile`. + +!!! warning + 👆 💪 📣 💗 `File` & `Form` 🔢 *➡ 🛠️*, ✋️ 👆 💪 🚫 📣 `Body` 🏑 👈 👆 ⌛ 📨 🎻, 📨 🔜 ✔️ 💪 🗜 ⚙️ `multipart/form-data` ↩️ `application/json`. + + 👉 🚫 🚫 **FastAPI**, ⚫️ 🍕 🇺🇸🔍 🛠️. + +## 🌃 + +⚙️ `File` & `Form` 👯‍♂️ 🕐❔ 👆 💪 📨 💽 & 📁 🎏 📨. diff --git a/docs/em/docs/tutorial/request-forms.md b/docs/em/docs/tutorial/request-forms.md new file mode 100644 index 0000000000..fa74adae5d --- /dev/null +++ b/docs/em/docs/tutorial/request-forms.md @@ -0,0 +1,58 @@ +# 📨 💽 + +🕐❔ 👆 💪 📨 📨 🏑 ↩️ 🎻, 👆 💪 ⚙️ `Form`. + +!!! info + ⚙️ 📨, 🥇 ❎ `python-multipart`. + + 🤶 Ⓜ. `pip install python-multipart`. + +## 🗄 `Form` + +🗄 `Form` ⚪️➡️ `fastapi`: + +```Python hl_lines="1" +{!../../../docs_src/request_forms/tutorial001.py!} +``` + +## 🔬 `Form` 🔢 + +✍ 📨 🔢 🎏 🌌 👆 🔜 `Body` ⚖️ `Query`: + +```Python hl_lines="7" +{!../../../docs_src/request_forms/tutorial001.py!} +``` + +🖼, 1️⃣ 🌌 Oauth2️⃣ 🔧 💪 ⚙️ (🤙 "🔐 💧") ⚫️ ✔ 📨 `username` & `password` 📨 🏑. + +🔌 🚚 🏑 ⚫️❔ 📛 `username` & `password`, & 📨 📨 🏑, 🚫 🎻. + +⏮️ `Form` 👆 💪 📣 🎏 📳 ⏮️ `Body` (& `Query`, `Path`, `Cookie`), 🔌 🔬, 🖼, 📛 (✅ `user-name` ↩️ `username`), ♒️. + +!!! info + `Form` 🎓 👈 😖 🔗 ⚪️➡️ `Body`. + +!!! tip + 📣 📨 💪, 👆 💪 ⚙️ `Form` 🎯, ↩️ 🍵 ⚫️ 🔢 🔜 🔬 🔢 🔢 ⚖️ 💪 (🎻) 🔢. + +## 🔃 "📨 🏑" + +🌌 🕸 📨 (`
`) 📨 💽 💽 🛎 ⚙️ "🎁" 🔢 👈 📊, ⚫️ 🎏 ⚪️➡️ 🎻. + +**FastAPI** 🔜 ⚒ 💭 ✍ 👈 📊 ⚪️➡️ ▶️️ 🥉 ↩️ 🎻. + +!!! note "📡 ℹ" + 📊 ⚪️➡️ 📨 🛎 🗜 ⚙️ "📻 🆎" `application/x-www-form-urlencoded`. + + ✋️ 🕐❔ 📨 🔌 📁, ⚫️ 🗜 `multipart/form-data`. 👆 🔜 ✍ 🔃 🚚 📁 ⏭ 📃. + + 🚥 👆 💚 ✍ 🌖 🔃 👉 🔢 & 📨 🏑, 👳 🏇 🕸 🩺 POST. + +!!! warning + 👆 💪 📣 💗 `Form` 🔢 *➡ 🛠️*, ✋️ 👆 💪 🚫 📣 `Body` 🏑 👈 👆 ⌛ 📨 🎻, 📨 🔜 ✔️ 💪 🗜 ⚙️ `application/x-www-form-urlencoded` ↩️ `application/json`. + + 👉 🚫 🚫 **FastAPI**, ⚫️ 🍕 🇺🇸🔍 🛠️. + +## 🌃 + +⚙️ `Form` 📣 📨 💽 🔢 🔢. diff --git a/docs/em/docs/tutorial/response-model.md b/docs/em/docs/tutorial/response-model.md new file mode 100644 index 0000000000..6ea4413f89 --- /dev/null +++ b/docs/em/docs/tutorial/response-model.md @@ -0,0 +1,481 @@ +# 📨 🏷 - 📨 🆎 + +👆 💪 📣 🆎 ⚙️ 📨 ✍ *➡ 🛠️ 🔢* **📨 🆎**. + +👆 💪 ⚙️ **🆎 ✍** 🎏 🌌 👆 🔜 🔢 💽 🔢 **🔢**, 👆 💪 ⚙️ Pydantic 🏷, 📇, 📖, 📊 💲 💖 🔢, 🎻, ♒️. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="16 21" + {!> ../../../docs_src/response_model/tutorial001_01_py310.py!} + ``` + +FastAPI 🔜 ⚙️ 👉 📨 🆎: + +* **✔** 📨 💽. + * 🚥 💽 ❌ (✅ 👆 ❌ 🏑), ⚫️ ⛓ 👈 *👆* 📱 📟 💔, 🚫 🛬 ⚫️❔ ⚫️ 🔜, & ⚫️ 🔜 📨 💽 ❌ ↩️ 🛬 ❌ 💽. 👉 🌌 👆 & 👆 👩‍💻 💪 🎯 👈 👫 🔜 📨 💽 & 💽 💠 📈. +* 🚮 **🎻 🔗** 📨, 🗄 *➡ 🛠️*. + * 👉 🔜 ⚙️ **🏧 🩺**. + * ⚫️ 🔜 ⚙️ 🏧 👩‍💻 📟 ⚡ 🧰. + +✋️ 🏆 🥈: + +* ⚫️ 🔜 **📉 & ⛽** 🔢 📊 ⚫️❔ 🔬 📨 🆎. + * 👉 ✴️ ⚠ **💂‍♂**, 👥 🔜 👀 🌅 👈 🔛. + +## `response_model` 🔢 + +📤 💼 🌐❔ 👆 💪 ⚖️ 💚 📨 💽 👈 🚫 ⚫️❔ ⚫️❔ 🆎 📣. + +🖼, 👆 💪 💚 **📨 📖** ⚖️ 💽 🎚, ✋️ **📣 ⚫️ Pydantic 🏷**. 👉 🌌 Pydantic 🏷 🔜 🌐 💽 🧾, 🔬, ♒️. 🎚 👈 👆 📨 (✅ 📖 ⚖️ 💽 🎚). + +🚥 👆 🚮 📨 🆎 ✍, 🧰 & 👨‍🎨 🔜 😭 ⏮️ (☑) ❌ 💬 👆 👈 👆 🔢 🛬 🆎 (✅#️⃣) 👈 🎏 ⚪️➡️ ⚫️❔ 👆 📣 (✅ Pydantic 🏷). + +📚 💼, 👆 💪 ⚙️ *➡ 🛠️ 👨‍🎨* 🔢 `response_model` ↩️ 📨 🆎. + +👆 💪 ⚙️ `response_model` 🔢 🙆 *➡ 🛠️*: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* ♒️. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py310.py!} + ``` + +!!! note + 👀 👈 `response_model` 🔢 "👨‍🎨" 👩‍🔬 (`get`, `post`, ♒️). 🚫 👆 *➡ 🛠️ 🔢*, 💖 🌐 🔢 & 💪. + +`response_model` 📨 🎏 🆎 👆 🔜 📣 Pydantic 🏷 🏑,, ⚫️ 💪 Pydantic 🏷, ✋️ ⚫️ 💪, ✅ `list` Pydantic 🏷, 💖 `List[Item]`. + +FastAPI 🔜 ⚙️ 👉 `response_model` 🌐 💽 🧾, 🔬, ♒️. & **🗜 & ⛽ 🔢 📊** 🚮 🆎 📄. + +!!! tip + 🚥 👆 ✔️ ⚠ 🆎 ✅ 👆 👨‍🎨, ✍, ♒️, 👆 💪 📣 🔢 📨 🆎 `Any`. + + 👈 🌌 👆 💬 👨‍🎨 👈 👆 😫 🛬 🕳. ✋️ FastAPI 🔜 💽 🧾, 🔬, 🖥, ♒️. ⏮️ `response_model`. + +### `response_model` 📫 + +🚥 👆 📣 👯‍♂️ 📨 🆎 & `response_model`, `response_model` 🔜 ✊ 📫 & ⚙️ FastAPI. + +👉 🌌 👆 💪 🚮 ☑ 🆎 ✍ 👆 🔢 🕐❔ 👆 🛬 🆎 🎏 🌘 📨 🏷, ⚙️ 👨‍🎨 & 🧰 💖 ✍. & 👆 💪 ✔️ FastAPI 💽 🔬, 🧾, ♒️. ⚙️ `response_model`. + +👆 💪 ⚙️ `response_model=None` ❎ 🏗 📨 🏷 👈 *➡ 🛠️*, 👆 5️⃣📆 💪 ⚫️ 🚥 👆 ❎ 🆎 ✍ 👜 👈 🚫 ☑ Pydantic 🏑, 👆 🔜 👀 🖼 👈 1️⃣ 📄 🔛. + +## 📨 🎏 🔢 💽 + +📥 👥 📣 `UserIn` 🏷, ⚫️ 🔜 🔌 🔢 🔐: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9 11" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7 9" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` + +!!! info + ⚙️ `EmailStr`, 🥇 ❎ `email_validator`. + + 🤶 Ⓜ. `pip install email-validator` + ⚖️ `pip install pydantic[email]`. + +& 👥 ⚙️ 👉 🏷 📣 👆 🔢 & 🎏 🏷 📣 👆 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="16" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` + +🔜, 🕐❔ 🖥 🏗 👩‍💻 ⏮️ 🔐, 🛠️ 🔜 📨 🎏 🔐 📨. + +👉 💼, ⚫️ 💪 🚫 ⚠, ↩️ ⚫️ 🎏 👩‍💻 📨 🔐. + +✋️ 🚥 👥 ⚙️ 🎏 🏷 ➕1️⃣ *➡ 🛠️*, 👥 💪 📨 👆 👩‍💻 🔐 🔠 👩‍💻. + +!!! danger + 🙅 🏪 ✅ 🔐 👩‍💻 ⚖️ 📨 ⚫️ 📨 💖 👉, 🚥 👆 💭 🌐 ⚠ & 👆 💭 ⚫️❔ 👆 🔨. + +## 🚮 🔢 🏷 + +👥 💪 ↩️ ✍ 🔢 🏷 ⏮️ 🔢 🔐 & 🔢 🏷 🍵 ⚫️: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +📥, ✋️ 👆 *➡ 🛠️ 🔢* 🛬 🎏 🔢 👩‍💻 👈 🔌 🔐: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +...👥 📣 `response_model` 👆 🏷 `UserOut`, 👈 🚫 🔌 🔐: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +, **FastAPI** 🔜 ✊ 💅 🖥 👅 🌐 💽 👈 🚫 📣 🔢 🏷 (⚙️ Pydantic). + +### `response_model` ⚖️ 📨 🆎 + +👉 💼, ↩️ 2️⃣ 🏷 🎏, 🚥 👥 ✍ 🔢 📨 🆎 `UserOut`, 👨‍🎨 & 🧰 🔜 😭 👈 👥 🛬 ❌ 🆎, 📚 🎏 🎓. + +👈 ⚫️❔ 👉 🖼 👥 ✔️ 📣 ⚫️ `response_model` 🔢. + +...✋️ 😣 👂 🔛 👀 ❔ ❎ 👈. + +## 📨 🆎 & 💽 🖥 + +➡️ 😣 ⚪️➡️ ⏮️ 🖼. 👥 💚 **✍ 🔢 ⏮️ 1️⃣ 🆎** ✋️ 📨 🕳 👈 🔌 **🌅 💽**. + +👥 💚 FastAPI 🚧 **🖥** 📊 ⚙️ 📨 🏷. + +⏮️ 🖼, ↩️ 🎓 🎏, 👥 ✔️ ⚙️ `response_model` 🔢. ✋️ 👈 ⛓ 👈 👥 🚫 🤚 🐕‍🦺 ⚪️➡️ 👨‍🎨 & 🧰 ✅ 🔢 📨 🆎. + +✋️ 🌅 💼 🌐❔ 👥 💪 🕳 💖 👉, 👥 💚 🏷 **⛽/❎** 📊 👉 🖼. + +& 👈 💼, 👥 💪 ⚙️ 🎓 & 🧬 ✊ 📈 🔢 **🆎 ✍** 🤚 👍 🐕‍🦺 👨‍🎨 & 🧰, & 🤚 FastAPI **💽 🖥**. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9-13 15-16 20" + {!> ../../../docs_src/response_model/tutorial003_01.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7-10 13-14 18" + {!> ../../../docs_src/response_model/tutorial003_01_py310.py!} + ``` + +⏮️ 👉, 👥 🤚 🏭 🐕‍🦺, ⚪️➡️ 👨‍🎨 & ✍ 👉 📟 ☑ ⚖ 🆎, ✋️ 👥 🤚 💽 🖥 ⚪️➡️ FastAPI. + +❔ 🔨 👉 👷 ❓ ➡️ ✅ 👈 👅. 👶 + +### 🆎 ✍ & 🏭 + +🥇 ➡️ 👀 ❔ 👨‍🎨, ✍ & 🎏 🧰 🔜 👀 👉. + +`BaseUser` ✔️ 🧢 🏑. ⤴️ `UserIn` 😖 ⚪️➡️ `BaseUser` & 🚮 `password` 🏑,, ⚫️ 🔜 🔌 🌐 🏑 ⚪️➡️ 👯‍♂️ 🏷. + +👥 ✍ 🔢 📨 🆎 `BaseUser`, ✋️ 👥 🤙 🛬 `UserIn` 👐. + +👨‍🎨, ✍, & 🎏 🧰 🏆 🚫 😭 🔃 👉 ↩️, ⌨ ⚖, `UserIn` 🏿 `BaseUser`, ❔ ⛓ ⚫️ *☑* 🆎 🕐❔ ⚫️❔ ⌛ 🕳 👈 `BaseUser`. + +### FastAPI 💽 🖥 + +🔜, FastAPI, ⚫️ 🔜 👀 📨 🆎 & ⚒ 💭 👈 ⚫️❔ 👆 📨 🔌 **🕴** 🏑 👈 📣 🆎. + +FastAPI 🔨 📚 👜 🔘 ⏮️ Pydantic ⚒ 💭 👈 📚 🎏 🚫 🎓 🧬 🚫 ⚙️ 📨 💽 🖥, ⏪ 👆 💪 🔚 🆙 🛬 🌅 🌅 💽 🌘 ⚫️❔ 👆 📈. + +👉 🌌, 👆 💪 🤚 🏆 👯‍♂️ 🌏: 🆎 ✍ ⏮️ **🏭 🐕‍🦺** & **💽 🖥**. + +## 👀 ⚫️ 🩺 + +🕐❔ 👆 👀 🏧 🩺, 👆 💪 ✅ 👈 🔢 🏷 & 🔢 🏷 🔜 👯‍♂️ ✔️ 👫 👍 🎻 🔗: + + + +& 👯‍♂️ 🏷 🔜 ⚙️ 🎓 🛠️ 🧾: + + + +## 🎏 📨 🆎 ✍ + +📤 5️⃣📆 💼 🌐❔ 👆 📨 🕳 👈 🚫 ☑ Pydantic 🏑 & 👆 ✍ ⚫️ 🔢, 🕴 🤚 🐕‍🦺 🚚 🏭 (👨‍🎨, ✍, ♒️). + +### 📨 📨 🔗 + +🏆 ⚠ 💼 🔜 [🛬 📨 🔗 🔬 ⏪ 🏧 🩺](../advanced/response-directly.md){.internal-link target=_blank}. + +```Python hl_lines="8 10-11" +{!> ../../../docs_src/response_model/tutorial003_02.py!} +``` + +👉 🙅 💼 🍵 🔁 FastAPI ↩️ 📨 🆎 ✍ 🎓 (⚖️ 🏿) `Response`. + +& 🧰 🔜 😄 ↩️ 👯‍♂️ `RedirectResponse` & `JSONResponse` 🏿 `Response`, 🆎 ✍ ☑. + +### ✍ 📨 🏿 + +👆 💪 ⚙️ 🏿 `Response` 🆎 ✍: + +```Python hl_lines="8-9" +{!> ../../../docs_src/response_model/tutorial003_03.py!} +``` + +👉 🔜 👷 ↩️ `RedirectResponse` 🏿 `Response`, & FastAPI 🔜 🔁 🍵 👉 🙅 💼. + +### ❌ 📨 🆎 ✍ + +✋️ 🕐❔ 👆 📨 🎏 ❌ 🎚 👈 🚫 ☑ Pydantic 🆎 (✅ 💽 🎚) & 👆 ✍ ⚫️ 💖 👈 🔢, FastAPI 🔜 🔄 ✍ Pydantic 📨 🏷 ⚪️➡️ 👈 🆎 ✍, & 🔜 ❌. + +🎏 🔜 🔨 🚥 👆 ✔️ 🕳 💖 🇪🇺 🖖 🎏 🆎 🌐❔ 1️⃣ ⚖️ 🌅 👫 🚫 ☑ Pydantic 🆎, 🖼 👉 🔜 ❌ 👶: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/response_model/tutorial003_04.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="8" + {!> ../../../docs_src/response_model/tutorial003_04_py310.py!} + ``` + +...👉 ❌ ↩️ 🆎 ✍ 🚫 Pydantic 🆎 & 🚫 👁 `Response` 🎓 ⚖️ 🏿, ⚫️ 🇪🇺 (🙆 2️⃣) 🖖 `Response` & `dict`. + +### ❎ 📨 🏷 + +▶️ ⚪️➡️ 🖼 🔛, 👆 5️⃣📆 🚫 💚 ✔️ 🔢 💽 🔬, 🧾, 🖥, ♒️. 👈 🎭 FastAPI. + +✋️ 👆 💪 💚 🚧 📨 🆎 ✍ 🔢 🤚 🐕‍🦺 ⚪️➡️ 🧰 💖 👨‍🎨 & 🆎 ☑ (✅ ✍). + +👉 💼, 👆 💪 ❎ 📨 🏷 ⚡ ⚒ `response_model=None`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/response_model/tutorial003_05.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/response_model/tutorial003_05_py310.py!} + ``` + +👉 🔜 ⚒ FastAPI 🚶 📨 🏷 ⚡ & 👈 🌌 👆 💪 ✔️ 🙆 📨 🆎 ✍ 👆 💪 🍵 ⚫️ 🤕 👆 FastAPI 🈸. 👶 + +## 📨 🏷 🔢 🔢 + +👆 📨 🏷 💪 ✔️ 🔢 💲, 💖: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="9 11-12" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` + +* `description: Union[str, None] = None` (⚖️ `str | None = None` 🐍 3️⃣.1️⃣0️⃣) ✔️ 🔢 `None`. +* `tax: float = 10.5` ✔️ 🔢 `10.5`. +* `tags: List[str] = []` 🔢 🛁 📇: `[]`. + +✋️ 👆 💪 💚 🚫 👫 ⚪️➡️ 🏁 🚥 👫 🚫 🤙 🏪. + +🖼, 🚥 👆 ✔️ 🏷 ⏮️ 📚 📦 🔢 ☁ 💽, ✋️ 👆 🚫 💚 📨 📶 📏 🎻 📨 🌕 🔢 💲. + +### ⚙️ `response_model_exclude_unset` 🔢 + +👆 💪 ⚒ *➡ 🛠️ 👨‍🎨* 🔢 `response_model_exclude_unset=True`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` + +& 👈 🔢 💲 🏆 🚫 🔌 📨, 🕴 💲 🤙 ⚒. + +, 🚥 👆 📨 📨 👈 *➡ 🛠️* 🏬 ⏮️ 🆔 `foo`, 📨 (🚫 ✅ 🔢 💲) 🔜: + +```JSON +{ + "name": "Foo", + "price": 50.2 +} +``` + +!!! info + FastAPI ⚙️ Pydantic 🏷 `.dict()` ⏮️ 🚮 `exclude_unset` 🔢 🏆 👉. + +!!! info + 👆 💪 ⚙️: + + * `response_model_exclude_defaults=True` + * `response_model_exclude_none=True` + + 🔬 Pydantic 🩺 `exclude_defaults` & `exclude_none`. + +#### 📊 ⏮️ 💲 🏑 ⏮️ 🔢 + +✋️ 🚥 👆 📊 ✔️ 💲 🏷 🏑 ⏮️ 🔢 💲, 💖 🏬 ⏮️ 🆔 `bar`: + +```Python hl_lines="3 5" +{ + "name": "Bar", + "description": "The bartenders", + "price": 62, + "tax": 20.2 +} +``` + +👫 🔜 🔌 📨. + +#### 📊 ⏮️ 🎏 💲 🔢 + +🚥 📊 ✔️ 🎏 💲 🔢 🕐, 💖 🏬 ⏮️ 🆔 `baz`: + +```Python hl_lines="3 5-6" +{ + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [] +} +``` + +FastAPI 🙃 🥃 (🤙, Pydantic 🙃 🥃) 🤔 👈, ✋️ `description`, `tax`, & `tags` ✔️ 🎏 💲 🔢, 👫 ⚒ 🎯 (↩️ ✊ ⚪️➡️ 🔢). + +, 👫 🔜 🔌 🎻 📨. + +!!! tip + 👀 👈 🔢 💲 💪 🕳, 🚫 🕴 `None`. + + 👫 💪 📇 (`[]`), `float` `10.5`, ♒️. + +### `response_model_include` & `response_model_exclude` + +👆 💪 ⚙️ *➡ 🛠️ 👨‍🎨* 🔢 `response_model_include` & `response_model_exclude`. + +👫 ✊ `set` `str` ⏮️ 📛 🔢 🔌 (❎ 🎂) ⚖️ 🚫 (✅ 🎂). + +👉 💪 ⚙️ ⏩ ⌨ 🚥 👆 ✔️ 🕴 1️⃣ Pydantic 🏷 & 💚 ❎ 💽 ⚪️➡️ 🔢. + +!!! tip + ✋️ ⚫️ 👍 ⚙️ 💭 🔛, ⚙️ 💗 🎓, ↩️ 👫 🔢. + + 👉 ↩️ 🎻 🔗 🏗 👆 📱 🗄 (& 🩺) 🔜 1️⃣ 🏁 🏷, 🚥 👆 ⚙️ `response_model_include` ⚖️ `response_model_exclude` 🚫 🔢. + + 👉 ✔ `response_model_by_alias` 👈 👷 ➡. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial005.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial005_py310.py!} + ``` + +!!! tip + ❕ `{"name", "description"}` ✍ `set` ⏮️ 📚 2️⃣ 💲. + + ⚫️ 🌓 `set(["name", "description"])`. + +#### ⚙️ `list`Ⓜ ↩️ `set`Ⓜ + +🚥 👆 💭 ⚙️ `set` & ⚙️ `list` ⚖️ `tuple` ↩️, FastAPI 🔜 🗜 ⚫️ `set` & ⚫️ 🔜 👷 ☑: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial006.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial006_py310.py!} + ``` + +## 🌃 + +⚙️ *➡ 🛠️ 👨‍🎨* 🔢 `response_model` 🔬 📨 🏷 & ✴️ 🚚 📢 💽 ⛽ 👅. + +⚙️ `response_model_exclude_unset` 📨 🕴 💲 🎯 ⚒. diff --git a/docs/em/docs/tutorial/response-status-code.md b/docs/em/docs/tutorial/response-status-code.md new file mode 100644 index 0000000000..e5149de7db --- /dev/null +++ b/docs/em/docs/tutorial/response-status-code.md @@ -0,0 +1,89 @@ +# 📨 👔 📟 + +🎏 🌌 👆 💪 ✔ 📨 🏷, 👆 💪 📣 🇺🇸🔍 👔 📟 ⚙️ 📨 ⏮️ 🔢 `status_code` 🙆 *➡ 🛠️*: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* ♒️. + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +!!! note + 👀 👈 `status_code` 🔢 "👨‍🎨" 👩‍🔬 (`get`, `post`, ♒️). 🚫 👆 *➡ 🛠️ 🔢*, 💖 🌐 🔢 & 💪. + +`status_code` 🔢 📨 🔢 ⏮️ 🇺🇸🔍 👔 📟. + +!!! info + `status_code` 💪 👐 📨 `IntEnum`, ✅ 🐍 `http.HTTPStatus`. + +⚫️ 🔜: + +* 📨 👈 👔 📟 📨. +* 📄 ⚫️ ✅ 🗄 🔗 ( & , 👩‍💻 🔢): + + + +!!! note + 📨 📟 (👀 ⏭ 📄) 🎦 👈 📨 🔨 🚫 ✔️ 💪. + + FastAPI 💭 👉, & 🔜 🏭 🗄 🩺 👈 🇵🇸 📤 🙅‍♂ 📨 💪. + +## 🔃 🇺🇸🔍 👔 📟 + +!!! note + 🚥 👆 ⏪ 💭 ⚫️❔ 🇺🇸🔍 👔 📟, 🚶 ⏭ 📄. + +🇺🇸🔍, 👆 📨 🔢 👔 📟 3️⃣ 9️⃣ 🍕 📨. + +👫 👔 📟 ✔️ 📛 🔗 🤔 👫, ✋️ ⚠ 🍕 🔢. + +📏: + +* `100` & 🔛 "ℹ". 👆 🛎 ⚙️ 👫 🔗. 📨 ⏮️ 👫 👔 📟 🚫🔜 ✔️ 💪. +* **`200`** & 🔛 "🏆" 📨. 👫 🕐 👆 🔜 ⚙️ 🏆. + * `200` 🔢 👔 📟, ❔ ⛓ 🌐 "👌". + * ➕1️⃣ 🖼 🔜 `201`, "✍". ⚫️ 🛎 ⚙️ ⏮️ 🏗 🆕 ⏺ 💽. + * 🎁 💼 `204`, "🙅‍♂ 🎚". 👉 📨 ⚙️ 🕐❔ 📤 🙅‍♂ 🎚 📨 👩‍💻, & 📨 🔜 🚫 ✔️ 💪. +* **`300`** & 🔛 "❎". 📨 ⏮️ 👫 👔 📟 5️⃣📆 ⚖️ 5️⃣📆 🚫 ✔️ 💪, 🌖 `304`, "🚫 🔀", ❔ 🔜 🚫 ✔️ 1️⃣. +* **`400`** & 🔛 "👩‍💻 ❌" 📨. 👫 🥈 🆎 👆 🔜 🎲 ⚙️ 🏆. + * 🖼 `404`, "🚫 🔎" 📨. + * 💊 ❌ ⚪️➡️ 👩‍💻, 👆 💪 ⚙️ `400`. +* `500` & 🔛 💽 ❌. 👆 🌖 🙅 ⚙️ 👫 🔗. 🕐❔ 🕳 🚶 ❌ 🍕 👆 🈸 📟, ⚖️ 💽, ⚫️ 🔜 🔁 📨 1️⃣ 👫 👔 📟. + +!!! tip + 💭 🌅 🔃 🔠 👔 📟 & ❔ 📟 ⚫️❔, ✅ 🏇 🧾 🔃 🇺🇸🔍 👔 📟. + +## ⌨ 💭 📛 + +➡️ 👀 ⏮️ 🖼 🔄: + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +`201` 👔 📟 "✍". + +✋️ 👆 🚫 ✔️ ✍ ⚫️❔ 🔠 👉 📟 ⛓. + +👆 💪 ⚙️ 🏪 🔢 ⚪️➡️ `fastapi.status`. + +```Python hl_lines="1 6" +{!../../../docs_src/response_status_code/tutorial002.py!} +``` + +👫 🏪, 👫 🧑‍🤝‍🧑 🎏 🔢, ✋️ 👈 🌌 👆 💪 ⚙️ 👨‍🎨 📋 🔎 👫: + + + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette import status`. + + **FastAPI** 🚚 🎏 `starlette.status` `fastapi.status` 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. + +## 🔀 🔢 + +⏪, [🏧 👩‍💻 🦮](../advanced/response-change-status-code.md){.internal-link target=_blank}, 👆 🔜 👀 ❔ 📨 🎏 👔 📟 🌘 🔢 👆 📣 📥. diff --git a/docs/em/docs/tutorial/schema-extra-example.md b/docs/em/docs/tutorial/schema-extra-example.md new file mode 100644 index 0000000000..d5bf8810aa --- /dev/null +++ b/docs/em/docs/tutorial/schema-extra-example.md @@ -0,0 +1,141 @@ +# 📣 📨 🖼 💽 + +👆 💪 📣 🖼 💽 👆 📱 💪 📨. + +📥 📚 🌌 ⚫️. + +## Pydantic `schema_extra` + +👆 💪 📣 `example` Pydantic 🏷 ⚙️ `Config` & `schema_extra`, 🔬 Pydantic 🩺: 🔗 🛃: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="15-23" + {!> ../../../docs_src/schema_extra_example/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="13-21" + {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} + ``` + +👈 ➕ ℹ 🔜 🚮-🔢 **🎻 🔗** 👈 🏷, & ⚫️ 🔜 ⚙️ 🛠️ 🩺. + +!!! tip + 👆 💪 ⚙️ 🎏 ⚒ ↔ 🎻 🔗 & 🚮 👆 👍 🛃 ➕ ℹ. + + 🖼 👆 💪 ⚙️ ⚫️ 🚮 🗃 🕸 👩‍💻 🔢, ♒️. + +## `Field` 🌖 ❌ + +🕐❔ ⚙️ `Field()` ⏮️ Pydantic 🏷, 👆 💪 📣 ➕ ℹ **🎻 🔗** 🚶‍♀️ 🙆 🎏 ❌ ❌ 🔢. + +👆 💪 ⚙️ 👉 🚮 `example` 🔠 🏑: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="4 10-13" + {!> ../../../docs_src/schema_extra_example/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="2 8-11" + {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} + ``` + +!!! warning + 🚧 🤯 👈 📚 ➕ ❌ 🚶‍♀️ 🏆 🚫 🚮 🙆 🔬, 🕴 ➕ ℹ, 🧾 🎯. + +## `example` & `examples` 🗄 + +🕐❔ ⚙️ 🙆: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +👆 💪 📣 💽 `example` ⚖️ 👪 `examples` ⏮️ 🌖 ℹ 👈 🔜 🚮 **🗄**. + +### `Body` ⏮️ `example` + +📥 👥 🚶‍♀️ `example` 📊 ⌛ `Body()`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="20-25" + {!> ../../../docs_src/schema_extra_example/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="18-23" + {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} + ``` + +### 🖼 🩺 🎚 + +⏮️ 🙆 👩‍🔬 🔛 ⚫️ 🔜 👀 💖 👉 `/docs`: + + + +### `Body` ⏮️ 💗 `examples` + +👐 👁 `example`, 👆 💪 🚶‍♀️ `examples` ⚙️ `dict` ⏮️ **💗 🖼**, 🔠 ⏮️ ➕ ℹ 👈 🔜 🚮 **🗄** 💁‍♂️. + +🔑 `dict` 🔬 🔠 🖼, & 🔠 💲 ➕1️⃣ `dict`. + +🔠 🎯 🖼 `dict` `examples` 💪 🔌: + +* `summary`: 📏 📛 🖼. +* `description`: 📏 📛 👈 💪 🔌 ✍ ✍. +* `value`: 👉 ☑ 🖼 🎦, ✅ `dict`. +* `externalValue`: 🎛 `value`, 📛 ☝ 🖼. 👐 👉 5️⃣📆 🚫 🐕‍🦺 📚 🧰 `value`. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="21-47" + {!> ../../../docs_src/schema_extra_example/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="19-45" + {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} + ``` + +### 🖼 🩺 🎚 + +⏮️ `examples` 🚮 `Body()` `/docs` 🔜 👀 💖: + + + +## 📡 ℹ + +!!! warning + 👉 📶 📡 ℹ 🔃 🐩 **🎻 🔗** & **🗄**. + + 🚥 💭 🔛 ⏪ 👷 👆, 👈 💪 🥃, & 👆 🎲 🚫 💪 👉 ℹ, 💭 🆓 🚶 👫. + +🕐❔ 👆 🚮 🖼 🔘 Pydantic 🏷, ⚙️ `schema_extra` ⚖️ `Field(example="something")` 👈 🖼 🚮 **🎻 🔗** 👈 Pydantic 🏷. + +& 👈 **🎻 🔗** Pydantic 🏷 🔌 **🗄** 👆 🛠️, & ⤴️ ⚫️ ⚙️ 🩺 🎚. + +**🎻 🔗** 🚫 🤙 ✔️ 🏑 `example` 🐩. ⏮️ ⏬ 🎻 🔗 🔬 🏑 `examples`, ✋️ 🗄 3️⃣.0️⃣.3️⃣ ⚓️ 🔛 🗝 ⏬ 🎻 🔗 👈 🚫 ✔️ `examples`. + +, 🗄 3️⃣.0️⃣.3️⃣ 🔬 🚮 👍 `example` 🔀 ⏬ **🎻 🔗** ⚫️ ⚙️, 🎏 🎯 (✋️ ⚫️ 👁 `example`, 🚫 `examples`), & 👈 ⚫️❔ ⚙️ 🛠️ 🩺 🎚 (⚙️ 🦁 🎚). + +, 👐 `example` 🚫 🍕 🎻 🔗, ⚫️ 🍕 🗄 🛃 ⏬ 🎻 🔗, & 👈 ⚫️❔ 🔜 ⚙️ 🩺 🎚. + +✋️ 🕐❔ 👆 ⚙️ `example` ⚖️ `examples` ⏮️ 🙆 🎏 🚙 (`Query()`, `Body()`, ♒️.) 📚 🖼 🚫 🚮 🎻 🔗 👈 🔬 👈 💽 (🚫 🗄 👍 ⏬ 🎻 🔗), 👫 🚮 🔗 *➡ 🛠️* 📄 🗄 (🏞 🍕 🗄 👈 ⚙️ 🎻 🔗). + +`Path()`, `Query()`, `Header()`, & `Cookie()`, `example` ⚖️ `examples` 🚮 🗄 🔑, `Parameter Object` (🔧). + +& `Body()`, `File()`, & `Form()`, `example` ⚖️ `examples` 📊 🚮 🗄 🔑, `Request Body Object`, 🏑 `content`, 🔛 `Media Type Object` (🔧). + +🔛 🎏 ✋, 📤 🆕 ⏬ 🗄: **3️⃣.1️⃣.0️⃣**, ⏳ 🚀. ⚫️ ⚓️ 🔛 ⏪ 🎻 🔗 & 🏆 🛠️ ⚪️➡️ 🗄 🛃 ⏬ 🎻 🔗 ❎, 💱 ⚒ ⚪️➡️ ⏮️ ⏬ 🎻 🔗, 🌐 👫 🤪 🔺 📉. 👐, 🦁 🎚 ⏳ 🚫 🐕‍🦺 🗄 3️⃣.1️⃣.0️⃣,, 🔜, ⚫️ 👍 😣 ⚙️ 💭 🔛. diff --git a/docs/em/docs/tutorial/security/first-steps.md b/docs/em/docs/tutorial/security/first-steps.md new file mode 100644 index 0000000000..6dec6f2c34 --- /dev/null +++ b/docs/em/docs/tutorial/security/first-steps.md @@ -0,0 +1,182 @@ +# 💂‍♂ - 🥇 🔁 + +➡️ 🌈 👈 👆 ✔️ 👆 **👩‍💻** 🛠️ 🆔. + +& 👆 ✔️ **🕸** ➕1️⃣ 🆔 ⚖️ 🎏 ➡ 🎏 🆔 (⚖️ 📱 🈸). + +& 👆 💚 ✔️ 🌌 🕸 🔓 ⏮️ 👩‍💻, ⚙️ **🆔** & **🔐**. + +👥 💪 ⚙️ **Oauth2️⃣** 🏗 👈 ⏮️ **FastAPI**. + +✋️ ➡️ 🖊 👆 🕰 👂 🌕 📏 🔧 🔎 👈 🐥 🍖 ℹ 👆 💪. + +➡️ ⚙️ 🧰 🚚 **FastAPI** 🍵 💂‍♂. + +## ❔ ⚫️ 👀 + +➡️ 🥇 ⚙️ 📟 & 👀 ❔ ⚫️ 👷, & ⤴️ 👥 🔜 👟 🔙 🤔 ⚫️❔ 😥. + +## ✍ `main.py` + +📁 🖼 📁 `main.py`: + +```Python +{!../../../docs_src/security/tutorial001.py!} +``` + +## 🏃 ⚫️ + +!!! info + 🥇 ❎ `python-multipart`. + + 🤶 Ⓜ. `pip install python-multipart`. + + 👉 ↩️ **Oauth2️⃣** ⚙️ "📨 📊" 📨 `username` & `password`. + +🏃 🖼 ⏮️: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +## ✅ ⚫️ + +🚶 🎓 🩺: http://127.0.0.1:8000/docs. + +👆 🔜 👀 🕳 💖 👉: + + + +!!! check "✔ 🔼 ❗" + 👆 ⏪ ✔️ ✨ 🆕 "✔" 🔼. + + & 👆 *➡ 🛠️* ✔️ 🐥 🔒 🔝-▶️️ ↩ 👈 👆 💪 🖊. + +& 🚥 👆 🖊 ⚫️, 👆 ✔️ 🐥 ✔ 📨 🆎 `username` & `password` (& 🎏 📦 🏑): + + + +!!! note + ⚫️ 🚫 🤔 ⚫️❔ 👆 🆎 📨, ⚫️ 🏆 🚫 👷. ✋️ 👥 🔜 🤚 📤. + +👉 ↗️ 🚫 🕸 🏁 👩‍💻, ✋️ ⚫️ 👑 🏧 🧰 📄 🖥 🌐 👆 🛠️. + +⚫️ 💪 ⚙️ 🕸 🏉 (👈 💪 👆). + +⚫️ 💪 ⚙️ 🥉 🥳 🈸 & ⚙️. + +& ⚫️ 💪 ⚙️ 👆, ℹ, ✅ & 💯 🎏 🈸. + +## `password` 💧 + +🔜 ➡️ 🚶 🔙 👄 & 🤔 ⚫️❔ 🌐 👈. + +`password` "💧" 1️⃣ 🌌 ("💧") 🔬 Oauth2️⃣, 🍵 💂‍♂ & 🤝. + +Oauth2️⃣ 🔧 👈 👩‍💻 ⚖️ 🛠️ 💪 🔬 💽 👈 🔓 👩‍💻. + +✋️ 👉 💼, 🎏 **FastAPI** 🈸 🔜 🍵 🛠️ & 🤝. + +, ➡️ 📄 ⚫️ ⚪️➡️ 👈 📉 ☝ 🎑: + +* 👩‍💻 🆎 `username` & `password` 🕸, & 🎯 `Enter`. +* 🕸 (🏃‍♂ 👩‍💻 🖥) 📨 👈 `username` & `password` 🎯 📛 👆 🛠️ (📣 ⏮️ `tokenUrl="token"`). +* 🛠️ ✅ 👈 `username` & `password`, & 📨 ⏮️ "🤝" (👥 🚫 🛠️ 🙆 👉). + * "🤝" 🎻 ⏮️ 🎚 👈 👥 💪 ⚙️ ⏪ ✔ 👉 👩‍💻. + * 🛎, 🤝 ⚒ 🕛 ⏮️ 🕰. + * , 👩‍💻 🔜 ✔️ 🕹 🔄 ☝ ⏪. + * & 🚥 🤝 📎, ⚠ 🌘. ⚫️ 🚫 💖 🧲 🔑 👈 🔜 👷 ♾ (🏆 💼). +* 🕸 🏪 👈 🤝 🍕 👱. +* 👩‍💻 🖊 🕸 🚶 ➕1️⃣ 📄 🕸 🕸 📱. +* 🕸 💪 ☕ 🌅 💽 ⚪️➡️ 🛠️. + * ✋️ ⚫️ 💪 🤝 👈 🎯 🔗. + * , 🔓 ⏮️ 👆 🛠️, ⚫️ 📨 🎚 `Authorization` ⏮️ 💲 `Bearer ` ➕ 🤝. + * 🚥 🤝 🔌 `foobar`, 🎚 `Authorization` 🎚 🔜: `Bearer foobar`. + +## **FastAPI**'Ⓜ `OAuth2PasswordBearer` + +**FastAPI** 🚚 📚 🧰, 🎏 🎚 ⚛, 🛠️ 👫 💂‍♂ ⚒. + +👉 🖼 👥 🔜 ⚙️ **Oauth2️⃣**, ⏮️ **🔐** 💧, ⚙️ **📨** 🤝. 👥 👈 ⚙️ `OAuth2PasswordBearer` 🎓. + +!!! info + "📨" 🤝 🚫 🕴 🎛. + + ✋️ ⚫️ 🏆 1️⃣ 👆 ⚙️ 💼. + + & ⚫️ 💪 🏆 🏆 ⚙️ 💼, 🚥 👆 Oauth2️⃣ 🕴 & 💭 ⚫️❔ ⚫️❔ 📤 ➕1️⃣ 🎛 👈 ♣ 👻 👆 💪. + + 👈 💼, **FastAPI** 🚚 👆 ⏮️ 🧰 🏗 ⚫️. + +🕐❔ 👥 ✍ 👐 `OAuth2PasswordBearer` 🎓 👥 🚶‍♀️ `tokenUrl` 🔢. 👉 🔢 🔌 📛 👈 👩‍💻 (🕸 🏃 👩‍💻 🖥) 🔜 ⚙️ 📨 `username` & `password` ✔ 🤚 🤝. + +```Python hl_lines="6" +{!../../../docs_src/security/tutorial001.py!} +``` + +!!! tip + 📥 `tokenUrl="token"` 🔗 ⚖ 📛 `token` 👈 👥 🚫 ✍. ⚫️ ⚖ 📛, ⚫️ 🌓 `./token`. + + ↩️ 👥 ⚙️ ⚖ 📛, 🚥 👆 🛠️ 🔎 `https://example.com/`, ⤴️ ⚫️ 🔜 🔗 `https://example.com/token`. ✋️ 🚥 👆 🛠️ 🔎 `https://example.com/api/v1/`, ⤴️ ⚫️ 🔜 🔗 `https://example.com/api/v1/token`. + + ⚙️ ⚖ 📛 ⚠ ⚒ 💭 👆 🈸 🚧 👷 🏧 ⚙️ 💼 💖 [⛅ 🗳](../../advanced/behind-a-proxy.md){.internal-link target=_blank}. + +👉 🔢 🚫 ✍ 👈 🔗 / *➡ 🛠️*, ✋️ 📣 👈 📛 `/token` 🔜 1️⃣ 👈 👩‍💻 🔜 ⚙️ 🤚 🤝. 👈 ℹ ⚙️ 🗄, & ⤴️ 🎓 🛠️ 🧾 ⚙️. + +👥 🔜 🔜 ✍ ☑ ➡ 🛠️. + +!!! info + 🚥 👆 📶 ⚠ "✍" 👆 💪 👎 👗 🔢 📛 `tokenUrl` ↩️ `token_url`. + + 👈 ↩️ ⚫️ ⚙️ 🎏 📛 🗄 🔌. 👈 🚥 👆 💪 🔬 🌅 🔃 🙆 👫 💂‍♂ ⚖ 👆 💪 📁 & 📋 ⚫️ 🔎 🌖 ℹ 🔃 ⚫️. + +`oauth2_scheme` 🔢 👐 `OAuth2PasswordBearer`, ✋️ ⚫️ "🇧🇲". + +⚫️ 💪 🤙: + +```Python +oauth2_scheme(some, parameters) +``` + +, ⚫️ 💪 ⚙️ ⏮️ `Depends`. + +### ⚙️ ⚫️ + +🔜 👆 💪 🚶‍♀️ 👈 `oauth2_scheme` 🔗 ⏮️ `Depends`. + +```Python hl_lines="10" +{!../../../docs_src/security/tutorial001.py!} +``` + +👉 🔗 🔜 🚚 `str` 👈 🛠️ 🔢 `token` *➡ 🛠️ 🔢*. + +**FastAPI** 🔜 💭 👈 ⚫️ 💪 ⚙️ 👉 🔗 🔬 "💂‍♂ ⚖" 🗄 🔗 (& 🏧 🛠️ 🩺). + +!!! info "📡 ℹ" + **FastAPI** 🔜 💭 👈 ⚫️ 💪 ⚙️ 🎓 `OAuth2PasswordBearer` (📣 🔗) 🔬 💂‍♂ ⚖ 🗄 ↩️ ⚫️ 😖 ⚪️➡️ `fastapi.security.oauth2.OAuth2`, ❔ 🔄 😖 ⚪️➡️ `fastapi.security.base.SecurityBase`. + + 🌐 💂‍♂ 🚙 👈 🛠️ ⏮️ 🗄 (& 🏧 🛠️ 🩺) 😖 ⚪️➡️ `SecurityBase`, 👈 ❔ **FastAPI** 💪 💭 ❔ 🛠️ 👫 🗄. + +## ⚫️❔ ⚫️ 🔨 + +⚫️ 🔜 🚶 & 👀 📨 👈 `Authorization` 🎚, ✅ 🚥 💲 `Bearer ` ➕ 🤝, & 🔜 📨 🤝 `str`. + +🚥 ⚫️ 🚫 👀 `Authorization` 🎚, ⚖️ 💲 🚫 ✔️ `Bearer ` 🤝, ⚫️ 🔜 📨 ⏮️ 4️⃣0️⃣1️⃣ 👔 📟 ❌ (`UNAUTHORIZED`) 🔗. + +👆 🚫 ✔️ ✅ 🚥 🤝 🔀 📨 ❌. 👆 💪 💭 👈 🚥 👆 🔢 🛠️, ⚫️ 🔜 ✔️ `str` 👈 🤝. + +👆 💪 🔄 ⚫️ ⏪ 🎓 🩺: + + + +👥 🚫 ✔ 🔬 🤝, ✋️ 👈 ▶️ ⏪. + +## 🌃 + +, 3️⃣ ⚖️ 4️⃣ ➕ ⏸, 👆 ⏪ ✔️ 🐒 📨 💂‍♂. diff --git a/docs/em/docs/tutorial/security/get-current-user.md b/docs/em/docs/tutorial/security/get-current-user.md new file mode 100644 index 0000000000..455cb4f46f --- /dev/null +++ b/docs/em/docs/tutorial/security/get-current-user.md @@ -0,0 +1,151 @@ +# 🤚 ⏮️ 👩‍💻 + +⏮️ 📃 💂‍♂ ⚙️ (❔ 🧢 🔛 🔗 💉 ⚙️) 🤝 *➡ 🛠️ 🔢* `token` `str`: + +```Python hl_lines="10" +{!../../../docs_src/security/tutorial001.py!} +``` + +✋️ 👈 🚫 👈 ⚠. + +➡️ ⚒ ⚫️ 🤝 👥 ⏮️ 👩‍💻. + +## ✍ 👩‍💻 🏷 + +🥇, ➡️ ✍ Pydantic 👩‍💻 🏷. + +🎏 🌌 👥 ⚙️ Pydantic 📣 💪, 👥 💪 ⚙️ ⚫️ 🙆 🙆: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="5 12-16" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="3 10-14" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +## ✍ `get_current_user` 🔗 + +➡️ ✍ 🔗 `get_current_user`. + +💭 👈 🔗 💪 ✔️ 🎧-🔗 ❓ + +`get_current_user` 🔜 ✔️ 🔗 ⏮️ 🎏 `oauth2_scheme` 👥 ✍ ⏭. + +🎏 👥 🔨 ⏭ *➡ 🛠️* 🔗, 👆 🆕 🔗 `get_current_user` 🔜 📨 `token` `str` ⚪️➡️ 🎧-🔗 `oauth2_scheme`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="23" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +## 🤚 👩‍💻 + +`get_current_user` 🔜 ⚙️ (❌) 🚙 🔢 👥 ✍, 👈 ✊ 🤝 `str` & 📨 👆 Pydantic `User` 🏷: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17-20 24-25" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +## 💉 ⏮️ 👩‍💻 + +🔜 👥 💪 ⚙️ 🎏 `Depends` ⏮️ 👆 `get_current_user` *➡ 🛠️*: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="29" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +👀 👈 👥 📣 🆎 `current_user` Pydantic 🏷 `User`. + +👉 🔜 ℹ 🇺🇲 🔘 🔢 ⏮️ 🌐 🛠️ & 🆎 ✅. + +!!! tip + 👆 5️⃣📆 💭 👈 📨 💪 📣 ⏮️ Pydantic 🏷. + + 📥 **FastAPI** 🏆 🚫 🤚 😨 ↩️ 👆 ⚙️ `Depends`. + +!!! check + 🌌 👉 🔗 ⚙️ 🏗 ✔ 👥 ✔️ 🎏 🔗 (🎏 "☑") 👈 🌐 📨 `User` 🏷. + + 👥 🚫 🚫 ✔️ 🕴 1️⃣ 🔗 👈 💪 📨 👈 🆎 💽. + +## 🎏 🏷 + +👆 💪 🔜 🤚 ⏮️ 👩‍💻 🔗 *➡ 🛠️ 🔢* & 🙅 ⏮️ 💂‍♂ 🛠️ **🔗 💉** 🎚, ⚙️ `Depends`. + +& 👆 💪 ⚙️ 🙆 🏷 ⚖️ 💽 💂‍♂ 📄 (👉 💼, Pydantic 🏷 `User`). + +✋️ 👆 🚫 🚫 ⚙️ 🎯 💽 🏷, 🎓 ⚖️ 🆎. + +👆 💚 ✔️ `id` & `email` & 🚫 ✔️ 🙆 `username` 👆 🏷 ❓ 💭. 👆 💪 ⚙️ 👉 🎏 🧰. + +👆 💚 ✔️ `str`❓ ⚖️ `dict`❓ ⚖️ 💽 🎓 🏷 👐 🔗 ❓ ⚫️ 🌐 👷 🎏 🌌. + +👆 🤙 🚫 ✔️ 👩‍💻 👈 🕹 👆 🈸 ✋️ 🤖, 🤖, ⚖️ 🎏 ⚙️, 👈 ✔️ 🔐 🤝 ❓ 🔄, ⚫️ 🌐 👷 🎏. + +⚙️ 🙆 😇 🏷, 🙆 😇 🎓, 🙆 😇 💽 👈 👆 💪 👆 🈸. **FastAPI** ✔️ 👆 📔 ⏮️ 🔗 💉 ⚙️. + +## 📟 📐 + +👉 🖼 5️⃣📆 😑 🔁. ✔️ 🤯 👈 👥 🌀 💂‍♂, 📊 🏷, 🚙 🔢 & *➡ 🛠️* 🎏 📁. + +✋️ 📥 🔑 ☝. + +💂‍♂ & 🔗 💉 💩 ✍ 🕐. + +& 👆 💪 ⚒ ⚫️ 🏗 👆 💚. & , ✔️ ⚫️ ✍ 🕴 🕐, 👁 🥉. ⏮️ 🌐 💪. + +✋️ 👆 💪 ✔️ 💯 🔗 (*➡ 🛠️*) ⚙️ 🎏 💂‍♂ ⚙️. + +& 🌐 👫 (⚖️ 🙆 ↔ 👫 👈 👆 💚) 💪 ✊ 📈 🏤-⚙️ 👫 🔗 ⚖️ 🙆 🎏 🔗 👆 ✍. + +& 🌐 👉 💯 *➡ 🛠️* 💪 🤪 3️⃣ ⏸: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="28-30" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +## 🌃 + +👆 💪 🔜 🤚 ⏮️ 👩‍💻 🔗 👆 *➡ 🛠️ 🔢*. + +👥 ⏪ 😬 📤. + +👥 💪 🚮 *➡ 🛠️* 👩‍💻/👩‍💻 🤙 📨 `username` & `password`. + +👈 👟 ⏭. diff --git a/docs/em/docs/tutorial/security/index.md b/docs/em/docs/tutorial/security/index.md new file mode 100644 index 0000000000..d76f7203fe --- /dev/null +++ b/docs/em/docs/tutorial/security/index.md @@ -0,0 +1,101 @@ +# 💂‍♂ + +📤 📚 🌌 🍵 💂‍♂, 🤝 & ✔. + +& ⚫️ 🛎 🏗 & "⚠" ❔. + +📚 🛠️ & ⚙️ 🍵 💂‍♂ & 🤝 ✊ 🦏 💸 🎯 & 📟 (📚 💼 ⚫️ 💪 5️⃣0️⃣ 💯 ⚖️ 🌅 🌐 📟 ✍). + +**FastAPI** 🚚 📚 🧰 ℹ 👆 🙅 ⏮️ **💂‍♂** 💪, 📉, 🐩 🌌, 🍵 ✔️ 🔬 & 💡 🌐 💂‍♂ 🔧. + +✋️ 🥇, ➡️ ✅ 🤪 🔧. + +## 🏃 ❓ + +🚥 👆 🚫 💅 🔃 🙆 👉 ⚖ & 👆 💪 🚮 💂‍♂ ⏮️ 🤝 ⚓️ 🔛 🆔 & 🔐 *▶️️ 🔜*, 🚶 ⏭ 📃. + +## Oauth2️⃣ + +Oauth2️⃣ 🔧 👈 🔬 📚 🌌 🍵 🤝 & ✔. + +⚫️ 🔬 🔧 & 📔 📚 🏗 ⚙️ 💼. + +⚫️ 🔌 🌌 🔓 ⚙️ "🥉 🥳". + +👈 ⚫️❔ 🌐 ⚙️ ⏮️ "💳 ⏮️ 👱📔, 🇺🇸🔍, 👱📔, 📂" ⚙️ 🔘. + +### ✳ 1️⃣ + +📤 ✳ 1️⃣, ❔ 📶 🎏 ⚪️➡️ Oauth2️⃣, & 🌖 🏗, ⚫️ 🔌 🔗 🔧 🔛 ❔ 🗜 📻. + +⚫️ 🚫 📶 🌟 ⚖️ ⚙️ 🛎. + +Oauth2️⃣ 🚫 ✔ ❔ 🗜 📻, ⚫️ ⌛ 👆 ✔️ 👆 🈸 🍦 ⏮️ 🇺🇸🔍. + +!!! tip + 📄 🔃 **🛠️** 👆 🔜 👀 ❔ ⚒ 🆙 🇺🇸🔍 🆓, ⚙️ Traefik & ➡️ 🗜. + + +## 👩‍💻 🔗 + +👩‍💻 🔗 ➕1️⃣ 🔧, 🧢 🔛 **Oauth2️⃣**. + +⚫️ ↔ Oauth2️⃣ ✔ 👜 👈 📶 🌌 Oauth2️⃣, 🔄 ⚒ ⚫️ 🌅 🛠️. + +🖼, 🇺🇸🔍 💳 ⚙️ 👩‍💻 🔗 (❔ 🔘 ⚙️ Oauth2️⃣). + +✋️ 👱📔 💳 🚫 🐕‍🦺 👩‍💻 🔗. ⚫️ ✔️ 🚮 👍 🍛 Oauth2️⃣. + +### 👩‍💻 (🚫 "👩‍💻 🔗") + +📤 "👩‍💻" 🔧. 👈 🔄 ❎ 🎏 👜 **👩‍💻 🔗**, ✋️ 🚫 ⚓️ 🔛 Oauth2️⃣. + +, ⚫️ 🏁 🌖 ⚙️. + +⚫️ 🚫 📶 🌟 ⚖️ ⚙️ 🛎. + +## 🗄 + +🗄 (⏪ 💭 🦁) 📂 🔧 🏗 🔗 (🔜 🍕 💾 🏛). + +**FastAPI** ⚓️ 🔛 **🗄**. + +👈 ⚫️❔ ⚒ ⚫️ 💪 ✔️ 💗 🏧 🎓 🧾 🔢, 📟 ⚡, ♒️. + +🗄 ✔️ 🌌 🔬 💗 💂‍♂ "⚖". + +⚙️ 👫, 👆 💪 ✊ 📈 🌐 👫 🐩-⚓️ 🧰, 🔌 👉 🎓 🧾 ⚙️. + +🗄 🔬 📄 💂‍♂ ⚖: + +* `apiKey`: 🈸 🎯 🔑 👈 💪 👟 ⚪️➡️: + * 🔢 🔢. + * 🎚. + * 🍪. +* `http`: 🐩 🇺🇸🔍 🤝 ⚙️, 🔌: + * `bearer`: 🎚 `Authorization` ⏮️ 💲 `Bearer ` ➕ 🤝. 👉 😖 ⚪️➡️ Oauth2️⃣. + * 🇺🇸🔍 🔰 🤝. + * 🇺🇸🔍 📰, ♒️. +* `oauth2`: 🌐 Oauth2️⃣ 🌌 🍵 💂‍♂ (🤙 "💧"). + * 📚 👫 💧 ☑ 🏗 ✳ 2️⃣.0️⃣ 🤝 🐕‍🦺 (💖 🇺🇸🔍, 👱📔, 👱📔, 📂, ♒️): + * `implicit` + * `clientCredentials` + * `authorizationCode` + * ✋️ 📤 1️⃣ 🎯 "💧" 👈 💪 👌 ⚙️ 🚚 🤝 🎏 🈸 🔗: + * `password`: ⏭ 📃 🔜 📔 🖼 👉. +* `openIdConnect`: ✔️ 🌌 🔬 ❔ 🔎 Oauth2️⃣ 🤝 📊 🔁. + * 👉 🏧 🔍 ⚫️❔ 🔬 👩‍💻 🔗 🔧. + + +!!! tip + 🛠️ 🎏 🤝/✔ 🐕‍🦺 💖 🇺🇸🔍, 👱📔, 👱📔, 📂, ♒️. 💪 & 📶 ⏩. + + 🌅 🏗 ⚠ 🏗 🤝/✔ 🐕‍🦺 💖 👈, ✋️ **FastAPI** 🤝 👆 🧰 ⚫️ 💪, ⏪ 🔨 🏋️ 🏋‍♂ 👆. + +## **FastAPI** 🚙 + +FastAPI 🚚 📚 🧰 🔠 👉 💂‍♂ ⚖ `fastapi.security` 🕹 👈 📉 ⚙️ 👉 💂‍♂ 🛠️. + +⏭ 📃 👆 🔜 👀 ❔ 🚮 💂‍♂ 👆 🛠️ ⚙️ 📚 🧰 🚚 **FastAPI**. + +& 👆 🔜 👀 ❔ ⚫️ 🤚 🔁 🛠️ 🔘 🎓 🧾 ⚙️. diff --git a/docs/em/docs/tutorial/security/oauth2-jwt.md b/docs/em/docs/tutorial/security/oauth2-jwt.md new file mode 100644 index 0000000000..bc3c943f86 --- /dev/null +++ b/docs/em/docs/tutorial/security/oauth2-jwt.md @@ -0,0 +1,297 @@ +# Oauth2️⃣ ⏮️ 🔐 (& 🔁), 📨 ⏮️ 🥙 🤝 + +🔜 👈 👥 ✔️ 🌐 💂‍♂ 💧, ➡️ ⚒ 🈸 🤙 🔐, ⚙️ 🥙 🤝 & 🔐 🔐 🔁. + +👉 📟 🕳 👆 💪 🤙 ⚙️ 👆 🈸, 🖊 🔐 #️⃣ 👆 💽, ♒️. + +👥 🔜 ▶️ ⚪️➡️ 🌐❔ 👥 ◀️ ⏮️ 📃 & 📈 ⚫️. + +## 🔃 🥙 + +🥙 ⛓ "🎻 🕸 🤝". + +⚫️ 🐩 🚫 🎻 🎚 📏 💧 🎻 🍵 🚀. ⚫️ 👀 💖 👉: + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c +``` + +⚫️ 🚫 🗜,, 🙆 💪 🛡 ℹ ⚪️➡️ 🎚. + +✋️ ⚫️ 🛑. , 🕐❔ 👆 📨 🤝 👈 👆 ♨, 👆 💪 ✔ 👈 👆 🤙 ♨ ⚫️. + +👈 🌌, 👆 💪 ✍ 🤝 ⏮️ 👔, ➡️ 💬, 1️⃣ 🗓️. & ⤴️ 🕐❔ 👩‍💻 👟 🔙 ⏭ 📆 ⏮️ 🤝, 👆 💭 👈 👩‍💻 🕹 👆 ⚙️. + +⏮️ 🗓️, 🤝 🔜 🕛 & 👩‍💻 🔜 🚫 ✔ & 🔜 ✔️ 🛑 🔄 🤚 🆕 🤝. & 🚥 👩‍💻 (⚖️ 🥉 🥳) 🔄 🔀 🤝 🔀 👔, 👆 🔜 💪 🔎 ⚫️, ↩️ 💳 🔜 🚫 🏏. + +🚥 👆 💚 🤾 ⏮️ 🥙 🤝 & 👀 ❔ 👫 👷, ✅ https://jwt.io. + +## ❎ `python-jose` + +👥 💪 ❎ `python-jose` 🏗 & ✔ 🥙 🤝 🐍: + +
+ +```console +$ pip install "python-jose[cryptography]" + +---> 100% +``` + +
+ +🐍-🇩🇬 🚚 🔐 👩‍💻 ➕. + +📥 👥 ⚙️ 👍 1️⃣: )/⚛. + +!!! tip + 👉 🔰 ⏪ ⚙️ PyJWT. + + ✋️ ⚫️ ℹ ⚙️ 🐍-🇩🇬 ↩️ ⚫️ 🚚 🌐 ⚒ ⚪️➡️ PyJWT ➕ ➕ 👈 👆 💪 💪 ⏪ 🕐❔ 🏗 🛠️ ⏮️ 🎏 🧰. + +## 🔐 🔁 + +"🔁" ⛓ 🏭 🎚 (🔐 👉 💼) 🔘 🔁 🔢 (🎻) 👈 👀 💖 🙃. + +🕐❔ 👆 🚶‍♀️ ⚫️❔ 🎏 🎚 (⚫️❔ 🎏 🔐) 👆 🤚 ⚫️❔ 🎏 🙃. + +✋️ 👆 🚫🔜 🗜 ⚪️➡️ 🙃 🔙 🔐. + +### ⚫️❔ ⚙️ 🔐 🔁 + +🚥 👆 💽 📎, 🧙‍♀ 🏆 🚫 ✔️ 👆 👩‍💻' 🔢 🔐, 🕴#️⃣. + +, 🧙‍♀ 🏆 🚫 💪 🔄 ⚙️ 👈 🔐 ➕1️⃣ ⚙️ (📚 👩‍💻 ⚙️ 🎏 🔐 🌐, 👉 🔜 ⚠). + +## ❎ `passlib` + +🇸🇲 👑 🐍 📦 🍵 🔐#️⃣. + +⚫️ 🐕‍🦺 📚 🔐 🔁 📊 & 🚙 👷 ⏮️ 👫. + +👍 📊 "🐡". + +, ❎ 🇸🇲 ⏮️ 🐡: + +
+ +```console +$ pip install "passlib[bcrypt]" + +---> 100% +``` + +
+ +!!! tip + ⏮️ `passlib`, 👆 💪 🔗 ⚫️ 💪 ✍ 🔐 ✍ **✳**, **🏺** 💂‍♂ 🔌-⚖️ 📚 🎏. + + , 👆 🔜 💪, 🖼, 💰 🎏 📊 ⚪️➡️ ✳ 🈸 💽 ⏮️ FastAPI 🈸. ⚖️ 📉 ↔ ✳ 🈸 ⚙️ 🎏 💽. + + & 👆 👩‍💻 🔜 💪 💳 ⚪️➡️ 👆 ✳ 📱 ⚖️ ⚪️➡️ 👆 **FastAPI** 📱, 🎏 🕰. + +## #️⃣ & ✔ 🔐 + +🗄 🧰 👥 💪 ⚪️➡️ `passlib`. + +✍ 🇸🇲 "🔑". 👉 ⚫️❔ 🔜 ⚙️ #️⃣ & ✔ 🔐. + +!!! tip + 🇸🇲 🔑 ✔️ 🛠️ ⚙️ 🎏 🔁 📊, 🔌 😢 🗝 🕐 🕴 ✔ ✔ 👫, ♒️. + + 🖼, 👆 💪 ⚙️ ⚫️ ✍ & ✔ 🔐 🏗 ➕1️⃣ ⚙️ (💖 ✳) ✋️ #️⃣ 🙆 🆕 🔐 ⏮️ 🎏 📊 💖 🐡. + + & 🔗 ⏮️ 🌐 👫 🎏 🕰. + +✍ 🚙 🔢 #️⃣ 🔐 👟 ⚪️➡️ 👩‍💻. + +& ➕1️⃣ 🚙 ✔ 🚥 📨 🔐 🏏 #️⃣ 🏪. + +& ➕1️⃣ 1️⃣ 🔓 & 📨 👩‍💻. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="7 48 55-56 59-60 69-75" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="6 47 54-55 58-59 68-74" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` + +!!! note + 🚥 👆 ✅ 🆕 (❌) 💽 `fake_users_db`, 👆 🔜 👀 ❔ #️⃣ 🔐 👀 💖 🔜: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. + +## 🍵 🥙 🤝 + +🗄 🕹 ❎. + +✍ 🎲 ㊙ 🔑 👈 🔜 ⚙️ 🛑 🥙 🤝. + +🏗 🔐 🎲 ㊙ 🔑 ⚙️ 📋: + +
+ +```console +$ openssl rand -hex 32 + +09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7 +``` + +
+ +& 📁 🔢 🔢 `SECRET_KEY` (🚫 ⚙️ 1️⃣ 🖼). + +✍ 🔢 `ALGORITHM` ⏮️ 📊 ⚙️ 🛑 🥙 🤝 & ⚒ ⚫️ `"HS256"`. + +✍ 🔢 👔 🤝. + +🔬 Pydantic 🏷 👈 🔜 ⚙️ 🤝 🔗 📨. + +✍ 🚙 🔢 🏗 🆕 🔐 🤝. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="6 12-14 28-30 78-86" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="5 11-13 27-29 77-85" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` + +## ℹ 🔗 + +ℹ `get_current_user` 📨 🎏 🤝 ⏭, ✋️ 👉 🕰, ⚙️ 🥙 🤝. + +🔣 📨 🤝, ✔ ⚫️, & 📨 ⏮️ 👩‍💻. + +🚥 🤝 ❌, 📨 🇺🇸🔍 ❌ ▶️️ ↖️. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="89-106" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="88-105" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` + +## ℹ `/token` *➡ 🛠️* + +✍ `timedelta` ⏮️ 👔 🕰 🤝. + +✍ 🎰 🥙 🔐 🤝 & 📨 ⚫️. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="115-130" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="114-129" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` + +### 📡 ℹ 🔃 🥙 "📄" `sub` + +🥙 🔧 💬 👈 📤 🔑 `sub`, ⏮️ 📄 🤝. + +⚫️ 📦 ⚙️ ⚫️, ✋️ 👈 🌐❔ 👆 🔜 🚮 👩‍💻 🆔, 👥 ⚙️ ⚫️ 📥. + +🥙 5️⃣📆 ⚙️ 🎏 👜 ↖️ ⚪️➡️ ⚖ 👩‍💻 & 🤝 👫 🎭 🛠️ 🔗 🔛 👆 🛠️. + +🖼, 👆 💪 🔬 "🚘" ⚖️ "📰 🏤". + +⤴️ 👆 💪 🚮 ✔ 🔃 👈 👨‍💼, 💖 "💾" (🚘) ⚖️ "✍" (📰). + +& ⤴️, 👆 💪 🤝 👈 🥙 🤝 👩‍💻 (⚖️ 🤖), & 👫 💪 ⚙️ ⚫️ 🎭 👈 🎯 (💾 🚘, ⚖️ ✍ 📰 🏤) 🍵 💆‍♂ ✔️ 🏧, ⏮️ 🥙 🤝 👆 🛠️ 🏗 👈. + +⚙️ 👫 💭, 🥙 💪 ⚙️ 🌌 🌖 🤓 😐. + +📚 💼, 📚 👈 👨‍💼 💪 ✔️ 🎏 🆔, ➡️ 💬 `foo` (👩‍💻 `foo`, 🚘 `foo`, & 📰 🏤 `foo`). + +, ❎ 🆔 💥, 🕐❔ 🏗 🥙 🤝 👩‍💻, 👆 💪 🔡 💲 `sub` 🔑, ✅ ⏮️ `username:`. , 👉 🖼, 💲 `sub` 💪 ✔️: `username:johndoe`. + +⚠ 👜 ✔️ 🤯 👈 `sub` 🔑 🔜 ✔️ 😍 🆔 🤭 🎂 🈸, & ⚫️ 🔜 🎻. + +## ✅ ⚫️ + +🏃 💽 & 🚶 🩺: http://127.0.0.1:8000/docs. + +👆 🔜 👀 👩‍💻 🔢 💖: + + + +✔ 🈸 🎏 🌌 ⏭. + +⚙️ 🎓: + +🆔: `johndoe` +🔐: `secret` + +!!! check + 👀 👈 🕳 📟 🔢 🔐 "`secret`", 👥 🕴 ✔️ #️⃣ ⏬. + + + +🤙 🔗 `/users/me/`, 👆 🔜 🤚 📨: + +```JSON +{ + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + "disabled": false +} +``` + + + +🚥 👆 📂 👩‍💻 🧰, 👆 💪 👀 ❔ 📊 📨 🕴 🔌 🤝, 🔐 🕴 📨 🥇 📨 🔓 👩‍💻 & 🤚 👈 🔐 🤝, ✋️ 🚫 ⏮️: + + + +!!! note + 👀 🎚 `Authorization`, ⏮️ 💲 👈 ▶️ ⏮️ `Bearer `. + +## 🏧 ⚙️ ⏮️ `scopes` + +Oauth2️⃣ ✔️ 🔑 "↔". + +👆 💪 ⚙️ 👫 🚮 🎯 ⚒ ✔ 🥙 🤝. + +⤴️ 👆 💪 🤝 👉 🤝 👩‍💻 🔗 ⚖️ 🥉 🥳, 🔗 ⏮️ 👆 🛠️ ⏮️ ⚒ 🚫. + +👆 💪 💡 ❔ ⚙️ 👫 & ❔ 👫 🛠️ 🔘 **FastAPI** ⏪ **🏧 👩‍💻 🦮**. + +## 🌃 + +⏮️ ⚫️❔ 👆 ✔️ 👀 🆙 🔜, 👆 💪 ⚒ 🆙 🔐 **FastAPI** 🈸 ⚙️ 🐩 💖 Oauth2️⃣ & 🥙. + +🌖 🙆 🛠️ 🚚 💂‍♂ ▶️️ 👍 🏗 📄 🔜. + +📚 📦 👈 📉 ⚫️ 📚 ✔️ ⚒ 📚 ⚠ ⏮️ 💽 🏷, 💽, & 💪 ⚒. & 👉 📦 👈 📉 👜 💁‍♂️ 🌅 🤙 ✔️ 💂‍♂ ⚠ 🔘. + +--- + +**FastAPI** 🚫 ⚒ 🙆 ⚠ ⏮️ 🙆 💽, 💽 🏷 ⚖️ 🧰. + +⚫️ 🤝 👆 🌐 💪 ⚒ 🕐 👈 👖 👆 🏗 🏆. + +& 👆 💪 ⚙️ 🔗 📚 👍 🚧 & 🛎 ⚙️ 📦 💖 `passlib` & `python-jose`, ↩️ **FastAPI** 🚫 🚚 🙆 🏗 🛠️ 🛠️ 🔢 📦. + +✋️ ⚫️ 🚚 👆 🧰 📉 🛠️ 🌅 💪 🍵 🎯 💪, ⚖, ⚖️ 💂‍♂. + +& 👆 💪 ⚙️ & 🛠️ 🔐, 🐩 🛠️, 💖 Oauth2️⃣ 📶 🙅 🌌. + +👆 💪 💡 🌅 **🏧 👩‍💻 🦮** 🔃 ❔ ⚙️ Oauth2️⃣ "↔", 🌖 👌-🧽 ✔ ⚙️, 📄 👫 🎏 🐩. Oauth2️⃣ ⏮️ ↔ 🛠️ ⚙️ 📚 🦏 🤝 🐕‍🦺, 💖 👱📔, 🇺🇸🔍, 📂, 🤸‍♂, 👱📔, ♒️. ✔ 🥉 🥳 🈸 🔗 ⏮️ 👫 🔗 🔛 👨‍💼 👫 👩‍💻. diff --git a/docs/em/docs/tutorial/security/simple-oauth2.md b/docs/em/docs/tutorial/security/simple-oauth2.md new file mode 100644 index 0000000000..765d940394 --- /dev/null +++ b/docs/em/docs/tutorial/security/simple-oauth2.md @@ -0,0 +1,315 @@ +# 🙅 Oauth2️⃣ ⏮️ 🔐 & 📨 + +🔜 ➡️ 🏗 ⚪️➡️ ⏮️ 📃 & 🚮 ❌ 🍕 ✔️ 🏁 💂‍♂ 💧. + +## 🤚 `username` & `password` + +👥 🔜 ⚙️ **FastAPI** 💂‍♂ 🚙 🤚 `username` & `password`. + +Oauth2️⃣ ✔ 👈 🕐❔ ⚙️ "🔐 💧" (👈 👥 ⚙️) 👩‍💻/👩‍💻 🔜 📨 `username` & `password` 🏑 📨 💽. + +& 🔌 💬 👈 🏑 ✔️ 🌟 💖 👈. `user-name` ⚖️ `email` 🚫🔜 👷. + +✋️ 🚫 😟, 👆 💪 🎦 ⚫️ 👆 🎋 👆 🏁 👩‍💻 🕸. + +& 👆 💽 🏷 💪 ⚙️ 🙆 🎏 📛 👆 💚. + +✋️ 💳 *➡ 🛠️*, 👥 💪 ⚙️ 👉 📛 🔗 ⏮️ 🔌 (& 💪, 🖼, ⚙️ 🛠️ 🛠️ 🧾 ⚙️). + +🔌 🇵🇸 👈 `username` & `password` 🔜 📨 📨 💽 (, 🙅‍♂ 🎻 📥). + +### `scope` + +🔌 💬 👈 👩‍💻 💪 📨 ➕1️⃣ 📨 🏑 "`scope`". + +📨 🏑 📛 `scope` (⭐), ✋️ ⚫️ 🤙 📏 🎻 ⏮️ "↔" 🎏 🚀. + +🔠 "↔" 🎻 (🍵 🚀). + +👫 🛎 ⚙️ 📣 🎯 💂‍♂ ✔, 🖼: + +* `users:read` ⚖️ `users:write` ⚠ 🖼. +* `instagram_basic` ⚙️ 👱📔 / 👱📔. +* `https://www.googleapis.com/auth/drive` ⚙️ 🇺🇸🔍. + +!!! info + Oauth2️⃣ "↔" 🎻 👈 📣 🎯 ✔ ✔. + + ⚫️ 🚫 🤔 🚥 ⚫️ ✔️ 🎏 🦹 💖 `:` ⚖️ 🚥 ⚫️ 📛. + + 👈 ℹ 🛠️ 🎯. + + Oauth2️⃣ 👫 🎻. + +## 📟 🤚 `username` & `password` + +🔜 ➡️ ⚙️ 🚙 🚚 **FastAPI** 🍵 👉. + +### `OAuth2PasswordRequestForm` + +🥇, 🗄 `OAuth2PasswordRequestForm`, & ⚙️ ⚫️ 🔗 ⏮️ `Depends` *➡ 🛠️* `/token`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="4 76" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="2 74" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +`OAuth2PasswordRequestForm` 🎓 🔗 👈 📣 📨 💪 ⏮️: + +* `username`. +* `password`. +* 📦 `scope` 🏑 🦏 🎻, ✍ 🎻 🎏 🚀. +* 📦 `grant_type`. + +!!! tip + Oauth2️⃣ 🔌 🤙 *🚚* 🏑 `grant_type` ⏮️ 🔧 💲 `password`, ✋️ `OAuth2PasswordRequestForm` 🚫 🛠️ ⚫️. + + 🚥 👆 💪 🛠️ ⚫️, ⚙️ `OAuth2PasswordRequestFormStrict` ↩️ `OAuth2PasswordRequestForm`. + +* 📦 `client_id` (👥 🚫 💪 ⚫️ 👆 🖼). +* 📦 `client_secret` (👥 🚫 💪 ⚫️ 👆 🖼). + +!!! info + `OAuth2PasswordRequestForm` 🚫 🎁 🎓 **FastAPI** `OAuth2PasswordBearer`. + + `OAuth2PasswordBearer` ⚒ **FastAPI** 💭 👈 ⚫️ 💂‍♂ ⚖. ⚫️ 🚮 👈 🌌 🗄. + + ✋️ `OAuth2PasswordRequestForm` 🎓 🔗 👈 👆 💪 ✔️ ✍ 👆, ⚖️ 👆 💪 ✔️ 📣 `Form` 🔢 🔗. + + ✋️ ⚫️ ⚠ ⚙️ 💼, ⚫️ 🚚 **FastAPI** 🔗, ⚒ ⚫️ ⏩. + +### ⚙️ 📨 💽 + +!!! tip + 👐 🔗 🎓 `OAuth2PasswordRequestForm` 🏆 🚫 ✔️ 🔢 `scope` ⏮️ 📏 🎻 👽 🚀, ↩️, ⚫️ 🔜 ✔️ `scopes` 🔢 ⏮️ ☑ 📇 🎻 🔠 ↔ 📨. + + 👥 🚫 ⚙️ `scopes` 👉 🖼, ✋️ 🛠️ 📤 🚥 👆 💪 ⚫️. + +🔜, 🤚 👩‍💻 📊 ⚪️➡️ (❌) 💽, ⚙️ `username` ⚪️➡️ 📨 🏑. + +🚥 📤 🙅‍♂ ✅ 👩‍💻, 👥 📨 ❌ 💬 "❌ 🆔 ⚖️ 🔐". + +❌, 👥 ⚙️ ⚠ `HTTPException`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3 77-79" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1 75-77" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +### ✅ 🔐 + +👉 ☝ 👥 ✔️ 👩‍💻 📊 ⚪️➡️ 👆 💽, ✋️ 👥 🚫 ✅ 🔐. + +➡️ 🚮 👈 💽 Pydantic `UserInDB` 🏷 🥇. + +👆 🔜 🙅 🖊 🔢 🔐,, 👥 🔜 ⚙️ (❌) 🔐 🔁 ⚙️. + +🚥 🔐 🚫 🏏, 👥 📨 🎏 ❌. + +#### 🔐 🔁 + +"🔁" ⛓: 🏭 🎚 (🔐 👉 💼) 🔘 🔁 🔢 (🎻) 👈 👀 💖 🙃. + +🕐❔ 👆 🚶‍♀️ ⚫️❔ 🎏 🎚 (⚫️❔ 🎏 🔐) 👆 🤚 ⚫️❔ 🎏 🙃. + +✋️ 👆 🚫🔜 🗜 ⚪️➡️ 🙃 🔙 🔐. + +##### ⚫️❔ ⚙️ 🔐 🔁 + +🚥 👆 💽 📎, 🧙‍♀ 🏆 🚫 ✔️ 👆 👩‍💻' 🔢 🔐, 🕴#️⃣. + +, 🧙‍♀ 🏆 🚫 💪 🔄 ⚙️ 👈 🎏 🔐 ➕1️⃣ ⚙️ (📚 👩‍💻 ⚙️ 🎏 🔐 🌐, 👉 🔜 ⚠). + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="80-83" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="78-81" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +#### 🔃 `**user_dict` + +`UserInDB(**user_dict)` ⛓: + +*🚶‍♀️ 🔑 & 💲 `user_dict` 🔗 🔑-💲 ❌, 🌓:* + +```Python +UserInDB( + username = user_dict["username"], + email = user_dict["email"], + full_name = user_dict["full_name"], + disabled = user_dict["disabled"], + hashed_password = user_dict["hashed_password"], +) +``` + +!!! info + 🌅 🏁 🔑 `**👩‍💻_ #️⃣ ` ✅ 🔙 [🧾 **➕ 🏷**](../extra-models.md#about-user_indict){.internal-link target=_blank}. + +## 📨 🤝 + +📨 `token` 🔗 🔜 🎻 🎚. + +⚫️ 🔜 ✔️ `token_type`. 👆 💼, 👥 ⚙️ "📨" 🤝, 🤝 🆎 🔜 "`bearer`". + +& ⚫️ 🔜 ✔️ `access_token`, ⏮️ 🎻 ⚗ 👆 🔐 🤝. + +👉 🙅 🖼, 👥 🔜 🍕 😟 & 📨 🎏 `username` 🤝. + +!!! tip + ⏭ 📃, 👆 🔜 👀 🎰 🔐 🛠️, ⏮️ 🔐 #️⃣ & 🥙 🤝. + + ✋️ 🔜, ➡️ 🎯 🔛 🎯 ℹ 👥 💪. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="85" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="83" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +!!! tip + 🔌, 👆 🔜 📨 🎻 ⏮️ `access_token` & `token_type`, 🎏 👉 🖼. + + 👉 🕳 👈 👆 ✔️ 👆 👆 📟, & ⚒ 💭 👆 ⚙️ 📚 🎻 🔑. + + ⚫️ 🌖 🕴 👜 👈 👆 ✔️ 💭 ☑ 👆, 🛠️ ⏮️ 🔧. + + 🎂, **FastAPI** 🍵 ⚫️ 👆. + +## ℹ 🔗 + +🔜 👥 🔜 ℹ 👆 🔗. + +👥 💚 🤚 `current_user` *🕴* 🚥 👉 👩‍💻 🦁. + +, 👥 ✍ 🌖 🔗 `get_current_active_user` 👈 🔄 ⚙️ `get_current_user` 🔗. + +👯‍♂️ 👉 🔗 🔜 📨 🇺🇸🔍 ❌ 🚥 👩‍💻 🚫 🔀, ⚖️ 🚥 🔕. + +, 👆 🔗, 👥 🔜 🕴 🤚 👩‍💻 🚥 👩‍💻 🔀, ☑ 🔓, & 🦁: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="58-66 69-72 90" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="55-64 67-70 88" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +!!! info + 🌖 🎚 `WWW-Authenticate` ⏮️ 💲 `Bearer` 👥 🛬 📥 🍕 🔌. + + 🙆 🇺🇸🔍 (❌) 👔 📟 4️⃣0️⃣1️⃣ "⛔" 🤔 📨 `WWW-Authenticate` 🎚. + + 💼 📨 🤝 (👆 💼), 💲 👈 🎚 🔜 `Bearer`. + + 👆 💪 🤙 🚶 👈 ➕ 🎚 & ⚫️ 🔜 👷. + + ✋️ ⚫️ 🚚 📥 🛠️ ⏮️ 🔧. + + , 📤 5️⃣📆 🧰 👈 ⌛ & ⚙️ ⚫️ (🔜 ⚖️ 🔮) & 👈 💪 ⚠ 👆 ⚖️ 👆 👩‍💻, 🔜 ⚖️ 🔮. + + 👈 💰 🐩... + +## 👀 ⚫️ 🎯 + +📂 🎓 🩺: http://127.0.0.1:8000/docs. + +### 🔓 + +🖊 "✔" 🔼. + +⚙️ 🎓: + +👩‍💻: `johndoe` + +🔐: `secret` + + + +⏮️ 🔗 ⚙️, 👆 🔜 👀 ⚫️ 💖: + + + +### 🤚 👆 👍 👩‍💻 💽 + +🔜 ⚙️ 🛠️ `GET` ⏮️ ➡ `/users/me`. + +👆 🔜 🤚 👆 👩‍💻 📊, 💖: + +```JSON +{ + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + "disabled": false, + "hashed_password": "fakehashedsecret" +} +``` + + + +🚥 👆 🖊 🔒 ℹ & ⏏, & ⤴️ 🔄 🎏 🛠️ 🔄, 👆 🔜 🤚 🇺🇸🔍 4️⃣0️⃣1️⃣ ❌: + +```JSON +{ + "detail": "Not authenticated" +} +``` + +### 🔕 👩‍💻 + +🔜 🔄 ⏮️ 🔕 👩‍💻, 🔓 ⏮️: + +👩‍💻: `alice` + +🔐: `secret2` + +& 🔄 ⚙️ 🛠️ `GET` ⏮️ ➡ `/users/me`. + +👆 🔜 🤚 "🔕 👩‍💻" ❌, 💖: + +```JSON +{ + "detail": "Inactive user" +} +``` + +## 🌃 + +👆 🔜 ✔️ 🧰 🛠️ 🏁 💂‍♂ ⚙️ ⚓️ 🔛 `username` & `password` 👆 🛠️. + +⚙️ 👫 🧰, 👆 💪 ⚒ 💂‍♂ ⚙️ 🔗 ⏮️ 🙆 💽 & ⏮️ 🙆 👩‍💻 ⚖️ 💽 🏷. + +🕴 ℹ ❌ 👈 ⚫️ 🚫 🤙 "🔐". + +⏭ 📃 👆 🔜 👀 ❔ ⚙️ 🔐 🔐 🔁 🗃 & 🥙 🤝. diff --git a/docs/em/docs/tutorial/sql-databases.md b/docs/em/docs/tutorial/sql-databases.md new file mode 100644 index 0000000000..e3ced7ef4c --- /dev/null +++ b/docs/em/docs/tutorial/sql-databases.md @@ -0,0 +1,786 @@ +# 🗄 (🔗) 💽 + +**FastAPI** 🚫 🚚 👆 ⚙️ 🗄 (🔗) 💽. + +✋️ 👆 💪 ⚙️ 🙆 🔗 💽 👈 👆 💚. + +📥 👥 🔜 👀 🖼 ⚙️ 🇸🇲. + +👆 💪 💪 🛠️ ⚫️ 🙆 💽 🐕‍🦺 🇸🇲, 💖: + +* ✳ +* ✳ +* 🗄 +* 🐸 +* 🤸‍♂ 🗄 💽, ♒️. + +👉 🖼, 👥 🔜 ⚙️ **🗄**, ↩️ ⚫️ ⚙️ 👁 📁 & 🐍 ✔️ 🛠️ 🐕‍🦺. , 👆 💪 📁 👉 🖼 & 🏃 ⚫️. + +⏪, 👆 🏭 🈸, 👆 💪 💚 ⚙️ 💽 💽 💖 **✳**. + +!!! tip + 📤 🛂 🏗 🚂 ⏮️ **FastAPI** & **✳**, 🌐 ⚓️ 🔛 **☁**, 🔌 🕸 & 🌖 🧰: https://github.com/tiangolo/full-stack-fastapi-postgresql + +!!! note + 👀 👈 📚 📟 🐩 `SQLAlchemy` 📟 👆 🔜 ⚙️ ⏮️ 🙆 🛠️. + + **FastAPI** 🎯 📟 🤪 🕧. + +## 🐜 + +**FastAPI** 👷 ⏮️ 🙆 💽 & 🙆 👗 🗃 💬 💽. + +⚠ ⚓ ⚙️ "🐜": "🎚-🔗 🗺" 🗃. + +🐜 ✔️ 🧰 🗜 ("*🗺*") 🖖 *🎚* 📟 & 💽 🏓 ("*🔗*"). + +⏮️ 🐜, 👆 🛎 ✍ 🎓 👈 🎨 🏓 🗄 💽, 🔠 🔢 🎓 🎨 🏓, ⏮️ 📛 & 🆎. + +🖼 🎓 `Pet` 💪 🎨 🗄 🏓 `pets`. + +& 🔠 *👐* 🎚 👈 🎓 🎨 ⏭ 💽. + +🖼 🎚 `orion_cat` (👐 `Pet`) 💪 ✔️ 🔢 `orion_cat.type`, 🏓 `type`. & 💲 👈 🔢 💪, ✅ `"cat"`. + +👫 🐜 ✔️ 🧰 ⚒ 🔗 ⚖️ 🔗 🖖 🏓 ⚖️ 👨‍💼. + +👉 🌌, 👆 💪 ✔️ 🔢 `orion_cat.owner` & 👨‍💼 🔜 🔌 💽 👉 🐶 👨‍💼, ✊ ⚪️➡️ 🏓 *👨‍💼*. + +, `orion_cat.owner.name` 💪 📛 (⚪️➡️ `name` 🏓 `owners` 🏓) 👉 🐶 👨‍💼. + +⚫️ 💪 ✔️ 💲 💖 `"Arquilian"`. + +& 🐜 🔜 🌐 👷 🤚 ℹ ⚪️➡️ 🔗 🏓 *👨‍💼* 🕐❔ 👆 🔄 🔐 ⚫️ ⚪️➡️ 👆 🐶 🎚. + +⚠ 🐜 🖼: ✳-🐜 (🍕 ✳ 🛠️), 🇸🇲 🐜 (🍕 🇸🇲, 🔬 🛠️) & 🏒 (🔬 🛠️), 👪 🎏. + +📥 👥 🔜 👀 ❔ 👷 ⏮️ **🇸🇲 🐜**. + +🎏 🌌 👆 💪 ⚙️ 🙆 🎏 🐜. + +!!! tip + 📤 🌓 📄 ⚙️ 🏒 📥 🩺. + +## 📁 📊 + +👫 🖼, ➡️ 💬 👆 ✔️ 📁 📛 `my_super_project` 👈 🔌 🎧-📁 🤙 `sql_app` ⏮️ 📊 💖 👉: + +``` +. +└── sql_app + ├── __init__.py + ├── crud.py + ├── database.py + ├── main.py + ├── models.py + └── schemas.py +``` + +📁 `__init__.py` 🛁 📁, ✋️ ⚫️ 💬 🐍 👈 `sql_app` ⏮️ 🌐 🚮 🕹 (🐍 📁) 📦. + +🔜 ➡️ 👀 ⚫️❔ 🔠 📁/🕹 🔨. + +## ❎ `SQLAlchemy` + +🥇 👆 💪 ❎ `SQLAlchemy`: + +
+ +```console +$ pip install sqlalchemy + +---> 100% +``` + +
+ +## ✍ 🇸🇲 🍕 + +➡️ 🔗 📁 `sql_app/database.py`. + +### 🗄 🇸🇲 🍕 + +```Python hl_lines="1-3" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +### ✍ 💽 📛 🇸🇲 + +```Python hl_lines="5-6" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +👉 🖼, 👥 "🔗" 🗄 💽 (📂 📁 ⏮️ 🗄 💽). + +📁 🔜 🔎 🎏 📁 📁 `sql_app.db`. + +👈 ⚫️❔ 🏁 🍕 `./sql_app.db`. + +🚥 👆 ⚙️ **✳** 💽 ↩️, 👆 🔜 ✔️ ✍ ⏸: + +```Python +SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" +``` + +...& 🛠️ ⚫️ ⏮️ 👆 💽 📊 & 🎓 (📊 ✳, ✳ ⚖️ 🙆 🎏). + +!!! tip + + 👉 👑 ⏸ 👈 👆 🔜 ✔️ 🔀 🚥 👆 💚 ⚙️ 🎏 💽. + +### ✍ 🇸🇲 `engine` + +🥇 🔁 ✍ 🇸🇲 "🚒". + +👥 🔜 ⏪ ⚙️ 👉 `engine` 🎏 🥉. + +```Python hl_lines="8-10" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +#### 🗒 + +❌: + +```Python +connect_args={"check_same_thread": False} +``` + +...💪 🕴 `SQLite`. ⚫️ 🚫 💪 🎏 💽. + +!!! info "📡 ℹ" + + 🔢 🗄 🔜 🕴 ✔ 1️⃣ 🧵 🔗 ⏮️ ⚫️, 🤔 👈 🔠 🧵 🔜 🍵 🔬 📨. + + 👉 ❎ 😫 🤝 🎏 🔗 🎏 👜 (🎏 📨). + + ✋️ FastAPI, ⚙️ 😐 🔢 (`def`) 🌅 🌘 1️⃣ 🧵 💪 🔗 ⏮️ 💽 🎏 📨, 👥 💪 ⚒ 🗄 💭 👈 ⚫️ 🔜 ✔ 👈 ⏮️ `connect_args={"check_same_thread": False}`. + + , 👥 🔜 ⚒ 💭 🔠 📨 🤚 🚮 👍 💽 🔗 🎉 🔗, 📤 🙅‍♂ 💪 👈 🔢 🛠️. + +### ✍ `SessionLocal` 🎓 + +🔠 👐 `SessionLocal` 🎓 🔜 💽 🎉. 🎓 ⚫️ 🚫 💽 🎉. + +✋️ 🕐 👥 ✍ 👐 `SessionLocal` 🎓, 👉 👐 🔜 ☑ 💽 🎉. + +👥 📛 ⚫️ `SessionLocal` 🔬 ⚫️ ⚪️➡️ `Session` 👥 🏭 ⚪️➡️ 🇸🇲. + +👥 🔜 ⚙️ `Session` (1️⃣ 🗄 ⚪️➡️ 🇸🇲) ⏪. + +✍ `SessionLocal` 🎓, ⚙️ 🔢 `sessionmaker`: + +```Python hl_lines="11" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +### ✍ `Base` 🎓 + +🔜 👥 🔜 ⚙️ 🔢 `declarative_base()` 👈 📨 🎓. + +⏪ 👥 🔜 😖 ⚪️➡️ 👉 🎓 ✍ 🔠 💽 🏷 ⚖️ 🎓 (🐜 🏷): + +```Python hl_lines="13" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +## ✍ 💽 🏷 + +➡️ 🔜 👀 📁 `sql_app/models.py`. + +### ✍ 🇸🇲 🏷 ⚪️➡️ `Base` 🎓 + +👥 🔜 ⚙️ 👉 `Base` 🎓 👥 ✍ ⏭ ✍ 🇸🇲 🏷. + +!!! tip + 🇸🇲 ⚙️ ⚖ "**🏷**" 🔗 👉 🎓 & 👐 👈 🔗 ⏮️ 💽. + + ✋️ Pydantic ⚙️ ⚖ "**🏷**" 🔗 🕳 🎏, 💽 🔬, 🛠️, & 🧾 🎓 & 👐. + +🗄 `Base` ⚪️➡️ `database` (📁 `database.py` ⚪️➡️ 🔛). + +✍ 🎓 👈 😖 ⚪️➡️ ⚫️. + +👫 🎓 🇸🇲 🏷. + +```Python hl_lines="4 7-8 18-19" +{!../../../docs_src/sql_databases/sql_app/models.py!} +``` + +`__tablename__` 🔢 💬 🇸🇲 📛 🏓 ⚙️ 💽 🔠 👫 🏷. + +### ✍ 🏷 🔢/🏓 + +🔜 ✍ 🌐 🏷 (🎓) 🔢. + +🔠 👫 🔢 🎨 🏓 🚮 🔗 💽 🏓. + +👥 ⚙️ `Column` ⚪️➡️ 🇸🇲 🔢 💲. + +& 👥 🚶‍♀️ 🇸🇲 🎓 "🆎", `Integer`, `String`, & `Boolean`, 👈 🔬 🆎 💽, ❌. + +```Python hl_lines="1 10-13 21-24" +{!../../../docs_src/sql_databases/sql_app/models.py!} +``` + +### ✍ 💛 + +🔜 ✍ 💛. + +👉, 👥 ⚙️ `relationship` 🚚 🇸🇲 🐜. + +👉 🔜 ▶️️, 🌅 ⚖️ 🌘, "🎱" 🔢 👈 🔜 🔌 💲 ⚪️➡️ 🎏 🏓 🔗 👉 1️⃣. + +```Python hl_lines="2 15 26" +{!../../../docs_src/sql_databases/sql_app/models.py!} +``` + +🕐❔ 🔐 🔢 `items` `User`, `my_user.items`, ⚫️ 🔜 ✔️ 📇 `Item` 🇸🇲 🏷 (⚪️➡️ `items` 🏓) 👈 ✔️ 💱 🔑 ☝ 👉 ⏺ `users` 🏓. + +🕐❔ 👆 🔐 `my_user.items`, 🇸🇲 🔜 🤙 🚶 & ☕ 🏬 ⚪️➡️ 💽 `items` 🏓 & 🔗 👫 📥. + +& 🕐❔ 🔐 🔢 `owner` `Item`, ⚫️ 🔜 🔌 `User` 🇸🇲 🏷 ⚪️➡️ `users` 🏓. ⚫️ 🔜 ⚙️ `owner_id` 🔢/🏓 ⏮️ 🚮 💱 🔑 💭 ❔ ⏺ 🤚 ⚪️➡️ `users` 🏓. + +## ✍ Pydantic 🏷 + +🔜 ➡️ ✅ 📁 `sql_app/schemas.py`. + +!!! tip + ❎ 😨 🖖 🇸🇲 *🏷* & Pydantic *🏷*, 👥 🔜 ✔️ 📁 `models.py` ⏮️ 🇸🇲 🏷, & 📁 `schemas.py` ⏮️ Pydantic 🏷. + + 👫 Pydantic 🏷 🔬 🌅 ⚖️ 🌘 "🔗" (☑ 📊 💠). + + 👉 🔜 ℹ 👥 ❎ 😨 ⏪ ⚙️ 👯‍♂️. + +### ✍ ▶️ Pydantic *🏷* / 🔗 + +✍ `ItemBase` & `UserBase` Pydantic *🏷* (⚖️ ➡️ 💬 "🔗") ✔️ ⚠ 🔢 ⏪ 🏗 ⚖️ 👂 📊. + +& ✍ `ItemCreate` & `UserCreate` 👈 😖 ⚪️➡️ 👫 (👫 🔜 ✔️ 🎏 🔢), ➕ 🙆 🌖 📊 (🔢) 💪 🏗. + +, 👩‍💻 🔜 ✔️ `password` 🕐❔ 🏗 ⚫️. + +✋️ 💂‍♂, `password` 🏆 🚫 🎏 Pydantic *🏷*, 🖼, ⚫️ 🏆 🚫 📨 ⚪️➡️ 🛠️ 🕐❔ 👂 👩‍💻. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1 4-6 9-10 21-22 25-26" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` + +#### 🇸🇲 👗 & Pydantic 👗 + +👀 👈 🇸🇲 *🏷* 🔬 🔢 ⚙️ `=`, & 🚶‍♀️ 🆎 🔢 `Column`, 💖: + +```Python +name = Column(String) +``` + +⏪ Pydantic *🏷* 📣 🆎 ⚙️ `:`, 🆕 🆎 ✍ ❕/🆎 🔑: + +```Python +name: str +``` + +✔️ ⚫️ 🤯, 👆 🚫 🤚 😕 🕐❔ ⚙️ `=` & `:` ⏮️ 👫. + +### ✍ Pydantic *🏷* / 🔗 👂 / 📨 + +🔜 ✍ Pydantic *🏷* (🔗) 👈 🔜 ⚙️ 🕐❔ 👂 💽, 🕐❔ 🛬 ⚫️ ⚪️➡️ 🛠️. + +🖼, ⏭ 🏗 🏬, 👥 🚫 💭 ⚫️❔ 🔜 🆔 🛠️ ⚫️, ✋️ 🕐❔ 👂 ⚫️ (🕐❔ 🛬 ⚫️ ⚪️➡️ 🛠️) 👥 🔜 ⏪ 💭 🚮 🆔. + +🎏 🌌, 🕐❔ 👂 👩‍💻, 👥 💪 🔜 📣 👈 `items` 🔜 🔌 🏬 👈 💭 👉 👩‍💻. + +🚫 🕴 🆔 📚 🏬, ✋️ 🌐 💽 👈 👥 🔬 Pydantic *🏷* 👂 🏬: `Item`. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="13-15 29-32" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` + +!!! tip + 👀 👈 `User`, Pydantic *🏷* 👈 🔜 ⚙️ 🕐❔ 👂 👩‍💻 (🛬 ⚫️ ⚪️➡️ 🛠️) 🚫 🔌 `password`. + +### ⚙️ Pydantic `orm_mode` + +🔜, Pydantic *🏷* 👂, `Item` & `User`, 🚮 🔗 `Config` 🎓. + +👉 `Config` 🎓 ⚙️ 🚚 📳 Pydantic. + +`Config` 🎓, ⚒ 🔢 `orm_mode = True`. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="15 19-20 31 36-37" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="15 19-20 31 36-37" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="13 17-18 29 34-35" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` + +!!! tip + 👀 ⚫️ ⚖ 💲 ⏮️ `=`, 💖: + + `orm_mode = True` + + ⚫️ 🚫 ⚙️ `:` 🆎 📄 ⏭. + + 👉 ⚒ 📁 💲, 🚫 📣 🆎. + +Pydantic `orm_mode` 🔜 💬 Pydantic *🏷* ✍ 💽 🚥 ⚫️ 🚫 `dict`, ✋️ 🐜 🏷 (⚖️ 🙆 🎏 ❌ 🎚 ⏮️ 🔢). + +👉 🌌, ↩️ 🕴 🔄 🤚 `id` 💲 ⚪️➡️ `dict`,: + +```Python +id = data["id"] +``` + +⚫️ 🔜 🔄 🤚 ⚫️ ⚪️➡️ 🔢,: + +```Python +id = data.id +``` + +& ⏮️ 👉, Pydantic *🏷* 🔗 ⏮️ 🐜, & 👆 💪 📣 ⚫️ `response_model` ❌ 👆 *➡ 🛠️*. + +👆 🔜 💪 📨 💽 🏷 & ⚫️ 🔜 ✍ 💽 ⚪️➡️ ⚫️. + +#### 📡 ℹ 🔃 🐜 📳 + +🇸🇲 & 📚 🎏 🔢 "🙃 🚚". + +👈 ⛓, 🖼, 👈 👫 🚫 ☕ 💽 💛 ⚪️➡️ 💽 🚥 👆 🔄 🔐 🔢 👈 🔜 🔌 👈 💽. + +🖼, 🔐 🔢 `items`: + +```Python +current_user.items +``` + +🔜 ⚒ 🇸🇲 🚶 `items` 🏓 & 🤚 🏬 👉 👩‍💻, ✋️ 🚫 ⏭. + +🍵 `orm_mode`, 🚥 👆 📨 🇸🇲 🏷 ⚪️➡️ 👆 *➡ 🛠️*, ⚫️ 🚫🔜 🔌 💛 💽. + +🚥 👆 📣 📚 💛 👆 Pydantic 🏷. + +✋️ ⏮️ 🐜 📳, Pydantic ⚫️ 🔜 🔄 🔐 💽 ⚫️ 💪 ⚪️➡️ 🔢 (↩️ 🤔 `dict`), 👆 💪 📣 🎯 💽 👆 💚 📨 & ⚫️ 🔜 💪 🚶 & 🤚 ⚫️, ⚪️➡️ 🐜. + +## 💩 🇨🇻 + +🔜 ➡️ 👀 📁 `sql_app/crud.py`. + +👉 📁 👥 🔜 ✔️ ♻ 🔢 🔗 ⏮️ 💽 💽. + +**💩** 👟 ⚪️➡️: **🅱**📧, **Ⓜ**💳, **👤** = , & **🇨🇮**📧. + +...👐 👉 🖼 👥 🕴 🏗 & 👂. + +### ✍ 💽 + +🗄 `Session` ⚪️➡️ `sqlalchemy.orm`, 👉 🔜 ✔ 👆 📣 🆎 `db` 🔢 & ✔️ 👻 🆎 ✅ & 🛠️ 👆 🔢. + +🗄 `models` (🇸🇲 🏷) & `schemas` (Pydantic *🏷* / 🔗). + +✍ 🚙 🔢: + +* ✍ 👁 👩‍💻 🆔 & 📧. +* ✍ 💗 👩‍💻. +* ✍ 💗 🏬. + +```Python hl_lines="1 3 6-7 10-11 14-15 27-28" +{!../../../docs_src/sql_databases/sql_app/crud.py!} +``` + +!!! tip + 🏗 🔢 👈 🕴 💡 🔗 ⏮️ 💽 (🤚 👩‍💻 ⚖️ 🏬) 🔬 👆 *➡ 🛠️ 🔢*, 👆 💪 🌖 💪 ♻ 👫 💗 🍕 & 🚮 ⚒ 💯 👫. + +### ✍ 💽 + +🔜 ✍ 🚙 🔢 ✍ 💽. + +🔁: + +* ✍ 🇸🇲 🏷 *👐* ⏮️ 👆 📊. +* `add` 👈 👐 🎚 👆 💽 🎉. +* `commit` 🔀 💽 (👈 👫 🖊). +* `refresh` 👆 👐 (👈 ⚫️ 🔌 🙆 🆕 📊 ⚪️➡️ 💽, 💖 🏗 🆔). + +```Python hl_lines="18-24 31-36" +{!../../../docs_src/sql_databases/sql_app/crud.py!} +``` + +!!! tip + 🇸🇲 🏷 `User` 🔌 `hashed_password` 👈 🔜 🔌 🔐 #️⃣ ⏬ 🔐. + + ✋️ ⚫️❔ 🛠️ 👩‍💻 🚚 ⏮️ 🔐, 👆 💪 ⚗ ⚫️ & 🏗 #️⃣ 🔐 👆 🈸. + + & ⤴️ 🚶‍♀️ `hashed_password` ❌ ⏮️ 💲 🖊. + +!!! warning + 👉 🖼 🚫 🔐, 🔐 🚫#️⃣. + + 🎰 👨‍❤‍👨 🈸 👆 🔜 💪 #️⃣ 🔐 & 🙅 🖊 👫 🔢. + + 🌅 ℹ, 🚶 🔙 💂‍♂ 📄 🔰. + + 📥 👥 🎯 🕴 🔛 🧰 & 👨‍🔧 💽. + +!!! tip + ↩️ 🚶‍♀️ 🔠 🇨🇻 ❌ `Item` & 👂 🔠 1️⃣ 👫 ⚪️➡️ Pydantic *🏷*, 👥 🏭 `dict` ⏮️ Pydantic *🏷*'Ⓜ 📊 ⏮️: + + `item.dict()` + + & ⤴️ 👥 🚶‍♀️ `dict`'Ⓜ 🔑-💲 👫 🇨🇻 ❌ 🇸🇲 `Item`, ⏮️: + + `Item(**item.dict())` + + & ⤴️ 👥 🚶‍♀️ ➕ 🇨🇻 ❌ `owner_id` 👈 🚫 🚚 Pydantic *🏷*, ⏮️: + + `Item(**item.dict(), owner_id=user_id)` + +## 👑 **FastAPI** 📱 + +& 🔜 📁 `sql_app/main.py` ➡️ 🛠️ & ⚙️ 🌐 🎏 🍕 👥 ✍ ⏭. + +### ✍ 💽 🏓 + +📶 🙃 🌌 ✍ 💽 🏓: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +#### ⚗ 🗒 + +🛎 👆 🔜 🎲 🔢 👆 💽 (✍ 🏓, ♒️) ⏮️ . + +& 👆 🔜 ⚙️ ⚗ "🛠️" (👈 🚮 👑 👨‍🏭). + +"🛠️" ⚒ 🔁 💪 🕐❔ 👆 🔀 📊 👆 🇸🇲 🏷, 🚮 🆕 🔢, ♒️. 🔁 👈 🔀 💽, 🚮 🆕 🏓, 🆕 🏓, ♒️. + +👆 💪 🔎 🖼 ⚗ FastAPI 🏗 📄 ⚪️➡️ [🏗 ⚡ - 📄](../project-generation.md){.internal-link target=_blank}. 🎯 `alembic` 📁 ℹ 📟. + +### ✍ 🔗 + +🔜 ⚙️ `SessionLocal` 🎓 👥 ✍ `sql_app/database.py` 📁 ✍ 🔗. + +👥 💪 ✔️ 🔬 💽 🎉/🔗 (`SessionLocal`) 📍 📨, ⚙️ 🎏 🎉 🔘 🌐 📨 & ⤴️ 🔐 ⚫️ ⏮️ 📨 🏁. + +& ⤴️ 🆕 🎉 🔜 ✍ ⏭ 📨. + +👈, 👥 🔜 ✍ 🆕 🔗 ⏮️ `yield`, 🔬 ⏭ 📄 🔃 [🔗 ⏮️ `yield`](dependencies/dependencies-with-yield.md){.internal-link target=_blank}. + +👆 🔗 🔜 ✍ 🆕 🇸🇲 `SessionLocal` 👈 🔜 ⚙️ 👁 📨, & ⤴️ 🔐 ⚫️ 🕐 📨 🏁. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="15-20" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="13-18" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +!!! info + 👥 🚮 🏗 `SessionLocal()` & 🚚 📨 `try` 🍫. + + & ⤴️ 👥 🔐 ⚫️ `finally` 🍫. + + 👉 🌌 👥 ⚒ 💭 💽 🎉 🕧 📪 ⏮️ 📨. 🚥 📤 ⚠ ⏪ 🏭 📨. + + ✋️ 👆 💪 🚫 🤚 ➕1️⃣ ⚠ ⚪️➡️ 🚪 📟 (⏮️ `yield`). 👀 🌖 [🔗 ⏮️ `yield` & `HTTPException`](./dependencies/dependencies-with-yield.md#dependencies-with-yield-and-httpexception){.internal-link target=_blank} + +& ⤴️, 🕐❔ ⚙️ 🔗 *➡ 🛠️ 🔢*, 👥 📣 ⚫️ ⏮️ 🆎 `Session` 👥 🗄 🔗 ⚪️➡️ 🇸🇲. + +👉 🔜 ⤴️ 🤝 👥 👍 👨‍🎨 🐕‍🦺 🔘 *➡ 🛠️ 🔢*, ↩️ 👨‍🎨 🔜 💭 👈 `db` 🔢 🆎 `Session`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="24 32 38 47 53" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="22 30 36 45 51" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +!!! info "📡 ℹ" + 🔢 `db` 🤙 🆎 `SessionLocal`, ✋️ 👉 🎓 (✍ ⏮️ `sessionmaker()`) "🗳" 🇸🇲 `Session`,, 👨‍🎨 🚫 🤙 💭 ⚫️❔ 👩‍🔬 🚚. + + ✋️ 📣 🆎 `Session`, 👨‍🎨 🔜 💪 💭 💪 👩‍🔬 (`.add()`, `.query()`, `.commit()`, ♒️) & 💪 🚚 👍 🐕‍🦺 (💖 🛠️). 🆎 📄 🚫 📉 ☑ 🎚. + +### ✍ 👆 **FastAPI** *➡ 🛠️* + +🔜, 😒, 📥 🐩 **FastAPI** *➡ 🛠️* 📟. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +👥 🏗 💽 🎉 ⏭ 🔠 📨 🔗 ⏮️ `yield`, & ⤴️ 📪 ⚫️ ⏮️. + +& ⤴️ 👥 💪 ✍ 🚚 🔗 *➡ 🛠️ 🔢*, 🤚 👈 🎉 🔗. + +⏮️ 👈, 👥 💪 🤙 `crud.get_user` 🔗 ⚪️➡️ 🔘 *➡ 🛠️ 🔢* & ⚙️ 👈 🎉. + +!!! tip + 👀 👈 💲 👆 📨 🇸🇲 🏷, ⚖️ 📇 🇸🇲 🏷. + + ✋️ 🌐 *➡ 🛠️* ✔️ `response_model` ⏮️ Pydantic *🏷* / 🔗 ⚙️ `orm_mode`, 💽 📣 👆 Pydantic 🏷 🔜 ⚗ ⚪️➡️ 👫 & 📨 👩‍💻, ⏮️ 🌐 😐 ⛽ & 🔬. + +!!! tip + 👀 👈 📤 `response_models` 👈 ✔️ 🐩 🐍 🆎 💖 `List[schemas.Item]`. + + ✋️ 🎚/🔢 👈 `List` Pydantic *🏷* ⏮️ `orm_mode`, 💽 🔜 🗃 & 📨 👩‍💻 🛎, 🍵 ⚠. + +### 🔃 `def` 🆚 `async def` + +📥 👥 ⚙️ 🇸🇲 📟 🔘 *➡ 🛠️ 🔢* & 🔗, &, 🔄, ⚫️ 🔜 🚶 & 🔗 ⏮️ 🔢 💽. + +👈 💪 ⚠ 🚚 "⌛". + +✋️ 🇸🇲 🚫 ✔️ 🔗 ⚙️ `await` 🔗, 🔜 ⏮️ 🕳 💖: + +```Python +user = await db.query(User).first() +``` + +...& ↩️ 👥 ⚙️: + +```Python +user = db.query(User).first() +``` + +⤴️ 👥 🔜 📣 *➡ 🛠️ 🔢* & 🔗 🍵 `async def`, ⏮️ 😐 `def`,: + +```Python hl_lines="2" +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + ... +``` + +!!! info + 🚥 👆 💪 🔗 👆 🔗 💽 🔁, 👀 [🔁 🗄 (🔗) 💽](../advanced/async-sql-databases.md){.internal-link target=_blank}. + +!!! note "📶 📡 ℹ" + 🚥 👆 😟 & ✔️ ⏬ 📡 💡, 👆 💪 ✅ 📶 📡 ℹ ❔ 👉 `async def` 🆚 `def` 🍵 [🔁](../async.md#very-technical-details){.internal-link target=_blank} 🩺. + +## 🛠️ + +↩️ 👥 ⚙️ 🇸🇲 🔗 & 👥 🚫 🚚 🙆 😇 🔌-⚫️ 👷 ⏮️ **FastAPI**, 👥 💪 🛠️ 💽 🛠️ ⏮️ 🔗. + +& 📟 🔗 🇸🇲 & 🇸🇲 🏷 🖖 🎏 🔬 📁, 👆 🔜 💪 🎭 🛠️ ⏮️ ⚗ 🍵 ✔️ ❎ FastAPI, Pydantic, ⚖️ 🕳 🙆. + +🎏 🌌, 👆 🔜 💪 ⚙️ 🎏 🇸🇲 🏷 & 🚙 🎏 🍕 👆 📟 👈 🚫 🔗 **FastAPI**. + +🖼, 🖥 📋 👨‍🏭 ⏮️ 🥒, 🅿, ⚖️ 📶. + +## 📄 🌐 📁 + + 💭 👆 🔜 ✔️ 📁 📛 `my_super_project` 👈 🔌 🎧-📁 🤙 `sql_app`. + +`sql_app` 🔜 ✔️ 📄 📁: + +* `sql_app/__init__.py`: 🛁 📁. + +* `sql_app/database.py`: + +```Python +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +* `sql_app/models.py`: + +```Python +{!../../../docs_src/sql_databases/sql_app/models.py!} +``` + +* `sql_app/schemas.py`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` + +* `sql_app/crud.py`: + +```Python +{!../../../docs_src/sql_databases/sql_app/crud.py!} +``` + +* `sql_app/main.py`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +## ✅ ⚫️ + +👆 💪 📁 👉 📟 & ⚙️ ⚫️. + +!!! info + + 👐, 📟 🎦 📥 🍕 💯. 🌅 📟 👉 🩺. + +⤴️ 👆 💪 🏃 ⚫️ ⏮️ Uvicorn: + + +
+ +```console +$ uvicorn sql_app.main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +& ⤴️, 👆 💪 📂 👆 🖥 http://127.0.0.1:8000/docs. + +& 👆 🔜 💪 🔗 ⏮️ 👆 **FastAPI** 🈸, 👂 📊 ⚪️➡️ 🎰 💽: + + + +## 🔗 ⏮️ 💽 🔗 + +🚥 👆 💚 🔬 🗄 💽 (📁) 🔗, ➡ FastAPI, ℹ 🚮 🎚, 🚮 🏓, 🏓, ⏺, 🔀 📊, ♒️. 👆 💪 ⚙️ 💽 🖥 🗄. + +⚫️ 🔜 👀 💖 👉: + + + +👆 💪 ⚙️ 💳 🗄 🖥 💖 🗄 📋 ⚖️ ExtendsClass. + +## 🎛 💽 🎉 ⏮️ 🛠️ + +🚥 👆 💪 🚫 ⚙️ 🔗 ⏮️ `yield` - 🖼, 🚥 👆 🚫 ⚙️ **🐍 3️⃣.7️⃣** & 💪 🚫 ❎ "🐛" 🤔 🔛 **🐍 3️⃣.6️⃣** - 👆 💪 ⚒ 🆙 🎉 "🛠️" 🎏 🌌. + +"🛠️" 🌖 🔢 👈 🕧 🛠️ 🔠 📨, ⏮️ 📟 🛠️ ⏭, & 📟 🛠️ ⏮️ 🔗 🔢. + +### ✍ 🛠️ + +🛠️ 👥 🔜 🚮 (🔢) 🔜 ✍ 🆕 🇸🇲 `SessionLocal` 🔠 📨, 🚮 ⚫️ 📨 & ⤴️ 🔐 ⚫️ 🕐 📨 🏁. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="14-22" + {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="12-20" + {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} + ``` + +!!! info + 👥 🚮 🏗 `SessionLocal()` & 🚚 📨 `try` 🍫. + + & ⤴️ 👥 🔐 ⚫️ `finally` 🍫. + + 👉 🌌 👥 ⚒ 💭 💽 🎉 🕧 📪 ⏮️ 📨. 🚥 📤 ⚠ ⏪ 🏭 📨. + +### 🔃 `request.state` + +`request.state` 🏠 🔠 `Request` 🎚. ⚫️ 📤 🏪 ❌ 🎚 📎 📨 ⚫️, 💖 💽 🎉 👉 💼. 👆 💪 ✍ 🌅 🔃 ⚫️ 💃 🩺 🔃 `Request` 🇵🇸. + +👥 👉 💼, ⚫️ ℹ 👥 🚚 👁 💽 🎉 ⚙️ 🔘 🌐 📨, & ⤴️ 🔐 ⏮️ (🛠️). + +### 🔗 ⏮️ `yield` ⚖️ 🛠️ + +❎ **🛠️** 📥 🎏 ⚫️❔ 🔗 ⏮️ `yield` 🔨, ⏮️ 🔺: + +* ⚫️ 🚚 🌖 📟 & 👄 🌅 🏗. +* 🛠️ ✔️ `async` 🔢. + * 🚥 📤 📟 ⚫️ 👈 ✔️ "⌛" 🕸, ⚫️ 💪 "🍫" 👆 🈸 📤 & 📉 🎭 🍖. + * 👐 ⚫️ 🎲 🚫 📶 ⚠ 📥 ⏮️ 🌌 `SQLAlchemy` 👷. + * ✋️ 🚥 👆 🚮 🌖 📟 🛠️ 👈 ✔️ 📚 👤/🅾 ⌛, ⚫️ 💪 ⤴️ ⚠. +* 🛠️ 🏃 *🔠* 📨. + * , 🔗 🔜 ✍ 🔠 📨. + * 🕐❔ *➡ 🛠️* 👈 🍵 👈 📨 🚫 💪 💽. + +!!! tip + ⚫️ 🎲 👍 ⚙️ 🔗 ⏮️ `yield` 🕐❔ 👫 🥃 ⚙️ 💼. + +!!! info + 🔗 ⏮️ `yield` 🚮 ⏳ **FastAPI**. + + ⏮️ ⏬ 👉 🔰 🕴 ✔️ 🖼 ⏮️ 🛠️ & 📤 🎲 📚 🈸 ⚙️ 🛠️ 💽 🎉 🧾. diff --git a/docs/em/docs/tutorial/static-files.md b/docs/em/docs/tutorial/static-files.md new file mode 100644 index 0000000000..6090c53385 --- /dev/null +++ b/docs/em/docs/tutorial/static-files.md @@ -0,0 +1,39 @@ +# 🎻 📁 + +👆 💪 🍦 🎻 📁 🔁 ⚪️➡️ 📁 ⚙️ `StaticFiles`. + +## ⚙️ `StaticFiles` + +* 🗄 `StaticFiles`. +* "🗻" `StaticFiles()` 👐 🎯 ➡. + +```Python hl_lines="2 6" +{!../../../docs_src/static_files/tutorial001.py!} +``` + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.staticfiles import StaticFiles`. + + **FastAPI** 🚚 🎏 `starlette.staticfiles` `fastapi.staticfiles` 🏪 👆, 👩‍💻. ✋️ ⚫️ 🤙 👟 🔗 ⚪️➡️ 💃. + +### ⚫️❔ "🗜" + +"🗜" ⛓ ❎ 🏁 "🔬" 🈸 🎯 ➡, 👈 ⤴️ ✊ 💅 🚚 🌐 🎧-➡. + +👉 🎏 ⚪️➡️ ⚙️ `APIRouter` 🗻 🈸 🍕 🔬. 🗄 & 🩺 ⚪️➡️ 👆 👑 🈸 🏆 🚫 🔌 🕳 ⚪️➡️ 🗻 🈸, ♒️. + +👆 💪 ✍ 🌅 🔃 👉 **🏧 👩‍💻 🦮**. + +## ℹ + +🥇 `"/static"` 🔗 🎧-➡ 👉 "🎧-🈸" 🔜 "🗻" 🔛. , 🙆 ➡ 👈 ▶️ ⏮️ `"/static"` 🔜 🍵 ⚫️. + +`directory="static"` 🔗 📛 📁 👈 🔌 👆 🎻 📁. + +`name="static"` 🤝 ⚫️ 📛 👈 💪 ⚙️ 🔘 **FastAPI**. + +🌐 👫 🔢 💪 🎏 🌘 "`static`", 🔆 👫 ⏮️ 💪 & 🎯 ℹ 👆 👍 🈸. + +## 🌅 ℹ + +🌖 ℹ & 🎛 ✅ 💃 🩺 🔃 🎻 📁. diff --git a/docs/em/docs/tutorial/testing.md b/docs/em/docs/tutorial/testing.md new file mode 100644 index 0000000000..999d67cd35 --- /dev/null +++ b/docs/em/docs/tutorial/testing.md @@ -0,0 +1,188 @@ +# 🔬 + +👏 💃, 🔬 **FastAPI** 🈸 ⏩ & 😌. + +⚫️ ⚓️ 🔛 🇸🇲, ❔ 🔄 🏗 ⚓️ 🔛 📨, ⚫️ 📶 😰 & 🏋️. + +⏮️ ⚫️, 👆 💪 ⚙️ 🔗 ⏮️ **FastAPI**. + +## ⚙️ `TestClient` + +!!! info + ⚙️ `TestClient`, 🥇 ❎ `httpx`. + + 🤶 Ⓜ. `pip install httpx`. + +🗄 `TestClient`. + +✍ `TestClient` 🚶‍♀️ 👆 **FastAPI** 🈸 ⚫️. + +✍ 🔢 ⏮️ 📛 👈 ▶️ ⏮️ `test_` (👉 🐩 `pytest` 🏛). + +⚙️ `TestClient` 🎚 🎏 🌌 👆 ⏮️ `httpx`. + +✍ 🙅 `assert` 📄 ⏮️ 🐩 🐍 🧬 👈 👆 💪 ✅ (🔄, 🐩 `pytest`). + +```Python hl_lines="2 12 15-18" +{!../../../docs_src/app_testing/tutorial001.py!} +``` + +!!! tip + 👀 👈 🔬 🔢 😐 `def`, 🚫 `async def`. + + & 🤙 👩‍💻 😐 🤙, 🚫 ⚙️ `await`. + + 👉 ✔ 👆 ⚙️ `pytest` 🔗 🍵 🤢. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.testclient import TestClient`. + + **FastAPI** 🚚 🎏 `starlette.testclient` `fastapi.testclient` 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. + +!!! tip + 🚥 👆 💚 🤙 `async` 🔢 👆 💯 ↖️ ⚪️➡️ 📨 📨 👆 FastAPI 🈸 (✅ 🔁 💽 🔢), ✔️ 👀 [🔁 💯](../advanced/async-tests.md){.internal-link target=_blank} 🏧 🔰. + +## 🎏 💯 + +🎰 🈸, 👆 🎲 🔜 ✔️ 👆 💯 🎏 📁. + +& 👆 **FastAPI** 🈸 5️⃣📆 ✍ 📚 📁/🕹, ♒️. + +### **FastAPI** 📱 📁 + +➡️ 💬 👆 ✔️ 📁 📊 🔬 [🦏 🈸](./bigger-applications.md){.internal-link target=_blank}: + +``` +. +├── app +│   ├── __init__.py +│   └── main.py +``` + +📁 `main.py` 👆 ✔️ 👆 **FastAPI** 📱: + + +```Python +{!../../../docs_src/app_testing/main.py!} +``` + +### 🔬 📁 + +⤴️ 👆 💪 ✔️ 📁 `test_main.py` ⏮️ 👆 💯. ⚫️ 💪 🖖 🔛 🎏 🐍 📦 (🎏 📁 ⏮️ `__init__.py` 📁): + +``` hl_lines="5" +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +↩️ 👉 📁 🎏 📦, 👆 💪 ⚙️ ⚖ 🗄 🗄 🎚 `app` ⚪️➡️ `main` 🕹 (`main.py`): + +```Python hl_lines="3" +{!../../../docs_src/app_testing/test_main.py!} +``` + +...& ✔️ 📟 💯 💖 ⏭. + +## 🔬: ↔ 🖼 + +🔜 ➡️ ↔ 👉 🖼 & 🚮 🌖 ℹ 👀 ❔ 💯 🎏 🍕. + +### ↔ **FastAPI** 📱 📁 + +➡️ 😣 ⏮️ 🎏 📁 📊 ⏭: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +➡️ 💬 👈 🔜 📁 `main.py` ⏮️ 👆 **FastAPI** 📱 ✔️ 🎏 **➡ 🛠️**. + +⚫️ ✔️ `GET` 🛠️ 👈 💪 📨 ❌. + +⚫️ ✔️ `POST` 🛠️ 👈 💪 📨 📚 ❌. + +👯‍♂️ *➡ 🛠️* 🚚 `X-Token` 🎚. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/app_testing/app_b_py310/main.py!} + ``` + +### ↔ 🔬 📁 + +👆 💪 ⤴️ ℹ `test_main.py` ⏮️ ↔ 💯: + +```Python +{!> ../../../docs_src/app_testing/app_b/test_main.py!} +``` + +🕐❔ 👆 💪 👩‍💻 🚶‍♀️ ℹ 📨 & 👆 🚫 💭 ❔, 👆 💪 🔎 (🇺🇸🔍) ❔ ⚫️ `httpx`, ⚖️ ❔ ⚫️ ⏮️ `requests`, 🇸🇲 🔧 ⚓️ 🔛 📨' 🔧. + +⤴️ 👆 🎏 👆 💯. + +🤶 Ⓜ.: + +* 🚶‍♀️ *➡* ⚖️ *🔢* 🔢, 🚮 ⚫️ 📛 ⚫️. +* 🚶‍♀️ 🎻 💪, 🚶‍♀️ 🐍 🎚 (✅ `dict`) 🔢 `json`. +* 🚥 👆 💪 📨 *📨 💽* ↩️ 🎻, ⚙️ `data` 🔢 ↩️. +* 🚶‍♀️ *🎚*, ⚙️ `dict` `headers` 🔢. +* *🍪*, `dict` `cookies` 🔢. + +🌖 ℹ 🔃 ❔ 🚶‍♀️ 💽 👩‍💻 (⚙️ `httpx` ⚖️ `TestClient`) ✅ 🇸🇲 🧾. + +!!! info + 🗒 👈 `TestClient` 📨 💽 👈 💪 🗜 🎻, 🚫 Pydantic 🏷. + + 🚥 👆 ✔️ Pydantic 🏷 👆 💯 & 👆 💚 📨 🚮 💽 🈸 ⏮️ 🔬, 👆 💪 ⚙️ `jsonable_encoder` 🔬 [🎻 🔗 🔢](encoder.md){.internal-link target=_blank}. + +## 🏃 ⚫️ + +⏮️ 👈, 👆 💪 ❎ `pytest`: + +
+ +```console +$ pip install pytest + +---> 100% +``` + +
+ +⚫️ 🔜 🔍 📁 & 💯 🔁, 🛠️ 👫, & 📄 🏁 🔙 👆. + +🏃 💯 ⏮️: + +
+ +```console +$ pytest + +================ test session starts ================ +platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 +rootdir: /home/user/code/superawesome-cli/app +plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 +collected 6 items + +---> 100% + +test_main.py ...... [100%] + +================= 1 passed in 0.03s ================= +``` + +
diff --git a/docs/em/mkdocs.yml b/docs/em/mkdocs.yml new file mode 100644 index 0000000000..de18856f44 --- /dev/null +++ b/docs/em/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index b7db150380..00d6f696de 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,13 +1,45 @@ -articles: - english: +Articles: + English: + - author: Ankit Anchlia + author_link: https://linkedin.com/in/aanchlia21 + link: https://hackernoon.com/explore-how-to-effectively-use-jwt-with-fastapi + title: Explore How to Effectively Use JWT With FastAPI + - author: Nicoló Lino + author_link: https://www.nlino.com + link: https://github.com/softwarebloat/python-tracing-demo + title: Instrument a FastAPI service adding tracing with OpenTelemetry and send/show traces in Grafana Tempo + - author: Mikhail Rozhkov, Elena Samuylova + author_link: https://www.linkedin.com/in/mnrozhkov/ + link: https://www.evidentlyai.com/blog/fastapi-tutorial + title: ML serving and monitoring with FastAPI and Evidently + - author: Visual Studio Code Team + author_link: https://code.visualstudio.com/ + link: https://code.visualstudio.com/docs/python/tutorial-fastapi + title: FastAPI Tutorial in Visual Studio Code + - author: Apitally + author_link: https://apitally.io + link: https://blog.apitally.io/fastapi-application-monitoring-made-easy + title: FastAPI application monitoring made easy + - author: John Philip + author_link: https://medium.com/@amjohnphilip + link: https://python.plainenglish.io/building-a-restful-api-with-fastapi-secure-signup-and-login-functionality-included-45cdbcb36106 + title: "Building a RESTful API with FastAPI: Secure Signup and Login Functionality Included" + - author: Keshav Malik + author_link: https://theinfosecguy.xyz/ + link: https://blog.theinfosecguy.xyz/building-a-crud-api-with-fastapi-and-supabase-a-step-by-step-guide + title: Building a CRUD API with FastAPI and Supabase + - author: Adejumo Ridwan Suleiman + author_link: https://www.linkedin.com/in/adejumoridwan/ + link: https://medium.com/python-in-plain-english/build-an-sms-spam-classifier-serverless-database-with-faunadb-and-fastapi-23dbb275bc5b + title: Build an SMS Spam Classifier Serverless Database with FaunaDB and FastAPI + - author: Raf Rasenberg + author_link: https://rafrasenberg.com/about/ + link: https://rafrasenberg.com/fastapi-lambda/ + title: 'FastAPI lambda container: serverless simplified' - author: Teresa N. Fontanella De Santis author_link: https://dev.to/ link: https://dev.to/teresafds/authorization-on-fastapi-with-casbin-41og title: Authorization on FastAPI with Casbin - - author: WayScript - author_link: https://www.wayscript.com - link: https://blog.wayscript.com/fast-api-quickstart/ - title: Quickstart Guide to Build and Host Responsive APIs with Fast API and WayScript - author: New Relic author_link: https://newrelic.com link: https://newrelic.com/instant-observability/fastapi/e559ec64-f765-4470-a15f-1901fcebb468 @@ -60,10 +92,6 @@ articles: author_link: https://dev.to/factorlive link: https://dev.to/factorlive/python-facebook-messenger-webhook-with-fastapi-on-glitch-4n90 title: Python Facebook messenger webhook with FastAPI on Glitch - - author: Dom Patmore - author_link: https://twitter.com/dompatmore - link: https://dompatmore.com/blog/authenticate-your-fastapi-app-with-auth0 - title: Authenticate Your FastAPI App with auth0 - author: Valon Januzaj author_link: https://www.linkedin.com/in/valon-januzaj-b02692187/ link: https://valonjanuzaj.medium.com/deploy-a-dockerized-fastapi-application-to-aws-cc757830ba1b @@ -76,10 +104,6 @@ articles: author_link: https://twitter.com/louis_guitton link: https://guitton.co/posts/fastapi-monitoring/ title: How to monitor your FastAPI service - - author: Julien Harbulot - author_link: https://julienharbulot.com/ - link: https://julienharbulot.com/notification-server.html - title: HTTP server to display desktop notifications - author: Precious Ndubueze author_link: https://medium.com/@gabbyprecious2000 link: https://medium.com/@gabbyprecious2000/creating-a-crud-app-with-fastapi-part-one-7c049292ad37 @@ -128,18 +152,10 @@ articles: author_link: https://wuilly.com/ link: https://wuilly.com/2019/10/real-time-notifications-with-python-and-postgres/ title: Real-time Notifications with Python and Postgres - - author: Benjamin Ramser - author_link: https://iwpnd.pw - link: https://iwpnd.pw/articles/2020-03/apache-kafka-fastapi-geostream - title: Apache Kafka producer and consumer with FastAPI and aiokafka - author: Navule Pavan Kumar Rao author_link: https://www.linkedin.com/in/navule/ link: https://www.tutlinks.com/create-and-deploy-fastapi-app-to-heroku/ title: Create and Deploy FastAPI app to Heroku without using Docker - - author: Benjamin Ramser - author_link: https://iwpnd.pw - link: https://iwpnd.pw/articles/2020-01/deploy-fastapi-to-aws-lambda - title: How to continuously deploy a FastAPI to AWS Lambda with AWS SAM - author: Arthur Henrique author_link: https://twitter.com/arthurheinrique link: https://medium.com/@arthur393/another-boilerplate-to-fastapi-azure-pipeline-ci-pytest-3c8d9a4be0bb @@ -164,10 +180,6 @@ articles: author_link: https://dev.to/dbanty link: https://dev.to/dbanty/why-i-m-leaving-flask-3ki6 title: Why I'm Leaving Flask - - author: Rob Wagner - author_link: https://robwagner.dev/ - link: https://robwagner.dev/tortoise-fastapi-setup/ - title: Setting up Tortoise ORM with FastAPI - author: Mike Moritz author_link: https://medium.com/@mike.p.moritz link: https://medium.com/@mike.p.moritz/using-docker-compose-to-deploy-a-lightweight-python-rest-api-with-a-job-queue-37e6072a209b @@ -228,7 +240,11 @@ articles: author_link: https://medium.com/@krishnardt365 link: https://medium.com/@krishnardt365/fastapi-docker-and-postgres-91943e71be92 title: Fastapi, Docker(Docker compose) and Postgres - german: + German: + - author: Marcel Sander (actidoo) + author_link: https://www.actidoo.com + link: https://www.actidoo.com/de/blog/python-fastapi-domain-driven-design + title: Domain-driven Design mit Python und FastAPI - author: Nico Axtmann author_link: https://twitter.com/_nicoax link: https://blog.codecentric.de/2019/08/inbetriebnahme-eines-scikit-learn-modells-mit-onnx-und-fastapi/ @@ -237,7 +253,7 @@ articles: author_link: https://hellocoding.de/autor/felix-schuermeyer/ link: https://hellocoding.de/blog/coding-language/python/fastapi title: REST-API Programmieren mittels Python und dem FastAPI Modul - japanese: + Japanese: - author: '@bee2' author_link: https://qiita.com/bee2 link: https://qiita.com/bee2/items/75d9c0d7ba20e7a4a0e9 @@ -286,7 +302,7 @@ articles: author_link: https://qiita.com/mtitg link: https://qiita.com/mtitg/items/47770e9a562dd150631d title: FastAPI|DB接続してCRUDするPython製APIサーバーを構築 - russian: + Russian: - author: Troy Köhler author_link: https://www.linkedin.com/in/trkohler/ link: https://trkohler.com/fast-api-introduction-to-framework @@ -299,13 +315,18 @@ articles: author_link: https://habr.com/ru/users/57uff3r/ link: https://habr.com/ru/post/454440/ title: 'Мелкая питонячая радость #2: Starlette - Солидная примочка – FastAPI' - vietnamese: + Vietnamese: - author: Nguyễn Nhân author_link: https://fullstackstation.com/author/figonking/ link: https://fullstackstation.com/fastapi-trien-khai-bang-docker/ title: 'FASTAPI: TRIỂN KHAI BẰNG DOCKER' -podcasts: - english: + Taiwanese: + - author: Leon + author_link: http://editor.leonh.space/ + link: https://editor.leonh.space/2022/tortoise/ + title: 'Tortoise ORM / FastAPI 整合快速筆記' +Podcasts: + English: - author: Podcast.`__init__` author_link: https://www.pythonpodcast.com/ link: https://www.pythonpodcast.com/fastapi-web-application-framework-episode-259/ @@ -314,8 +335,12 @@ podcasts: author_link: https://pythonbytes.fm/ link: https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855 title: FastAPI on PythonBytes -talks: - english: +Talks: + English: + - author: Jeny Sadadia + author_link: https://github.com/JenySadadia + link: https://www.youtube.com/watch?v=uZdTe8_Z6BQ + title: 'PyCon AU 2023: Testing asynchronous applications with FastAPI and pytest' - author: Sebastián Ramírez (tiangolo) author_link: https://twitter.com/tiangolo link: https://www.youtube.com/watch?v=PnpTY1f4k2U diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 3d6831db61..713f229cf4 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -1,142 +1,124 @@ sponsors: -- - login: jina-ai - avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4 - url: https://github.com/jina-ai -- - login: cryptapi +- - login: scalar + avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4 + url: https://github.com/scalar + - login: codacy + avatarUrl: https://avatars.githubusercontent.com/u/1834093?v=4 + url: https://github.com/codacy + - login: bump-sh + avatarUrl: https://avatars.githubusercontent.com/u/33217836?v=4 + url: https://github.com/bump-sh + - login: Alek99 + avatarUrl: https://avatars.githubusercontent.com/u/38776361?u=bd6c163fe787c2de1a26c881598e54b67e2482dd&v=4 + url: https://github.com/Alek99 + - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi + - login: porter-dev + avatarUrl: https://avatars.githubusercontent.com/u/62078005?v=4 + url: https://github.com/porter-dev + - login: andrew-propelauth + avatarUrl: https://avatars.githubusercontent.com/u/89474256?u=1188c27cb744bbec36447a2cfd4453126b2ddb5c&v=4 + url: https://github.com/andrew-propelauth - - login: nihpo avatarUrl: https://avatars.githubusercontent.com/u/1841030?u=0264956d7580f7e46687a762a7baa629f84cf97c&v=4 url: https://github.com/nihpo - login: ObliviousAI avatarUrl: https://avatars.githubusercontent.com/u/65656077?v=4 url: https://github.com/ObliviousAI - - login: chaserowbotham - avatarUrl: https://avatars.githubusercontent.com/u/97751084?v=4 - url: https://github.com/chaserowbotham - - login: mikeckennedy - avatarUrl: https://avatars.githubusercontent.com/u/2035561?u=1bb18268bcd4d9249e1f783a063c27df9a84c05b&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/2035561?u=ce6165b799ea3164cb6f5ff54ea08042057442af&v=4 url: https://github.com/mikeckennedy + - login: ndimares + avatarUrl: https://avatars.githubusercontent.com/u/6267663?u=cfb27efde7a7212be8142abb6c058a1aeadb41b1&v=4 + url: https://github.com/ndimares - login: deta avatarUrl: https://avatars.githubusercontent.com/u/47275976?v=4 url: https://github.com/deta - login: deepset-ai avatarUrl: https://avatars.githubusercontent.com/u/51827949?v=4 url: https://github.com/deepset-ai - - login: investsuite - avatarUrl: https://avatars.githubusercontent.com/u/73833632?v=4 - url: https://github.com/investsuite + - login: databento + avatarUrl: https://avatars.githubusercontent.com/u/64141749?v=4 + url: https://github.com/databento - login: svix avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4 url: https://github.com/svix - login: VincentParedes avatarUrl: https://avatars.githubusercontent.com/u/103889729?v=4 url: https://github.com/VincentParedes -- - login: getsentry - avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4 - url: https://github.com/getsentry -- - login: vyos - avatarUrl: https://avatars.githubusercontent.com/u/5647000?v=4 - url: https://github.com/vyos - - login: SendCloud - avatarUrl: https://avatars.githubusercontent.com/u/7831959?v=4 - url: https://github.com/SendCloud +- - login: acsone + avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 + url: https://github.com/acsone - login: takashi-yoneya avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 url: https://github.com/takashi-yoneya - - login: mercedes-benz - avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 - url: https://github.com/mercedes-benz - login: xoflare avatarUrl: https://avatars.githubusercontent.com/u/74335107?v=4 url: https://github.com/xoflare - - login: Striveworks - avatarUrl: https://avatars.githubusercontent.com/u/45523576?v=4 - url: https://github.com/Striveworks + - login: marvin-robot + avatarUrl: https://avatars.githubusercontent.com/u/41086007?u=091c5cb75af363123d66f58194805a97220ee1a7&v=4 + url: https://github.com/marvin-robot - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP -- - login: InesIvanova - avatarUrl: https://avatars.githubusercontent.com/u/22920417?u=409882ec1df6dbd77455788bb383a8de223dbf6f&v=4 - url: https://github.com/InesIvanova -- - login: johnadjei - avatarUrl: https://avatars.githubusercontent.com/u/767860?v=4 - url: https://github.com/johnadjei - - login: HiredScore - avatarUrl: https://avatars.githubusercontent.com/u/3908850?v=4 - url: https://github.com/HiredScore - - login: Trivie + - login: jina-ai + avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4 + url: https://github.com/jina-ai +- - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie - - login: Lovage-Labs - avatarUrl: https://avatars.githubusercontent.com/u/71685552?v=4 - url: https://github.com/Lovage-Labs -- - login: xshapira - avatarUrl: https://avatars.githubusercontent.com/u/48856190?u=3b0927ad29addab29a43767b52e45bee5cd6da9f&v=4 - url: https://github.com/xshapira -- - login: moellenbeck - avatarUrl: https://avatars.githubusercontent.com/u/169372?v=4 - url: https://github.com/moellenbeck +- - login: birkjernstrom + avatarUrl: https://avatars.githubusercontent.com/u/281715?u=4be14b43f76b4bd497b1941309bb390250b405e6&v=4 + url: https://github.com/birkjernstrom + - login: yasyf + avatarUrl: https://avatars.githubusercontent.com/u/709645?u=f36736b3c6a85f578886ecc42a740e7b436e7a01&v=4 + url: https://github.com/yasyf - login: AccentDesign avatarUrl: https://avatars.githubusercontent.com/u/2429332?v=4 url: https://github.com/AccentDesign - - login: RodneyU215 - avatarUrl: https://avatars.githubusercontent.com/u/3329665?u=ec6a9adf8e7e8e306eed7d49687c398608d1604f&v=4 - url: https://github.com/RodneyU215 - - login: tizz98 - avatarUrl: https://avatars.githubusercontent.com/u/5739698?u=f095a3659e3a8e7c69ccd822696990b521ea25f9&v=4 - url: https://github.com/tizz98 - - login: dorianturba - avatarUrl: https://avatars.githubusercontent.com/u/9381120?u=4bfc7032a824d1ed1994aa8256dfa597c8f187ad&v=4 - url: https://github.com/dorianturba - - login: Qazzquimby - avatarUrl: https://avatars.githubusercontent.com/u/12368310?u=f4ed4a7167fd359cfe4502d56d7c64f9bf59bb38&v=4 - url: https://github.com/Qazzquimby - - login: jmaralc - avatarUrl: https://avatars.githubusercontent.com/u/21101214?u=b15a9f07b7cbf6c9dcdbcb6550bbd2c52f55aa50&v=4 - url: https://github.com/jmaralc + - login: americanair + avatarUrl: https://avatars.githubusercontent.com/u/12281813?v=4 + url: https://github.com/americanair - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries + - login: doseiai + avatarUrl: https://avatars.githubusercontent.com/u/57115726?v=4 + url: https://github.com/doseiai + - login: CanoaPBC + avatarUrl: https://avatars.githubusercontent.com/u/64223768?v=4 + url: https://github.com/CanoaPBC - - login: povilasb avatarUrl: https://avatars.githubusercontent.com/u/1213442?u=b11f58ed6ceea6e8297c9b310030478ebdac894d&v=4 url: https://github.com/povilasb - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io -- - login: guivaloz - avatarUrl: https://avatars.githubusercontent.com/u/1296621?u=bc4fc28f96c654aa2be7be051d03a315951e2491&v=4 - url: https://github.com/guivaloz - - login: indeedeng - avatarUrl: https://avatars.githubusercontent.com/u/2905043?v=4 - url: https://github.com/indeedeng - - login: fratambot - avatarUrl: https://avatars.githubusercontent.com/u/20300069?u=41c85ea08960c8a8f0ce967b780e242b1454690c&v=4 - url: https://github.com/fratambot +- - login: upciti + avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4 + url: https://github.com/upciti - - login: Kludex avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: samuelcolvin - avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=807390ba9cfe23906c3bf8a0d56aaca3cf2bfa0d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 url: https://github.com/samuelcolvin + - login: koconder + avatarUrl: https://avatars.githubusercontent.com/u/25068?u=582657b23622aaa3dfe68bd028a780f272f456fa&v=4 + url: https://github.com/koconder - login: jefftriplett avatarUrl: https://avatars.githubusercontent.com/u/50527?u=af1ddfd50f6afd6d99f333ba2ac8d0a5b245ea74&v=4 url: https://github.com/jefftriplett - - login: medecau - avatarUrl: https://avatars.githubusercontent.com/u/59870?u=f9341c95adaba780828162fd4c7442357ecfcefa&v=4 - url: https://github.com/medecau - - login: kamalgill - avatarUrl: https://avatars.githubusercontent.com/u/133923?u=0df9181d97436ce330e9acf90ab8a54b7022efe7&v=4 - url: https://github.com/kamalgill - - login: dekoza - avatarUrl: https://avatars.githubusercontent.com/u/210980?u=c03c78a8ae1039b500dfe343665536ebc51979b2&v=4 - url: https://github.com/dekoza + - login: jstanden + avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 + url: https://github.com/jstanden + - login: andreaso + avatarUrl: https://avatars.githubusercontent.com/u/285964?u=837265cc7562c0685f25b2d81cd9de0434fe107c&v=4 + url: https://github.com/andreaso - login: pamelafox avatarUrl: https://avatars.githubusercontent.com/u/297042?v=4 url: https://github.com/pamelafox - - login: deserat - avatarUrl: https://avatars.githubusercontent.com/u/299332?v=4 - url: https://github.com/deserat - login: ericof avatarUrl: https://avatars.githubusercontent.com/u/306014?u=cf7c8733620397e6584a451505581c01c5d842d7&v=4 url: https://github.com/ericof @@ -147,68 +129,44 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4 url: https://github.com/koxudaxi - login: falkben - avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 url: https://github.com/falkben - - login: jqueguiner - avatarUrl: https://avatars.githubusercontent.com/u/690878?u=bd65cc1f228ce6455e56dfaca3ef47c33bc7c3b0&v=4 - url: https://github.com/jqueguiner + - login: mintuhouse + avatarUrl: https://avatars.githubusercontent.com/u/769950?u=ecfbd79a97d33177e0d093ddb088283cf7fe8444&v=4 + url: https://github.com/mintuhouse - login: tcsmith avatarUrl: https://avatars.githubusercontent.com/u/989034?u=7d8d741552b3279e8f4d3878679823a705a46f8f&v=4 url: https://github.com/tcsmith - - login: ltieman - avatarUrl: https://avatars.githubusercontent.com/u/1084689?u=e69b17de17cb3ca141a17daa7ccbe173ceb1eb17&v=4 - url: https://github.com/ltieman - - login: mrkmcknz - avatarUrl: https://avatars.githubusercontent.com/u/1089376?u=2b9b8a8c25c33a4f6c220095638bd821cdfd13a3&v=4 - url: https://github.com/mrkmcknz - - login: theonlynexus - avatarUrl: https://avatars.githubusercontent.com/u/1515004?v=4 - url: https://github.com/theonlynexus - - login: coffeewasmyidea - avatarUrl: https://avatars.githubusercontent.com/u/1636488?u=8e32a4f200eff54dd79cd79d55d254bfce5e946d&v=4 - url: https://github.com/coffeewasmyidea - - login: corleyma - avatarUrl: https://avatars.githubusercontent.com/u/2080732?u=aed2ff652294a87d666b1c3f6dbe98104db76d26&v=4 - url: https://github.com/corleyma - - login: madisonmay - avatarUrl: https://avatars.githubusercontent.com/u/2645393?u=f22b93c6ea345a4d26a90a3834dfc7f0789fcb63&v=4 - url: https://github.com/madisonmay - - login: saivarunk - avatarUrl: https://avatars.githubusercontent.com/u/2976867?u=71f4385e781e9a9e871a52f2d4686f9a8d69ba2f&v=4 - url: https://github.com/saivarunk - - login: andre1sk - avatarUrl: https://avatars.githubusercontent.com/u/3148093?v=4 - url: https://github.com/andre1sk + - login: mickaelandrieu + avatarUrl: https://avatars.githubusercontent.com/u/1247388?u=599f6e73e452a9453f2bd91e5c3100750e731ad4&v=4 + url: https://github.com/mickaelandrieu + - login: knallgelb + avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 + url: https://github.com/knallgelb + - login: johannquerne + avatarUrl: https://avatars.githubusercontent.com/u/2736484?u=9b3381546a25679913a2b08110e4373c98840821&v=4 + url: https://github.com/johannquerne - login: Shark009 avatarUrl: https://avatars.githubusercontent.com/u/3163309?u=0c6f4091b0eda05c44c390466199826e6dc6e431&v=4 url: https://github.com/Shark009 - - login: ColliotL - avatarUrl: https://avatars.githubusercontent.com/u/3412402?u=ca64b07ecbef2f9da1cc2cac3f37522aa4814902&v=4 - url: https://github.com/ColliotL - login: dblackrun avatarUrl: https://avatars.githubusercontent.com/u/3528486?v=4 url: https://github.com/dblackrun - login: zsinx6 avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 url: https://github.com/zsinx6 - - login: MarekBleschke - avatarUrl: https://avatars.githubusercontent.com/u/3616870?v=4 - url: https://github.com/MarekBleschke + - login: kennywakeland + avatarUrl: https://avatars.githubusercontent.com/u/3631417?u=7c8f743f1ae325dfadea7c62bbf1abd6a824fc55&v=4 + url: https://github.com/kennywakeland - login: aacayaco avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 url: https://github.com/aacayaco - login: anomaly avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 url: https://github.com/anomaly - - login: peterHoburg - avatarUrl: https://avatars.githubusercontent.com/u/3860655?u=f55f47eb2d6a9b495e806ac5a044e3ae01ccc1fa&v=4 - url: https://github.com/peterHoburg - login: jgreys - avatarUrl: https://avatars.githubusercontent.com/u/4136890?u=c66ae617d614f6c886f1f1c1799d22100b3c848d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/4136890?u=096820d1ef89877d57d0f68e669ead8b0fde84df&v=4 url: https://github.com/jgreys - - login: gorhack - avatarUrl: https://avatars.githubusercontent.com/u/4141690?u=ec119ebc4bdf00a7bc84657a71aa17834f4f27f3&v=4 - url: https://github.com/gorhack - login: jaredtrog avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 url: https://github.com/jaredtrog @@ -221,162 +179,135 @@ sponsors: - login: ternaus avatarUrl: https://avatars.githubusercontent.com/u/5481618?u=fabc8d75c921b3380126adb5a931c5da6e7db04f&v=4 url: https://github.com/ternaus + - login: eseglem + avatarUrl: https://avatars.githubusercontent.com/u/5920492?u=208d419cf667b8ac594c82a8db01932c7e50d057&v=4 + url: https://github.com/eseglem - login: Yaleesa avatarUrl: https://avatars.githubusercontent.com/u/6135475?v=4 url: https://github.com/Yaleesa - login: iwpnd avatarUrl: https://avatars.githubusercontent.com/u/6152183?u=c485eefca5c6329600cae63dd35e4f5682ce6924&v=4 url: https://github.com/iwpnd + - login: FernandoCelmer + avatarUrl: https://avatars.githubusercontent.com/u/6262214?u=d29fff3fd862fda4ca752079f13f32e84c762ea4&v=4 + url: https://github.com/FernandoCelmer - login: simw avatarUrl: https://avatars.githubusercontent.com/u/6322526?v=4 url: https://github.com/simw - - login: pkucmus - avatarUrl: https://avatars.githubusercontent.com/u/6347418?u=98f5918b32e214a168a2f5d59b0b8ebdf57dca0d&v=4 - url: https://github.com/pkucmus - - login: s3ich4n - avatarUrl: https://avatars.githubusercontent.com/u/6926298?u=6690c5403bc1d9a1837886defdc5256e9a43b1db&v=4 - url: https://github.com/s3ich4n - login: Rehket avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 url: https://github.com/Rehket - login: hiancdtrsnm avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4 url: https://github.com/hiancdtrsnm - - login: Shackelford-Arden - avatarUrl: https://avatars.githubusercontent.com/u/7362263?v=4 - url: https://github.com/Shackelford-Arden - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow - - login: jacobkrit - avatarUrl: https://avatars.githubusercontent.com/u/11823915?u=4921a7ea429b7eadcad3077f119f90d15a3318b2&v=4 - url: https://github.com/jacobkrit - - login: svats2k - avatarUrl: https://avatars.githubusercontent.com/u/12378398?u=ecf28c19f61052e664bdfeb2391f8107d137915c&v=4 - url: https://github.com/svats2k - - login: gokulyc - avatarUrl: https://avatars.githubusercontent.com/u/13468848?u=269f269d3e70407b5fb80138c52daba7af783997&v=4 - url: https://github.com/gokulyc + - login: drcat101 + avatarUrl: https://avatars.githubusercontent.com/u/11951946?u=e714b957185b8cf3d301cced7fc3ad2842122c6a&v=4 + url: https://github.com/drcat101 + - login: jsoques + avatarUrl: https://avatars.githubusercontent.com/u/12414216?u=620921d94196546cc8b9eae2cc4cbc3f95bab42f&v=4 + url: https://github.com/jsoques + - login: joeds13 + avatarUrl: https://avatars.githubusercontent.com/u/13631604?u=628eb122e08bef43767b3738752b883e8e7f6259&v=4 + url: https://github.com/joeds13 - login: dannywade avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 url: https://github.com/dannywade - login: khadrawy avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4 url: https://github.com/khadrawy - - login: pablonnaoji - avatarUrl: https://avatars.githubusercontent.com/u/15187159?u=afc15bd5a4ba9c5c7206bbb1bcaeef606a0932e0&v=4 - url: https://github.com/pablonnaoji + - login: mjohnsey + avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 + url: https://github.com/mjohnsey - login: wedwardbeck avatarUrl: https://avatars.githubusercontent.com/u/19333237?u=1de4ae2bf8d59eb4c013f21d863cbe0f2010575f&v=4 url: https://github.com/wedwardbeck + - login: RaamEEIL + avatarUrl: https://avatars.githubusercontent.com/u/20320552?v=4 + url: https://github.com/RaamEEIL - login: Filimoa avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=0be845711495bbd7b756e13fcaeb8efc1ebd78ba&v=4 url: https://github.com/Filimoa - - login: shuheng-liu - avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 - url: https://github.com/shuheng-liu - - login: Joeriksson - avatarUrl: https://avatars.githubusercontent.com/u/25037079?v=4 - url: https://github.com/Joeriksson - - login: cometa-haley + - login: ehaca avatarUrl: https://avatars.githubusercontent.com/u/25950317?u=cec1a3e0643b785288ae8260cc295a85ab344995&v=4 - url: https://github.com/cometa-haley - - login: LarryGF - avatarUrl: https://avatars.githubusercontent.com/u/26148349?u=431bb34d36d41c172466252242175281ae132152&v=4 - url: https://github.com/LarryGF - - login: veprimk - avatarUrl: https://avatars.githubusercontent.com/u/29689749?u=f8cb5a15a286e522e5b189bc572d5a1a90217fb2&v=4 - url: https://github.com/veprimk + url: https://github.com/ehaca + - login: timlrx + avatarUrl: https://avatars.githubusercontent.com/u/28362229?u=9a745ca31372ee324af682715ae88ce8522f9094&v=4 + url: https://github.com/timlrx - login: BrettskiPy avatarUrl: https://avatars.githubusercontent.com/u/30988215?u=d8a94a67e140d5ee5427724b292cc52d8827087a&v=4 url: https://github.com/BrettskiPy - - login: mauroalejandrojm - avatarUrl: https://avatars.githubusercontent.com/u/31569442?u=cdada990a1527926a36e95f62c30a8b48bbc49a1&v=4 - url: https://github.com/mauroalejandrojm - login: Leay15 avatarUrl: https://avatars.githubusercontent.com/u/32212558?u=c4aa9c1737e515959382a5515381757b1fd86c53&v=4 url: https://github.com/Leay15 - login: ygorpontelo avatarUrl: https://avatars.githubusercontent.com/u/32963605?u=35f7103f9c4c4c2589ae5737ee882e9375ef072e&v=4 url: https://github.com/ygorpontelo - - login: AlrasheedA - avatarUrl: https://avatars.githubusercontent.com/u/33544979?u=7fe66bf62b47682612b222e3e8f4795ef3be769b&v=4 - url: https://github.com/AlrasheedA - login: ProteinQure avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 url: https://github.com/ProteinQure - - login: ybressler - avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4 - url: https://github.com/ybressler + - login: RafaelWO + avatarUrl: https://avatars.githubusercontent.com/u/38643099?u=56c676f024667ee416dc8b1cdf0c2611b9dc994f&v=4 + url: https://github.com/RafaelWO + - login: arleybri18 + avatarUrl: https://avatars.githubusercontent.com/u/39681546?u=5c028f81324b0e8c73b3c15bc4e7b0218d2ba0c3&v=4 + url: https://github.com/arleybri18 + - login: thenickben + avatarUrl: https://avatars.githubusercontent.com/u/40610922?u=1e907d904041b7c91213951a3cb344cd37c14aaf&v=4 + url: https://github.com/thenickben - login: ddilidili avatarUrl: https://avatars.githubusercontent.com/u/42176885?u=c0a849dde06987434653197b5f638d3deb55fc6c&v=4 url: https://github.com/ddilidili - - login: VictorCalderon - avatarUrl: https://avatars.githubusercontent.com/u/44529243?u=cea69884f826a29aff1415493405209e0706d07a&v=4 - url: https://github.com/VictorCalderon - - login: arthuRHD - avatarUrl: https://avatars.githubusercontent.com/u/48015496?u=05a0d5b8b9320eeb7990d35c9337b823f269d2ff&v=4 - url: https://github.com/arthuRHD - - login: rafsaf - avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 - url: https://github.com/rafsaf + - login: ramonalmeidam + avatarUrl: https://avatars.githubusercontent.com/u/45269580?u=3358750b3a5854d7c3ed77aaca7dd20a0f529d32&v=4 + url: https://github.com/ramonalmeidam - login: dudikbender - avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=494f85229115076121b3639a3806bbac1c6ae7f6&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4 url: https://github.com/dudikbender - - login: dazeddd - avatarUrl: https://avatars.githubusercontent.com/u/59472056?u=7a1b668449bf8b448db13e4c575576d24d7d658b&v=4 - url: https://github.com/dazeddd - - login: A-Edge - avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 - url: https://github.com/A-Edge + - login: Amirshox + avatarUrl: https://avatars.githubusercontent.com/u/56707784?u=2a2f8cc243d6f5b29cd63fd2772f7a97aadc6c6b&v=4 + url: https://github.com/Amirshox + - login: prodhype + avatarUrl: https://avatars.githubusercontent.com/u/60444672?u=3f278cff25ea37ead487d7861d4a984795de819e&v=4 + url: https://github.com/prodhype - login: yakkonaut avatarUrl: https://avatars.githubusercontent.com/u/60633704?u=90a71fd631aa998ba4a96480788f017c9904e07b&v=4 url: https://github.com/yakkonaut - login: patsatsia avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 url: https://github.com/patsatsia - - login: predictionmachine - avatarUrl: https://avatars.githubusercontent.com/u/63719559?v=4 - url: https://github.com/predictionmachine - - login: minsau - avatarUrl: https://avatars.githubusercontent.com/u/64386242?u=7e45f24b2958caf946fa3546ea33bacf5cd886f8&v=4 - url: https://github.com/minsau - - login: daverin - avatarUrl: https://avatars.githubusercontent.com/u/70378377?u=6d1814195c0de7162820eaad95a25b423a3869c0&v=4 - url: https://github.com/daverin - login: anthonycepeda avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=4252c6b6dc5024af502a823a3ac5e7a03a69963f&v=4 url: https://github.com/anthonycepeda + - login: patricioperezv + avatarUrl: https://avatars.githubusercontent.com/u/73832292?u=5f471f156e19ee7920e62ae0f4a47b95580e61cf&v=4 + url: https://github.com/patricioperezv + - login: kaoru0310 + avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 + url: https://github.com/kaoru0310 - login: DelfinaCare avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 url: https://github.com/DelfinaCare - - login: programvx - avatarUrl: https://avatars.githubusercontent.com/u/96057906?v=4 - url: https://github.com/programvx - - login: pyt3h - avatarUrl: https://avatars.githubusercontent.com/u/99658549?v=4 - url: https://github.com/pyt3h - - login: Dagmaara - avatarUrl: https://avatars.githubusercontent.com/u/115501964?v=4 - url: https://github.com/Dagmaara -- - login: linux-china - avatarUrl: https://avatars.githubusercontent.com/u/46711?u=cd77c65338b158750eb84dc7ff1acf3209ccfc4f&v=4 - url: https://github.com/linux-china + - login: osawa-koki + avatarUrl: https://avatars.githubusercontent.com/u/94336223?u=59c6fe6945bcbbaff87b2a794238671b060620d2&v=4 + url: https://github.com/osawa-koki + - login: apitally + avatarUrl: https://avatars.githubusercontent.com/u/138365043?v=4 + url: https://github.com/apitally +- - login: getsentry + avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4 + url: https://github.com/getsentry +- - login: pawamoy + avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 + url: https://github.com/pawamoy - login: ddanier avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 url: https://github.com/ddanier - - login: jhb - avatarUrl: https://avatars.githubusercontent.com/u/142217?v=4 - url: https://github.com/jhb - - login: justinrmiller - avatarUrl: https://avatars.githubusercontent.com/u/143998?u=b507a940394d4fc2bc1c27cea2ca9c22538874bd&v=4 - url: https://github.com/justinrmiller - login: bryanculbertson avatarUrl: https://avatars.githubusercontent.com/u/144028?u=defda4f90e93429221cc667500944abde60ebe4a&v=4 url: https://github.com/bryanculbertson - - login: hhatto - avatarUrl: https://avatars.githubusercontent.com/u/150309?u=3e8f63c27bf996bfc68464b0ce3f7a3e40e6ea7f&v=4 - url: https://github.com/hhatto - login: slafs avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 url: https://github.com/slafs @@ -392,86 +323,77 @@ sponsors: - login: securancy avatarUrl: https://avatars.githubusercontent.com/u/606673?v=4 url: https://github.com/securancy - - login: hardbyte - avatarUrl: https://avatars.githubusercontent.com/u/855189?u=aa29e92f34708814d6b67fcd47ca4cf2ce1c04ed&v=4 - url: https://github.com/hardbyte + - login: natehouk + avatarUrl: https://avatars.githubusercontent.com/u/805439?u=d8e4be629dc5d7efae7146157e41ee0bd129d9bc&v=4 + url: https://github.com/natehouk - login: browniebroke avatarUrl: https://avatars.githubusercontent.com/u/861044?u=5abfca5588f3e906b31583d7ee62f6de4b68aa24&v=4 url: https://github.com/browniebroke - - login: janfilips - avatarUrl: https://avatars.githubusercontent.com/u/870699?u=50de77b93d3a0b06887e672d4e8c7b9d643085aa&v=4 - url: https://github.com/janfilips - - login: woodrad - avatarUrl: https://avatars.githubusercontent.com/u/1410765?u=86707076bb03d143b3b11afc1743d2aa496bd8bf&v=4 - url: https://github.com/woodrad - - login: Pytlicek - avatarUrl: https://avatars.githubusercontent.com/u/1430522?u=01b1f2f7671ce3131e0877d08e2e3f8bdbb0a38a&v=4 - url: https://github.com/Pytlicek - - login: allen0125 - avatarUrl: https://avatars.githubusercontent.com/u/1448456?u=dc2ad819497eef494b88688a1796e0adb87e7cae&v=4 - url: https://github.com/allen0125 + - login: dodo5522 + avatarUrl: https://avatars.githubusercontent.com/u/1362607?u=9bf1e0e520cccc547c046610c468ce6115bbcf9f&v=4 + url: https://github.com/dodo5522 + - login: miguelgr + avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 + url: https://github.com/miguelgr - login: WillHogan avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4 url: https://github.com/WillHogan - login: my3 avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 url: https://github.com/my3 + - login: leobiscassi + avatarUrl: https://avatars.githubusercontent.com/u/1977418?u=f9f82445a847ab479bd7223debd677fcac6c49a0&v=4 + url: https://github.com/leobiscassi - login: cbonoz avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4 url: https://github.com/cbonoz - - login: Debakel - avatarUrl: https://avatars.githubusercontent.com/u/2857237?u=07df6d11c8feef9306d071cb1c1005a2dd596585&v=4 - url: https://github.com/Debakel - - login: paul121 - avatarUrl: https://avatars.githubusercontent.com/u/3116995?u=6e2d8691cc345e63ee02e4eb4d7cef82b1fcbedc&v=4 - url: https://github.com/paul121 - - login: igorcorrea - avatarUrl: https://avatars.githubusercontent.com/u/3438238?u=c57605077c31a8f7b2341fc4912507f91b4a5621&v=4 - url: https://github.com/igorcorrea - - login: larsvik - avatarUrl: https://avatars.githubusercontent.com/u/3442226?v=4 - url: https://github.com/larsvik - login: anthonycorletti avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 url: https://github.com/anthonycorletti - - login: jonathanhle - avatarUrl: https://avatars.githubusercontent.com/u/3851599?u=76b9c5d2fecd6c3a16e7645231878c4507380d4d&v=4 - url: https://github.com/jonathanhle - - login: pawamoy - avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 - url: https://github.com/pawamoy - - login: nikeee - avatarUrl: https://avatars.githubusercontent.com/u/4068864?u=63f8eee593f25138e0f1032ef442e9ad24907d4c&v=4 - url: https://github.com/nikeee - login: Alisa-lisa avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 url: https://github.com/Alisa-lisa + - login: gowikel + avatarUrl: https://avatars.githubusercontent.com/u/4339072?u=0e325ffcc539c38f89d9aa876bd87f9ec06ce0ee&v=4 + url: https://github.com/gowikel - login: danielunderwood avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 url: https://github.com/danielunderwood + - login: yuawn + avatarUrl: https://avatars.githubusercontent.com/u/5111198?u=5315576f3fe1a70fd2d0f02181588f4eea5d353d&v=4 + url: https://github.com/yuawn + - login: sdevkota + avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 + url: https://github.com/sdevkota - login: unredundant avatarUrl: https://avatars.githubusercontent.com/u/5607577?u=1ffbf39f5bb8736b75c0d235707d6e8f803725c5&v=4 url: https://github.com/unredundant - login: Baghdady92 avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 url: https://github.com/Baghdady92 - - login: holec - avatarUrl: https://avatars.githubusercontent.com/u/6438041?u=f5af71ec85b3a9d7b8139cb5af0512b02fa9ab1e&v=4 - url: https://github.com/holec - - login: mattwelke - avatarUrl: https://avatars.githubusercontent.com/u/7719209?u=5d963ead289969257190b133250653bd99df06ba&v=4 - url: https://github.com/mattwelke + - login: jakeecolution + avatarUrl: https://avatars.githubusercontent.com/u/5884696?u=4a7c7883fb064b593b50cb6697b54687e6f7aafe&v=4 + url: https://github.com/jakeecolution + - login: KentShikama + avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4 + url: https://github.com/KentShikama + - login: katnoria + avatarUrl: https://avatars.githubusercontent.com/u/7674948?u=09767eb13e07e09496c5fee4e5ce21d9eac34a56&v=4 + url: https://github.com/katnoria + - login: harsh183 + avatarUrl: https://avatars.githubusercontent.com/u/7780198?v=4 + url: https://github.com/harsh183 - login: hcristea avatarUrl: https://avatars.githubusercontent.com/u/7814406?u=61d7a4fcf846983a4606788eac25e1c6c1209ba8&v=4 url: https://github.com/hcristea - login: moonape1226 avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 url: https://github.com/moonape1226 - - login: yenchenLiu - avatarUrl: https://avatars.githubusercontent.com/u/9199638?u=8cdf5ae507448430d90f6f3518d1665a23afe99b&v=4 - url: https://github.com/yenchenLiu + - login: albertkun + avatarUrl: https://avatars.githubusercontent.com/u/8574425?u=aad2a9674273c9275fe414d99269b7418d144089&v=4 + url: https://github.com/albertkun - login: xncbf - avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=866a1311e4bd3ec5ae84185c4fcc99f397c883d7&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=ee91e210ae93b9cdd8f248b21cb028316cc0b747&v=4 url: https://github.com/xncbf - login: DMantis avatarUrl: https://avatars.githubusercontent.com/u/9536869?v=4 @@ -479,6 +401,9 @@ sponsors: - login: hard-coders avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders + - login: supdann + avatarUrl: https://avatars.githubusercontent.com/u/9986994?u=9671810f4ae9504c063227fee34fd47567ff6954&v=4 + url: https://github.com/supdann - login: satwikkansal avatarUrl: https://avatars.githubusercontent.com/u/10217535?u=b12d6ef74ea297de9e46da6933b1a5b7ba9e6a61&v=4 url: https://github.com/satwikkansal @@ -488,48 +413,57 @@ sponsors: - login: pheanex avatarUrl: https://avatars.githubusercontent.com/u/10408624?u=5b6bab6ee174aa6e991333e06eb29f628741013d&v=4 url: https://github.com/pheanex + - login: dzoladz + avatarUrl: https://avatars.githubusercontent.com/u/10561752?u=5ee314d54aa79592c18566827ad8914debd5630d&v=4 + url: https://github.com/dzoladz - login: JimFawkes avatarUrl: https://avatars.githubusercontent.com/u/12075115?u=dc58ecfd064d72887c34bf500ddfd52592509acd&v=4 url: https://github.com/JimFawkes + - login: artempronevskiy + avatarUrl: https://avatars.githubusercontent.com/u/12235104?u=03df6e1e55c9c6fe5d230adabb8dd7d43d8bbe8f&v=4 + url: https://github.com/artempronevskiy - login: giuliano-oliveira avatarUrl: https://avatars.githubusercontent.com/u/13181797?u=0ef2dfbf7fc9a9726d45c21d32b5d1038a174870&v=4 url: https://github.com/giuliano-oliveira - - login: logan-connolly - avatarUrl: https://avatars.githubusercontent.com/u/16244943?u=8ae66dfbba936463cc8aa0dd7a6d2b4c0cc757eb&v=4 - url: https://github.com/logan-connolly - - login: cdsre - avatarUrl: https://avatars.githubusercontent.com/u/16945936?v=4 - url: https://github.com/cdsre + - login: TheR1D + avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4 + url: https://github.com/TheR1D + - login: joshuatz + avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 + url: https://github.com/joshuatz - login: jangia avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 url: https://github.com/jangia - - login: paulowiz - avatarUrl: https://avatars.githubusercontent.com/u/18649504?u=d8a6ac40321f2bded0eba78b637751c7f86c6823&v=4 - url: https://github.com/paulowiz - - login: yannicschroeer - avatarUrl: https://avatars.githubusercontent.com/u/22749683?u=91515328b5418a4e7289a83f0dcec3573f1a6500&v=4 - url: https://github.com/yannicschroeer - - login: ghandic - avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 - url: https://github.com/ghandic - - login: fstau - avatarUrl: https://avatars.githubusercontent.com/u/24669867?u=60e7c8c09f8dafabee8fc3edcd6f9e19abbff918&v=4 - url: https://github.com/fstau + - login: shuheng-liu + avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 + url: https://github.com/shuheng-liu + - login: salahelfarissi + avatarUrl: https://avatars.githubusercontent.com/u/23387408?u=73222a4be627c1a3dee9736e0da22224eccdc8f6&v=4 + url: https://github.com/salahelfarissi - login: pers0n4 - avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=7e5d2bf26d0a0670ea347f7db5febebc4e031ed1&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4 url: https://github.com/pers0n4 + - login: kxzk + avatarUrl: https://avatars.githubusercontent.com/u/25046261?u=e185e58080090f9e678192cd214a14b14a2b232b&v=4 + url: https://github.com/kxzk - login: SebTota avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 url: https://github.com/SebTota + - login: nisutec + avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 + url: https://github.com/nisutec + - login: hoenie-ams + avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 + url: https://github.com/hoenie-ams - login: joerambo avatarUrl: https://avatars.githubusercontent.com/u/26282974?v=4 url: https://github.com/joerambo - - login: mertguvencli - avatarUrl: https://avatars.githubusercontent.com/u/29762151?u=16a906d90df96c8cff9ea131a575c4bc171b1523&v=4 - url: https://github.com/mertguvencli - - login: ruizdiazever - avatarUrl: https://avatars.githubusercontent.com/u/29817086?u=2df54af55663d246e3a4dc8273711c37f1adb117&v=4 - url: https://github.com/ruizdiazever + - login: rlnchow + avatarUrl: https://avatars.githubusercontent.com/u/28018479?u=a93ca9cf1422b9ece155784a72d5f2fdbce7adff&v=4 + url: https://github.com/rlnchow + - login: White-Mask + avatarUrl: https://avatars.githubusercontent.com/u/31826970?u=8625355dc25ddf9c85a8b2b0b9932826c4c8f44c&v=4 + url: https://github.com/White-Mask - login: HosamAlmoghraby avatarUrl: https://avatars.githubusercontent.com/u/32025281?u=aa1b09feabccbf9dc506b81c71155f32d126cefa&v=4 url: https://github.com/HosamAlmoghraby @@ -537,77 +471,68 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 - login: bnkc - avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=20f362505e2a994805233f42e69f9f14b4a9dd0c&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=fa1dc8db3e920cf5c5636b97180a6f811fa01aaf&v=4 url: https://github.com/bnkc - login: declon avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 url: https://github.com/declon - - login: alvarobartt - avatarUrl: https://avatars.githubusercontent.com/u/36760800?u=9b38695807eb981d452989699ff72ec2d8f6508e&v=4 - url: https://github.com/alvarobartt - - login: d-e-h-i-o - avatarUrl: https://avatars.githubusercontent.com/u/36816716?v=4 - url: https://github.com/d-e-h-i-o - - login: ww-daniel-mora - avatarUrl: https://avatars.githubusercontent.com/u/38921751?u=ae14bc1e40f2dd5a9c5741fc0b0dffbd416a5fa9&v=4 - url: https://github.com/ww-daniel-mora - - login: rwxd - avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=b3cb7a606207141c357e517071cf91a67fb5577e&v=4 - url: https://github.com/rwxd - - login: ilias-ant - avatarUrl: https://avatars.githubusercontent.com/u/42189572?u=a2d6121bac4d125d92ec207460fa3f1842d37e66&v=4 - url: https://github.com/ilias-ant + - login: curegit + avatarUrl: https://avatars.githubusercontent.com/u/37978051?u=1733c322079118c0cdc573c03d92813f50a9faec&v=4 + url: https://github.com/curegit + - login: kristiangronberg + avatarUrl: https://avatars.githubusercontent.com/u/42678548?v=4 + url: https://github.com/kristiangronberg - login: arrrrrmin - avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=2a812c1a2ec58227ed01778837f255143de9df97&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=36a3880a6eb29309c19e6cadbb173bafbe91deb1&v=4 url: https://github.com/arrrrrmin - - login: MauriceKuenicke - avatarUrl: https://avatars.githubusercontent.com/u/47433175?u=37455bc95c7851db296ac42626f0cacb77ca2443&v=4 - url: https://github.com/MauriceKuenicke + - login: ArtyomVancyan + avatarUrl: https://avatars.githubusercontent.com/u/44609997?v=4 + url: https://github.com/ArtyomVancyan - login: hgalytoby avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=f4888c2c54929bd86eed0d3971d09fcb306e5088&v=4 url: https://github.com/hgalytoby - - login: akanz1 - avatarUrl: https://avatars.githubusercontent.com/u/51492342?u=2280f57134118714645e16b535c1a37adf6b369b&v=4 - url: https://github.com/akanz1 - - login: shidenko97 - avatarUrl: https://avatars.githubusercontent.com/u/54946990?u=3fdc0caea36af9217dacf1cc7760c7ed9d67dcfe&v=4 - url: https://github.com/shidenko97 - - login: data-djinn - avatarUrl: https://avatars.githubusercontent.com/u/56449985?u=42146e140806908d49bd59ccc96f222abf587886&v=4 - url: https://github.com/data-djinn - - login: leo-jp-edwards - avatarUrl: https://avatars.githubusercontent.com/u/58213433?u=2c128e8b0794b7a66211cd7d8ebe05db20b7e9c0&v=4 - url: https://github.com/leo-jp-edwards - - login: apar-tiwari - avatarUrl: https://avatars.githubusercontent.com/u/61064197?v=4 - url: https://github.com/apar-tiwari - - login: Vyvy-vi - avatarUrl: https://avatars.githubusercontent.com/u/62864373?u=1a9b0b28779abc2bc9b62cb4d2e44d453973c9c3&v=4 - url: https://github.com/Vyvy-vi + - login: conservative-dude + avatarUrl: https://avatars.githubusercontent.com/u/55538308?u=f250c44942ea6e73a6bd90739b381c470c192c11&v=4 + url: https://github.com/conservative-dude + - login: Calesi19 + avatarUrl: https://avatars.githubusercontent.com/u/58052598?u=273d4fc364c004602c93dd6adeaf5cc915b93cd2&v=4 + url: https://github.com/Calesi19 - login: 0417taehyun avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun - - login: realabja - avatarUrl: https://avatars.githubusercontent.com/u/66185192?u=001e2dd9297784f4218997981b4e6fa8357bb70b&v=4 - url: https://github.com/realabja - - login: pondDevThai - avatarUrl: https://avatars.githubusercontent.com/u/71592181?u=08af9a59bccfd8f6b101de1005aa9822007d0a44&v=4 - url: https://github.com/pondDevThai - - login: zeb0x01 - avatarUrl: https://avatars.githubusercontent.com/u/77236545?u=c62bfcfbd463f9cf171c879cea1362a63de2c582&v=4 - url: https://github.com/zeb0x01 - - login: lironmiz - avatarUrl: https://avatars.githubusercontent.com/u/91504420?u=cb93dfec613911ac8d4e84fbb560711546711ad5&v=4 - url: https://github.com/lironmiz -- - login: gabrielmbmb - avatarUrl: https://avatars.githubusercontent.com/u/29572918?u=6d1e00b5d558e96718312ff910a2318f47cc3145&v=4 - url: https://github.com/gabrielmbmb + - login: fernandosmither + avatarUrl: https://avatars.githubusercontent.com/u/66154723?u=a76a037b5d674938a75d2cff862fb6dfd63ec214&v=4 + url: https://github.com/fernandosmither + - login: romabozhanovgithub + avatarUrl: https://avatars.githubusercontent.com/u/67696229?u=e4b921eef096415300425aca249348f8abb78ad7&v=4 + url: https://github.com/romabozhanovgithub + - login: mbukeRepo + avatarUrl: https://avatars.githubusercontent.com/u/70356088?u=d2eb23e2b222a3b316c4183b05a3236b32819dc2&v=4 + url: https://github.com/mbukeRepo + - login: adriiamontoto + avatarUrl: https://avatars.githubusercontent.com/u/75563346?u=eeb1350b82ecb4d96592f9b6cd1a16870c355e38&v=4 + url: https://github.com/adriiamontoto + - login: PelicanQ + avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 + url: https://github.com/PelicanQ + - login: tahmarrrr23 + avatarUrl: https://avatars.githubusercontent.com/u/138208610?u=465a46b0ff72a74252d3e3a71ac7d2f1919cda28&v=4 + url: https://github.com/tahmarrrr23 +- - login: ssbarnea + avatarUrl: https://avatars.githubusercontent.com/u/102495?u=b4bf6818deefe59952ac22fec6ed8c76de1b8f7c&v=4 + url: https://github.com/ssbarnea + - login: Patechoc + avatarUrl: https://avatars.githubusercontent.com/u/2376641?u=23b49e9eda04f078cb74fa3f93593aa6a57bb138&v=4 + url: https://github.com/Patechoc + - login: DazzyMlv + avatarUrl: https://avatars.githubusercontent.com/u/23006212?u=df429da52882b0432e5ac81d4f1b489abc86433c&v=4 + url: https://github.com/DazzyMlv + - login: sadikkuzu + avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=d179c06bb9f65c4167fcab118526819f8e0dac17&v=4 + url: https://github.com/sadikkuzu - login: danburonline - avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=94935cccfbec58083ab1e535212d54f1bf2c978a&v=4 url: https://github.com/danburonline - - login: Moises6669 - avatarUrl: https://avatars.githubusercontent.com/u/66188523?u=96af25b8d5be9f983cb96e9dd7c605c716caf1f5&v=4 - url: https://github.com/Moises6669 - - login: lyuboparvanov - avatarUrl: https://avatars.githubusercontent.com/u/106192895?u=367329c777320e01550afda9d8de670436181d86&v=4 - url: https://github.com/lyuboparvanov + - login: rwxd + avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 + url: https://github.com/rwxd diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index d46ec44ae4..2877e7938c 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,227 +1,223 @@ maintainers: - login: tiangolo - answers: 1878 - prs: 361 + answers: 1870 + prs: 523 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 379 + count: 522 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu - count: 262 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 + count: 241 + avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu -- login: ycd - count: 221 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 - url: https://github.com/ycd - login: Mause - count: 207 + count: 220 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause +- login: ycd + count: 217 + avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 + url: https://github.com/ycd +- login: jgould22 + count: 205 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 - login: JarroVGIT - count: 192 + count: 193 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: euri10 - count: 166 + count: 153 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -- login: phy25 - count: 130 - avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 - url: https://github.com/phy25 - login: iudeen - count: 103 + count: 127 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen +- login: phy25 + count: 126 + avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 + url: https://github.com/phy25 - login: raphaelauv - count: 77 + count: 83 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv - login: ArcLightSlavik count: 71 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik -- login: jgould22 - count: 68 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 +- login: ghandic + count: 71 + avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 + url: https://github.com/ghandic - login: falkben - count: 59 - avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 + count: 57 + avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 url: https://github.com/falkben - login: sm-Fifteen - count: 50 + count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen +- login: yinziyan1206 + count: 48 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 - login: insomnes - count: 46 + count: 45 avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 url: https://github.com/insomnes +- login: adriangb + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 + url: https://github.com/adriangb +- login: acidjunk + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: Dustyposa count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa -- login: adriangb - count: 41 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=1e2c2c9b39f5c9b780fb933d8995cf08ec235a47&v=4 - url: https://github.com/adriangb +- login: odiseo0 + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 + url: https://github.com/odiseo0 +- login: frankie567 + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=c159fe047727aedecbbeeaa96a1b03ceb9d39add&v=4 + url: https://github.com/frankie567 - login: includeamin - count: 39 + count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin +- login: n8sty + count: 40 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty +- login: chbndrhnns + count: 38 + avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 + url: https://github.com/chbndrhnns - login: STeveShary count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary -- login: chbndrhnns - count: 36 - avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 - url: https://github.com/chbndrhnns -- login: frankie567 - count: 34 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 - url: https://github.com/frankie567 -- login: prostomarkeloff - count: 33 - avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 - url: https://github.com/prostomarkeloff -- login: acidjunk - count: 32 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk - login: krishnardt - count: 31 + count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt - login: panla - count: 30 + count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 url: https://github.com/panla -- login: wshayes - count: 29 - avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 - url: https://github.com/wshayes -- login: ghandic - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 - url: https://github.com/ghandic +- login: JavierSanchezCastro + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro +- login: prostomarkeloff + count: 28 + avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 + url: https://github.com/prostomarkeloff - login: dbanty - count: 25 + count: 26 avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4 url: https://github.com/dbanty -- login: yinziyan1206 +- login: wshayes count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 + avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 + url: https://github.com/wshayes +- login: acnebs + count: 23 + avatarUrl: https://avatars.githubusercontent.com/u/9054108?v=4 + url: https://github.com/acnebs - login: SirTelemak - count: 24 + count: 23 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak -- login: odiseo0 - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 - url: https://github.com/odiseo0 -- login: acnebs - count: 22 - avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 - url: https://github.com/acnebs +- login: rafsaf + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=5fe59a56e1f2f9ccd8005d71752a8276f133ae1a&v=4 + url: https://github.com/rafsaf +- login: nymous + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 + url: https://github.com/nymous - login: nsidnev - count: 22 + count: 20 avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 url: https://github.com/nsidnev - login: chris-allnutt - count: 21 + count: 20 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt +- login: chrisK824 + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 + url: https://github.com/chrisK824 +- login: ebottos94 + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 + url: https://github.com/ebottos94 - login: retnikt - count: 19 + count: 18 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt -- login: rafsaf - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 - url: https://github.com/rafsaf -- login: Hultner +- login: zoliknemet count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 + url: https://github.com/zoliknemet +- login: Hultner + count: 17 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 url: https://github.com/Hultner -- login: jorgerpo - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 - url: https://github.com/jorgerpo -- login: nkhitrov - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4 - url: https://github.com/nkhitrov - login: harunyasar count: 17 avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 url: https://github.com/harunyasar -- login: waynerv - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 - url: https://github.com/waynerv +- login: nkhitrov + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4 + url: https://github.com/nkhitrov +- login: caeser1996 + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 + url: https://github.com/caeser1996 - login: dstlny count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny -- login: hellocoldworld - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4 - url: https://github.com/hellocoldworld - login: jonatasoli - count: 15 + count: 16 avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 url: https://github.com/jonatasoli -- login: mbroton +- login: ghost count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 - url: https://github.com/mbroton -- login: haizaar - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/58201?u=dd40d99a3e1935d0b768f122bfe2258d6ea53b2b&v=4 - url: https://github.com/haizaar -- login: n8sty - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty -- login: valentin994 - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/42819267?u=fdeeaa9242a59b243f8603496b00994f6951d5a2&v=4 - url: https://github.com/valentin994 -- login: David-Lor - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 - url: https://github.com/David-Lor + avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 + url: https://github.com/ghost last_month_active: -- login: iudeen - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen +- login: Ventura94 + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/43103937?u=ccb837005aaf212a449c374618c4339089e2f733&v=4 + url: https://github.com/Ventura94 +- login: JavierSanchezCastro + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro - login: jgould22 - count: 13 + count: 3 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 -- login: NewSouthMjos - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/77476573?v=4 - url: https://github.com/NewSouthMjos -- login: davismartens - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/69799848?u=dbdfd256dd4e0a12d93efb3463225f3e39d8df6f&v=4 - url: https://github.com/davismartens - login: Kludex count: 3 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex -- login: Lenclove +- login: n8sty count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/32355298?u=d0065e01650c63c2b2413f42d983634b2ea85481&v=4 - url: https://github.com/Lenclove + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty top_contributors: - login: waynerv count: 25 @@ -231,18 +227,22 @@ top_contributors: count: 22 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi +- login: Kludex + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + url: https://github.com/Kludex - login: jaystone776 - count: 17 + count: 18 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 url: https://github.com/jaystone776 - login: dmontagu - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu -- login: Kludex - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex +- login: Xewus + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 @@ -252,13 +252,17 @@ top_contributors: avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 url: https://github.com/mariacamilagl - login: Smlep - count: 10 + count: 11 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 url: https://github.com/Smlep - login: Serrones count: 8 avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 url: https://github.com/Serrones +- login: rjNemo + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 + url: https://github.com/rjNemo - login: RunningIkkyu count: 7 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 @@ -267,14 +271,26 @@ top_contributors: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders -- login: rjNemo +- login: Alexandrhub count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo + avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 + url: https://github.com/Alexandrhub +- login: NinaHwang + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4 + url: https://github.com/NinaHwang +- login: batlopes + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 + url: https://github.com/batlopes - login: wshayes count: 5 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes +- login: samuelcolvin + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 + url: https://github.com/samuelcolvin - login: SwftAlpc count: 5 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 @@ -287,18 +303,14 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp -- login: batlopes +- login: tamtam-fitness count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 - url: https://github.com/batlopes + avatarUrl: https://avatars.githubusercontent.com/u/62091034?u=8da19a6bd3d02f5d6ba30c7247d5b46c98dd1403&v=4 + url: https://github.com/tamtam-fitness - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 url: https://github.com/jekirl -- login: samuelcolvin - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=807390ba9cfe23906c3bf8a0d56aaca3cf2bfa0d&v=4 - url: https://github.com/samuelcolvin - login: jfunez count: 4 avatarUrl: https://avatars.githubusercontent.com/u/805749?v=4 @@ -315,31 +327,59 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/3360631?u=5fa1f475ad784d64eb9666bdd43cc4d285dcc773&v=4 url: https://github.com/hitrust +- login: JulianMaurin + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/63545168?u=b7d15ac865268cbefc2d739e2f23d9aeeac1a622&v=4 + url: https://github.com/JulianMaurin - login: lsglucas count: 4 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas -- login: NinaHwang +- login: adriangb count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 - url: https://github.com/NinaHwang + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 + url: https://github.com/adriangb +- login: iudeen + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen +- login: axel584 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 + url: https://github.com/axel584 +- login: ivan-abc + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 + url: https://github.com/ivan-abc +- login: rostik1410 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/11443899?u=e26a635c2ba220467b308a326a579b8ccf4a8701&v=4 + url: https://github.com/rostik1410 top_reviewers: - login: Kludex - count: 109 + count: 145 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan - count: 72 + count: 86 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan - login: yezz123 - count: 66 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 + count: 82 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4 url: https://github.com/yezz123 +- login: iudeen + count: 55 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: tokusumi count: 51 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi +- login: Xewus + count: 50 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: waynerv count: 47 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 @@ -352,10 +392,6 @@ top_reviewers: count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd -- login: iudeen - count: 42 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen - login: cikay count: 41 avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 @@ -372,34 +408,46 @@ top_reviewers: count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik +- login: cassiobotaro + count: 28 + avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 + url: https://github.com/cassiobotaro +- login: lsglucas + count: 27 + avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 + url: https://github.com/lsglucas - login: komtaki count: 27 avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki -- login: cassiobotaro - count: 26 - avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 - url: https://github.com/cassiobotaro -- login: lsglucas - count: 26 - avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 - url: https://github.com/lsglucas +- login: Ryandaydev + count: 25 + avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=48f68868db8886fce31a1d802c1003914c6cd7c6&v=4 + url: https://github.com/Ryandaydev +- login: LorhanSohaky + count: 24 + avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 + url: https://github.com/LorhanSohaky - login: dmontagu count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu +- login: rjNemo + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 + url: https://github.com/rjNemo - login: hard-coders - count: 20 + count: 21 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders +- login: odiseo0 + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 + url: https://github.com/odiseo0 - login: 0417taehyun count: 19 avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun -- login: rjNemo - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo - login: Smlep count: 17 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 @@ -408,6 +456,10 @@ top_reviewers: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4 url: https://github.com/zy7y +- login: peidrao + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=a66902b40c13647d0ed0e573d598128240a4dd04&v=4 + url: https://github.com/peidrao - login: yanever count: 16 avatarUrl: https://avatars.githubusercontent.com/u/21978760?v=4 @@ -416,10 +468,26 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 url: https://github.com/SwftAlpc +- login: nilslindemann + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 + url: https://github.com/nilslindemann +- login: axel584 + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 + url: https://github.com/axel584 +- login: Alexandrhub + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 + url: https://github.com/Alexandrhub - login: DevDae count: 16 avatarUrl: https://avatars.githubusercontent.com/u/87962045?u=08e10fa516e844934f4b3fc7c38b33c61697e4a1&v=4 url: https://github.com/DevDae +- login: hasansezertasan + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 + url: https://github.com/hasansezertasan - login: pedabraham count: 15 avatarUrl: https://avatars.githubusercontent.com/u/16860088?u=abf922a7b920bf8fdb7867d8b43e091f1e796178&v=4 @@ -428,30 +496,38 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 -- login: odiseo0 - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 - url: https://github.com/odiseo0 - login: sh0nk count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 url: https://github.com/sh0nk +- login: wdh99 + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/108172295?u=8a8fb95d5afe3e0fa33257b2aecae88d436249eb&v=4 + url: https://github.com/wdh99 +- login: r0b2g1t + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/5357541?u=6428442d875d5d71aaa1bb38bb11c4be1a526bc2&v=4 + url: https://github.com/r0b2g1t - login: RunningIkkyu count: 12 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 url: https://github.com/RunningIkkyu -- login: LorhanSohaky - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 - url: https://github.com/LorhanSohaky +- login: ivan-abc + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 + url: https://github.com/ivan-abc - login: solomein-sv count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=789927ee09cfabd752d3bd554fa6baf4850d2777&v=4 url: https://github.com/solomein-sv - login: mariacamilagl count: 10 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 url: https://github.com/mariacamilagl +- login: raphaelauv + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 + url: https://github.com/raphaelauv - login: Attsun1031 count: 10 avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 @@ -464,10 +540,10 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp -- login: peidrao - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=39edf7052371484cb488277638c23e1f6b584f4b&v=4 - url: https://github.com/peidrao +- login: romashevchenko + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/132477732?v=4 + url: https://github.com/romashevchenko - login: izaguerreiro count: 9 avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 @@ -476,47 +552,3 @@ top_reviewers: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/413772?u=64b77b6aa405c68a9c6bcf45f84257c66eea5f32&v=4 url: https://github.com/graingert -- login: PandaHun - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/13096845?u=646eba44db720e37d0dbe8e98e77ab534ea78a20&v=4 - url: https://github.com/PandaHun -- login: kty4119 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/49435654?v=4 - url: https://github.com/kty4119 -- login: bezaca - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 - url: https://github.com/bezaca -- login: dimaqq - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/662249?v=4 - url: https://github.com/dimaqq -- login: raphaelauv - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 - url: https://github.com/raphaelauv -- login: blt232018 - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/43393471?u=172b0e0391db1aa6c1706498d6dfcb003c8a4857&v=4 - url: https://github.com/blt232018 -- login: rogerbrinkmann - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/5690226?v=4 - url: https://github.com/rogerbrinkmann -- login: NinaHwang - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 - url: https://github.com/NinaHwang -- login: Xewus - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=4bdd4a0300530a504987db27488ba79c37f2fb18&v=4 - url: https://github.com/Xewus -- login: Serrones - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 - url: https://github.com/Serrones -- login: jovicon - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4 - url: https://github.com/jovicon diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index c39dbb589f..121a3b7616 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -1,20 +1,29 @@ gold: - - url: https://bit.ly/3dmXC5S - title: The data structure for unstructured multimodal data - img: https://fastapi.tiangolo.com/img/sponsors/docarray.svg - - url: https://bit.ly/3JJ7y5C - title: Build cross-modal and multimodal applications on the cloud - img: https://fastapi.tiangolo.com/img/sponsors/jina2.svg - url: https://cryptapi.io/ title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway." img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg + - url: https://platform.sh/try-it-now/?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023 + title: "Build, run and scale your apps on a modern, reliable, and secure PaaS." + img: https://fastapi.tiangolo.com/img/sponsors/platform-sh.png + - url: https://www.porter.run + title: Deploy FastAPI on AWS with a few clicks + img: https://fastapi.tiangolo.com/img/sponsors/porter.png + - url: https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor + title: Automate FastAPI documentation generation with Bump.sh + img: https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg + - url: https://reflex.dev + title: Reflex + img: https://fastapi.tiangolo.com/img/sponsors/reflex.png + - url: https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge + title: "Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files" + img: https://fastapi.tiangolo.com/img/sponsors/scalar.svg + - url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge + title: Auth, user management and more for your B2B product + img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas img: https://fastapi.tiangolo.com/img/sponsors/deta.svg - - url: https://www.investsuite.com/jobs - title: Wealthtech jobs with FastAPI - img: https://fastapi.tiangolo.com/img/sponsors/investsuite.svg - url: https://training.talkpython.fm/fastapi-courses title: FastAPI video courses on demand from people you trust img: https://fastapi.tiangolo.com/img/sponsors/talkpython.png @@ -24,19 +33,25 @@ silver: - url: https://github.com/deepset-ai/haystack/ title: Build powerful search from composable, open source building blocks img: https://fastapi.tiangolo.com/img/sponsors/haystack-fastapi.svg - - url: https://www.udemy.com/course/fastapi-rest/ - title: Learn FastAPI by building a complete project. Extend your knowledge on advanced web development-AWS, Payments, Emails. - img: https://fastapi.tiangolo.com/img/sponsors/ines-course.jpg - - url: https://careers.budget-insight.com/ - title: Budget Insight is hiring! - img: https://fastapi.tiangolo.com/img/sponsors/budget-insight.svg + - url: https://careers.powens.com/ + title: Powens is hiring! + img: https://fastapi.tiangolo.com/img/sponsors/powens.png + - url: https://databento.com/ + title: Pay as you go for market data + img: https://fastapi.tiangolo.com/img/sponsors/databento.svg + - url: https://speakeasyapi.dev?utm_source=fastapi+repo&utm_medium=github+sponsorship + title: SDKs for your API | Speakeasy + img: https://fastapi.tiangolo.com/img/sponsors/speakeasy.png - url: https://www.svix.com/ title: Svix - Webhooks as a service img: https://fastapi.tiangolo.com/img/sponsors/svix.svg + - url: https://www.codacy.com/?utm_source=github&utm_medium=sponsors&utm_id=pioneers + title: Take code reviews from hours to minutes + img: https://fastapi.tiangolo.com/img/sponsors/codacy.png bronze: - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source title: Biosecurity risk assessments made easy. img: https://fastapi.tiangolo.com/img/sponsors/exoflare.png - - url: https://bit.ly/3ccLCmM - title: https://striveworks.us/careers - img: https://fastapi.tiangolo.com/img/sponsors/striveworks2.png + - url: https://bit.ly/3JJ7y5C + title: Build cross-modal and multimodal applications on the cloud + img: https://fastapi.tiangolo.com/img/sponsors/jina2.svg diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index a8bfb0b1bf..4078454a8c 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -5,11 +5,21 @@ logins: - mikeckennedy - deepset-ai - cryptapi - - Striveworks - xoflare - - InesIvanova - DropbaseHQ - VincentParedes - BLUE-DEVIL1134 - ObliviousAI - Doist + - nihpo + - armand-sauzay + - databento-bot + - databento + - nanram22 + - Flint-company + - porter-dev + - fern-api + - ndimares + - svixhq + - Alek99 + - codacy diff --git a/docs/en/docs/about/index.md b/docs/en/docs/about/index.md new file mode 100644 index 0000000000..27b78696b5 --- /dev/null +++ b/docs/en/docs/about/index.md @@ -0,0 +1,3 @@ +# About + +About FastAPI, its design, inspiration and more. 🤓 diff --git a/docs/en/docs/advanced/additional-responses.md b/docs/en/docs/advanced/additional-responses.md index dca5f6a985..41b39c18e6 100644 --- a/docs/en/docs/advanced/additional-responses.md +++ b/docs/en/docs/advanced/additional-responses.md @@ -28,7 +28,7 @@ For example, to declare another response with a status code `404` and a Pydantic ``` !!! note - Have in mind that you have to return the `JSONResponse` directly. + Keep in mind that you have to return the `JSONResponse` directly. !!! info The `model` key is not part of OpenAPI. @@ -236,5 +236,5 @@ For example: To see what exactly you can include in the responses, you can check these sections in the OpenAPI specification: -* OpenAPI Responses Object, it includes the `Response Object`. -* OpenAPI Response Object, you can include anything from this directly in each response inside your `responses` parameter. Including `description`, `headers`, `content` (inside of this is that you declare different media types and JSON Schemas), and `links`. +* OpenAPI Responses Object, it includes the `Response Object`. +* OpenAPI Response Object, you can include anything from this directly in each response inside your `responses` parameter. Including `description`, `headers`, `content` (inside of this is that you declare different media types and JSON Schemas), and `links`. diff --git a/docs/en/docs/advanced/additional-status-codes.md b/docs/en/docs/advanced/additional-status-codes.md index b61f88b93d..0ce2753431 100644 --- a/docs/en/docs/advanced/additional-status-codes.md +++ b/docs/en/docs/advanced/additional-status-codes.md @@ -14,9 +14,41 @@ But you also want it to accept new items. And when the items didn't exist before To achieve that, import `JSONResponse`, and return your content there directly, setting the `status_code` that you want: -```Python hl_lines="4 25" -{!../../../docs_src/additional_status_codes/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4 26" + {!> ../../../docs_src/additional_status_codes/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="2 23" + {!> ../../../docs_src/additional_status_codes/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001.py!} + ``` !!! warning When you return a `Response` directly, like in the example above, it will be returned directly. diff --git a/docs/en/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md index 5e79776cac..0cffab56db 100644 --- a/docs/en/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -18,9 +18,26 @@ Not the class itself (which is already a callable), but an instance of that clas To do that, we declare a method `__call__`: -```Python hl_lines="10" -{!../../../docs_src/dependencies/tutorial011.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial011.py!} + ``` In this case, this `__call__` is what **FastAPI** will use to check for additional parameters and sub-dependencies, and this is what will be called to pass a value to the parameter in your *path operation function* later. @@ -28,9 +45,26 @@ In this case, this `__call__` is what **FastAPI** will use to check for addition And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency: -```Python hl_lines="7" -{!../../../docs_src/dependencies/tutorial011.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial011.py!} + ``` In this case, **FastAPI** won't ever touch or care about `__init__`, we will use it directly in our code. @@ -38,9 +72,26 @@ In this case, **FastAPI** won't ever touch or care about `__init__`, we will use We could create an instance of this class with: -```Python hl_lines="16" -{!../../../docs_src/dependencies/tutorial011.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="18" + {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial011.py!} + ``` And that way we are able to "parameterize" our dependency, that now has `"bar"` inside of it, as the attribute `checker.fixed_content`. @@ -56,9 +107,26 @@ checker(q="somequery") ...and pass whatever that returns as the value of the dependency in our *path operation function* as the parameter `fixed_content_included`: -```Python hl_lines="20" -{!../../../docs_src/dependencies/tutorial011.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="22" + {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="21" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial011.py!} + ``` !!! tip All this might seem contrived. And it might not be very clear how is it useful yet. diff --git a/docs/en/docs/advanced/async-tests.md b/docs/en/docs/advanced/async-tests.md index 9b39d70fca..f9c82e6ab4 100644 --- a/docs/en/docs/advanced/async-tests.md +++ b/docs/en/docs/advanced/async-tests.md @@ -84,6 +84,9 @@ response = client.get('/') !!! tip Note that we're using async/await with the new `AsyncClient` - the request is asynchronous. +!!! warning + If your application relies on lifespan events, the `AsyncClient` won't trigger these events. To ensure they are triggered, use `LifespanManager` from florimondmanca/asgi-lifespan. + ## Other Asynchronous Function Calls As the testing function is now asynchronous, you can now also call (and `await`) other `async` functions apart from sending requests to your FastAPI application in your tests, exactly as you would call them anywhere else in your code. diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index 766a218aa3..01998cc912 100644 --- a/docs/en/docs/advanced/behind-a-proxy.md +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -46,7 +46,7 @@ The docs UI would also need the OpenAPI schema to declare that this API `server` ```JSON hl_lines="4-8" { - "openapi": "3.0.2", + "openapi": "3.1.0", // More stuff here "servers": [ { @@ -125,7 +125,7 @@ Passing the `root_path` to `FastAPI` would be the equivalent of passing the `--r ### 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. +Keep 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: @@ -142,7 +142,7 @@ Uvicorn will expect the proxy to access Uvicorn at `http://127.0.0.1:8000/app`, ## 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. +Keep 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. @@ -251,7 +251,7 @@ We get the same response: 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. +Of course, the idea here is that everyone would access the app through the proxy, so the version with the path prefix `/api/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. @@ -298,7 +298,7 @@ Will generate an OpenAPI schema like: ```JSON hl_lines="5-7" { - "openapi": "3.0.2", + "openapi": "3.1.0", // More stuff here "servers": [ { diff --git a/docs/en/docs/advanced/custom-response.md b/docs/en/docs/advanced/custom-response.md index ce2619e8de..827776f5e5 100644 --- a/docs/en/docs/advanced/custom-response.md +++ b/docs/en/docs/advanced/custom-response.md @@ -101,7 +101,7 @@ But as you passed the `HTMLResponse` in the `response_class` too, **FastAPI** wi Here are some of the available responses. -Have in mind that you can use `Response` to return anything else, or even create a custom sub-class. +Keep in mind that you can use `Response` to return anything else, or even create a custom sub-class. !!! note "Technical Details" You could also use `from starlette.responses import HTMLResponse`. diff --git a/docs/en/docs/advanced/dataclasses.md b/docs/en/docs/advanced/dataclasses.md index 72daca06ad..ed1d5610fc 100644 --- a/docs/en/docs/advanced/dataclasses.md +++ b/docs/en/docs/advanced/dataclasses.md @@ -21,7 +21,7 @@ And of course, it supports the same: This works the same way as with Pydantic models. And it is actually achieved in the same way underneath, using Pydantic. !!! info - Have in mind that dataclasses can't do everything Pydantic models can do. + Keep in mind that dataclasses can't do everything Pydantic models can do. So, you might still need to use Pydantic models. diff --git a/docs/en/docs/advanced/events.md b/docs/en/docs/advanced/events.md index 7cd2998f05..ca9d86ae41 100644 --- a/docs/en/docs/advanced/events.md +++ b/docs/en/docs/advanced/events.md @@ -1,13 +1,108 @@ -# Events: startup - shutdown +# Lifespan Events + +You can define logic (code) that should be executed before the application **starts up**. This means that this code will be executed **once**, **before** the application **starts receiving requests**. + +The same way, you can define logic (code) that should be executed when the application is **shutting down**. In this case, this code will be executed **once**, **after** having handled possibly **many requests**. + +Because this code is executed before the application **starts** taking requests, and right after it **finishes** handling requests, it covers the whole application **lifespan** (the word "lifespan" will be important in a second 😉). + +This can be very useful for setting up **resources** that you need to use for the whole app, and that are **shared** among requests, and/or that you need to **clean up** afterwards. For example, a database connection pool, or loading a shared machine learning model. + +## Use Case + +Let's start with an example **use case** and then see how to solve it with this. + +Let's imagine that you have some **machine learning models** that you want to use to handle requests. 🤖 + +The same models are shared among requests, so, it's not one model per request, or one per user or something similar. + +Let's imagine that loading the model can **take quite some time**, because it has to read a lot of **data from disk**. So you don't want to do it for every request. + +You could load it at the top level of the module/file, but that would also mean that it would **load the model** even if you are just running a simple automated test, then that test would be **slow** because it would have to wait for the model to load before being able to run an independent part of the code. + +That's what we'll solve, let's load the model before the requests are handled, but only right before the application starts receiving requests, not while the code is being loaded. + +## Lifespan + +You can define this *startup* and *shutdown* logic using the `lifespan` parameter of the `FastAPI` app, and a "context manager" (I'll show you what that is in a second). + +Let's start with an example and then see it in detail. + +We create an async function `lifespan()` with `yield` like this: + +```Python hl_lines="16 19" +{!../../../docs_src/events/tutorial003.py!} +``` + +Here we are simulating the expensive *startup* operation of loading the model by putting the (fake) model function in the dictionary with machine learning models before the `yield`. This code will be executed **before** the application **starts taking requests**, during the *startup*. + +And then, right after the `yield`, we unload the model. This code will be executed **after** the application **finishes handling requests**, right before the *shutdown*. This could, for example, release resources like memory or a GPU. + +!!! tip + The `shutdown` would happen when you are **stopping** the application. + + Maybe you need to start a new version, or you just got tired of running it. 🤷 + +### Lifespan function + +The first thing to notice, is that we are defining an async function with `yield`. This is very similar to Dependencies with `yield`. + +```Python hl_lines="14-19" +{!../../../docs_src/events/tutorial003.py!} +``` + +The first part of the function, before the `yield`, will be executed **before** the application starts. + +And the part after the `yield` will be executed **after** the application has finished. + +### Async Context Manager + +If you check, the function is decorated with an `@asynccontextmanager`. + +That converts the function into something called an "**async context manager**". + +```Python hl_lines="1 13" +{!../../../docs_src/events/tutorial003.py!} +``` + +A **context manager** in Python is something that you can use in a `with` statement, for example, `open()` can be used as a context manager: + +```Python +with open("file.txt") as file: + file.read() +``` + +In recent versions of Python, there's also an **async context manager**. You would use it with `async with`: + +```Python +async with lifespan(app): + await do_stuff() +``` + +When you create a context manager or an async context manager like above, what it does is that, before entering the `with` block, it will execute the code before the `yield`, and after exiting the `with` block, it will execute the code after the `yield`. + +In our code example above, we don't use it directly, but we pass it to FastAPI for it to use it. + +The `lifespan` parameter of the `FastAPI` app takes an **async context manager**, so we can pass our new `lifespan` async context manager to it. + +```Python hl_lines="22" +{!../../../docs_src/events/tutorial003.py!} +``` + +## Alternative Events (deprecated) + +!!! warning + The recommended way to handle the *startup* and *shutdown* is using the `lifespan` parameter of the `FastAPI` app as described above. If you provide a `lifespan` parameter, `startup` and `shutdown` event handlers will no longer be called. It's all `lifespan` or all events, not both. + + You can probably skip this part. + +There's an alternative way to define this logic to be executed during *startup* and during *shutdown*. You can define event handlers (functions) that need to be executed before the application starts up, or when the application is shutting down. These functions can be declared with `async def` or normal `def`. -!!! warning - Only event handlers for the main application will be executed, not for [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}. - -## `startup` event +### `startup` event To add a function that should be run before the application starts, declare it with the event `"startup"`: @@ -21,7 +116,7 @@ You can add more than one event handler function. And your application won't start receiving requests until all the `startup` event handlers have completed. -## `shutdown` event +### `shutdown` event To add a function that should be run when the application is shutting down, declare it with the event `"shutdown"`: @@ -43,5 +138,25 @@ Here, the `shutdown` event handler function will write a text line `"Application So, we declare the event handler function with standard `def` instead of `async def`. +### `startup` and `shutdown` together + +There's a high chance that the logic for your *startup* and *shutdown* is connected, you might want to start something and then finish it, acquire a resource and then release it, etc. + +Doing that in separated functions that don't share logic or variables together is more difficult as you would need to store values in global variables or similar tricks. + +Because of that, it's now recommended to instead use the `lifespan` as explained above. + +## Technical Details + +Just a technical detail for the curious nerds. 🤓 + +Underneath, in the ASGI technical specification, this is part of the Lifespan Protocol, and it defines events called `startup` and `shutdown`. + !!! info - You can read more about these event handlers in Starlette's Events' docs. + You can read more about the Starlette `lifespan` handlers in Starlette's Lifespan' docs. + + Including how to handle lifespan state that can be used in other areas of your code. + +## Sub Applications + +🚨 Keep in mind that these lifespan events (startup and shutdown) will only be executed for the main application, not for [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}. diff --git a/docs/en/docs/advanced/extending-openapi.md b/docs/en/docs/advanced/extending-openapi.md deleted file mode 100644 index 36619696b5..0000000000 --- a/docs/en/docs/advanced/extending-openapi.md +++ /dev/null @@ -1,314 +0,0 @@ -# Extending OpenAPI - -!!! warning - This is a rather advanced feature. You probably can skip it. - - If you are just following the tutorial - user guide, you can probably skip this section. - - If you already know that you need to modify the generated OpenAPI schema, continue reading. - -There are some cases where you might need to modify the generated OpenAPI schema. - -In this section you will see how. - -## The normal process - -The normal (default) process, is as follows. - -A `FastAPI` application (instance) has an `.openapi()` method that is expected to return the OpenAPI schema. - -As part of the application object creation, a *path operation* for `/openapi.json` (or for whatever you set your `openapi_url`) is registered. - -It just returns a JSON response with the result of the application's `.openapi()` method. - -By default, what the method `.openapi()` does is check the property `.openapi_schema` to see if it has contents and return them. - -If it doesn't, it generates them using the utility function at `fastapi.openapi.utils.get_openapi`. - -And that function `get_openapi()` receives as parameters: - -* `title`: The OpenAPI title, shown in the docs. -* `version`: The version of your API, e.g. `2.5.0`. -* `openapi_version`: The version of the OpenAPI specification used. By default, the latest: `3.0.2`. -* `description`: The description of your API. -* `routes`: A list of routes, these are each of the registered *path operations*. They are taken from `app.routes`. - -## Overriding the defaults - -Using the information above, you can use the same utility function to generate the OpenAPI schema and override each part that you need. - -For example, let's add ReDoc's OpenAPI extension to include a custom logo. - -### Normal **FastAPI** - -First, write all your **FastAPI** application as normally: - -```Python hl_lines="1 4 7-9" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### Generate the OpenAPI schema - -Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function: - -```Python hl_lines="2 15-20" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### 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-23" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### Cache the OpenAPI schema - -You can use the property `.openapi_schema` as a "cache", to store your generated schema. - -That way, your application won't have to generate the schema every time a user opens your API docs. - -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" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### Override the method - -Now you can replace the `.openapi()` method with your new function. - -```Python hl_lines="28" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### Check it - -Once you go to http://127.0.0.1:8000/redoc you will see that you are using your custom logo (in this example, **FastAPI**'s logo): - - - -## Self-hosting JavaScript and CSS for docs - -The API docs use **Swagger UI** and **ReDoc**, and each of those need some JavaScript and CSS files. - -By default, those files are served from a CDN. - -But it's possible to customize it, you can set a specific CDN, or serve the files yourself. - -That's useful, for example, if you need your app to keep working even while offline, without open Internet access, or in a local network. - -Here you'll see how to serve those files yourself, in the same FastAPI app, and configure the docs to use them. - -### Project file structure - -Let's say your project file structure looks like this: - -``` -. -├── app -│ ├── __init__.py -│ ├── main.py -``` - -Now create a directory to store those static files. - -Your new file structure could look like this: - -``` -. -├── app -│   ├── __init__.py -│   ├── main.py -└── static/ -``` - -### Download the files - -Download the static files needed for the docs and put them on that `static/` directory. - -You can probably right-click each link and select an option similar to `Save link as...`. - -**Swagger UI** uses the files: - -* `swagger-ui-bundle.js` -* `swagger-ui.css` - -And **ReDoc** uses the file: - -* `redoc.standalone.js` - -After that, your file structure could look like: - -``` -. -├── app -│   ├── __init__.py -│   ├── main.py -└── static - ├── redoc.standalone.js - ├── swagger-ui-bundle.js - └── swagger-ui.css -``` - -### Serve the static files - -* Import `StaticFiles`. -* "Mount" a `StaticFiles()` instance in a specific path. - -```Python hl_lines="7 11" -{!../../../docs_src/extending_openapi/tutorial002.py!} -``` - -### Test the static files - -Start your application and go to http://127.0.0.1:8000/static/redoc.standalone.js. - -You should see a very long JavaScript file for **ReDoc**. - -It could start with something like: - -```JavaScript -/*! - * ReDoc - OpenAPI/Swagger-generated API Reference Documentation - * ------------------------------------------------------------- - * Version: "2.0.0-rc.18" - * Repo: https://github.com/Redocly/redoc - */ -!function(e,t){"object"==typeof exports&&"object"==typeof m - -... -``` - -That confirms that you are being able to serve static files from your app, and that you placed the static files for the docs in the correct place. - -Now we can configure the app to use those static files for the docs. - -### Disable the automatic docs - -The first step is to disable the automatic docs, as those use the CDN by default. - -To disable them, set their URLs to `None` when creating your `FastAPI` app: - -```Python hl_lines="9" -{!../../../docs_src/extending_openapi/tutorial002.py!} -``` - -### Include the custom docs - -Now you can create the *path operations* for the custom docs. - -You can re-use FastAPI's internal functions to create the HTML pages for the docs, and pass them the needed arguments: - -* `openapi_url`: the URL where the HTML page for the docs can get the OpenAPI schema for your API. You can use here the attribute `app.openapi_url`. -* `title`: the title of your API. -* `oauth2_redirect_url`: you can use `app.swagger_ui_oauth2_redirect_url` here to use the default. -* `swagger_js_url`: the URL where the HTML for your Swagger UI docs can get the **JavaScript** file. This is the one that your own app is now serving. -* `swagger_css_url`: the URL where the HTML for your Swagger UI docs can get the **CSS** file. This is the one that your own app is now serving. - -And similarly for ReDoc... - -```Python hl_lines="2-6 14-22 25-27 30-36" -{!../../../docs_src/extending_openapi/tutorial002.py!} -``` - -!!! tip - The *path operation* for `swagger_ui_redirect` is a helper for when you use OAuth2. - - If you integrate your API with an OAuth2 provider, you will be able to authenticate and come back to the API docs with the acquired credentials. And interact with it using the real OAuth2 authentication. - - Swagger UI will handle it behind the scenes for you, but it needs this "redirect" helper. - -### Create a *path operation* to test it - -Now, to be able to test that everything works, create a *path operation*: - -```Python hl_lines="39-41" -{!../../../docs_src/extending_openapi/tutorial002.py!} -``` - -### Test it - -Now, you should be able to disconnect your WiFi, go to your docs at http://127.0.0.1:8000/docs, and reload the page. - -And even without Internet, you would be able to see the docs for your API and interact with it. - -## Configuring Swagger UI - -You can configure some extra Swagger UI parameters. - -To configure them, pass the `swagger_ui_parameters` argument when creating the `FastAPI()` app object or to the `get_swagger_ui_html()` function. - -`swagger_ui_parameters` receives a dictionary with the configurations passed to Swagger UI directly. - -FastAPI converts the configurations to **JSON** to make them compatible with JavaScript, as that's what Swagger UI needs. - -### Disable Syntax Highlighting - -For example, you could disable syntax highlighting in Swagger UI. - -Without changing the settings, syntax highlighting is enabled by default: - - - -But you can disable it by setting `syntaxHighlight` to `False`: - -```Python hl_lines="3" -{!../../../docs_src/extending_openapi/tutorial003.py!} -``` - -...and then Swagger UI won't show the syntax highlighting anymore: - - - -### Change the Theme - -The same way you could set the syntax highlighting theme with the key `"syntaxHighlight.theme"` (notice that it has a dot in the middle): - -```Python hl_lines="3" -{!../../../docs_src/extending_openapi/tutorial004.py!} -``` - -That configuration would change the syntax highlighting color theme: - - - -### Change Default Swagger UI Parameters - -FastAPI includes some default configuration parameters appropriate for most of the use cases. - -It includes these default configurations: - -```Python -{!../../../fastapi/openapi/docs.py[ln:7-13]!} -``` - -You can override any of them by setting a different value in the argument `swagger_ui_parameters`. - -For example, to disable `deepLinking` you could pass these settings to `swagger_ui_parameters`: - -```Python hl_lines="3" -{!../../../docs_src/extending_openapi/tutorial005.py!} -``` - -### Other Swagger UI Parameters - -To see all the other possible configurations you can use, read the official docs for Swagger UI parameters. - -### JavaScript-only settings - -Swagger UI also allows other configurations to be **JavaScript-only** objects (for example, JavaScript functions). - -FastAPI also includes these JavaScript-only `presets` settings: - -```JavaScript -presets: [ - SwaggerUIBundle.presets.apis, - SwaggerUIBundle.SwaggerUIStandalonePreset -] -``` - -These are **JavaScript** objects, not strings, so you can't pass them from Python code directly. - -If you need to use JavaScript-only configurations like those, you can use one of the methods above. Override all the Swagger UI *path operation* and manually write any JavaScript you need. diff --git a/docs/en/docs/advanced/generate-clients.md b/docs/en/docs/advanced/generate-clients.md index f31a248d0b..3a810baee1 100644 --- a/docs/en/docs/advanced/generate-clients.md +++ b/docs/en/docs/advanced/generate-clients.md @@ -12,22 +12,34 @@ A common tool is openapi-typescript-codegen. +## Client and SDK Generators - Sponsor + +There are also some **company-backed** Client and SDK generators based on OpenAPI (FastAPI), in some cases they can offer you **additional features** on top of high-quality generated SDKs/clients. + +Some of them also ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, this ensures the continued and healthy **development** of FastAPI and its **ecosystem**. + +And it shows their true commitment to FastAPI and its **community** (you), as they not only want to provide you a **good service** but also want to make sure you have a **good and healthy framework**, FastAPI. 🙇 + +For example, you might want to try Speakeasy. + +There are also several other companies offering similar services that you can search and find online. 🤓 + ## Generate a TypeScript Frontend Client Let's start with a simple FastAPI application: -=== "Python 3.6 and above" - - ```Python hl_lines="9-11 14-15 18 19 23" - {!> ../../../docs_src/generate_clients/tutorial001.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="7-9 12-13 16-17 21" {!> ../../../docs_src/generate_clients/tutorial001_py39.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9-11 14-15 18 19 23" + {!> ../../../docs_src/generate_clients/tutorial001.py!} + ``` + Notice that the *path operations* define the models they use for request payload and response payload, using the models `Item` and `ResponseMessage`. ### API Docs @@ -75,7 +87,7 @@ It could look like this: "description": "", "main": "index.js", "scripts": { - "generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios" + "generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes" }, "author": "", "license": "", @@ -94,7 +106,7 @@ After having that NPM `generate-client` script there, you can run it with: $ npm run generate-client frontend-app@1.0.0 generate-client /home/user/code/frontend-app -> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios +> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes ``` @@ -128,19 +140,18 @@ In many cases your FastAPI app will be bigger, and you will probably use tags to For example, you could have a section for **items** and another section for **users**, and they could be separated by tags: - -=== "Python 3.6 and above" - - ```Python hl_lines="23 28 36" - {!> ../../../docs_src/generate_clients/tutorial002.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="21 26 34" {!> ../../../docs_src/generate_clients/tutorial002_py39.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="23 28 36" + {!> ../../../docs_src/generate_clients/tutorial002.py!} + ``` + ### Generate a TypeScript Client with Tags If you generate a client for a FastAPI app using tags, it will normally also separate the client code based on the tags. @@ -186,18 +197,18 @@ For example, here it is using the first tag (you will probably have only one tag You can then pass that custom function to **FastAPI** as the `generate_unique_id_function` parameter: -=== "Python 3.6 and above" - - ```Python hl_lines="8-9 12" - {!> ../../../docs_src/generate_clients/tutorial003.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="6-7 10" {!> ../../../docs_src/generate_clients/tutorial003_py39.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="8-9 12" + {!> ../../../docs_src/generate_clients/tutorial003.py!} + ``` + ### Generate a TypeScript Client with Custom Operation IDs Now if you generate the client again, you will see that it has the improved method names: @@ -218,9 +229,17 @@ But for the generated client we could **modify** the OpenAPI operation IDs right We could download the OpenAPI JSON to a file `openapi.json` and then we could **remove that prefixed tag** with a script like this: -```Python -{!../../../docs_src/generate_clients/tutorial004.py!} -``` +=== "Python" + + ```Python + {!> ../../../docs_src/generate_clients/tutorial004.py!} + ``` + +=== "Node.js" + + ```Python + {!> ../../../docs_src/generate_clients/tutorial004.js!} + ``` With that, the operation IDs would be renamed from things like `items-get_items` to just `get_items`, that way the client generator can generate simpler method names. @@ -235,7 +254,7 @@ Now as the end result is in a file `openapi.json`, you would modify the `package "description": "", "main": "index.js", "scripts": { - "generate-client": "openapi --input ./openapi.json --output ./src/client --client axios" + "generate-client": "openapi --input ./openapi.json --output ./src/client --client axios --useOptions --useUnionTypes" }, "author": "", "license": "", diff --git a/docs/en/docs/advanced/index.md b/docs/en/docs/advanced/index.md index 917f4a62eb..d8dcd4ca67 100644 --- a/docs/en/docs/advanced/index.md +++ b/docs/en/docs/advanced/index.md @@ -1,4 +1,4 @@ -# Advanced User Guide - Intro +# Advanced User Guide ## Additional Features @@ -17,8 +17,17 @@ You could still use most of the features in **FastAPI** with the knowledge from And the next sections assume you already read it, and assume that you know those main ideas. -## TestDriven.io course +## External Courses -If you would like to take an advanced-beginner course to complement this section of the docs, you might want to check: Test-Driven Development with FastAPI and Docker by **TestDriven.io**. +Although the [Tutorial - User Guide](../tutorial/){.internal-link target=_blank} and this **Advanced User Guide** are written as a guided tutorial (like a book) and should be enough for you to **learn FastAPI**, you might want to complement it with additional courses. -They are currently donating 10% of all profits to the development of **FastAPI**. 🎉 😄 +Or it might be the case that you just prefer to take other courses because they adapt better to your learning style. + +Some course providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, this ensures the continued and healthy **development** of FastAPI and its **ecosystem**. + +And it shows their true commitment to FastAPI and its **community** (you), as they not only want to provide you a **good learning experience** but also want to make sure you have a **good and healthy framework**, FastAPI. 🙇 + +You might want to try their courses: + +* Talk Python Training +* Test-Driven Development diff --git a/docs/en/docs/advanced/middleware.md b/docs/en/docs/advanced/middleware.md index 3bf49e3922..9219f1d2cb 100644 --- a/docs/en/docs/advanced/middleware.md +++ b/docs/en/docs/advanced/middleware.md @@ -92,7 +92,7 @@ There are many other ASGI middlewares. For example: -* Sentry +* Sentry * Uvicorn's `ProxyHeadersMiddleware` * MessagePack diff --git a/docs/en/docs/advanced/openapi-callbacks.md b/docs/en/docs/advanced/openapi-callbacks.md index 71924ce8b2..03429b187f 100644 --- a/docs/en/docs/advanced/openapi-callbacks.md +++ b/docs/en/docs/advanced/openapi-callbacks.md @@ -38,7 +38,7 @@ This part is pretty normal, most of the code is probably already familiar to you !!! tip The `callback_url` query parameter uses a Pydantic URL type. -The only new thing is the `callbacks=messages_callback_router.routes` as an argument to the *path operation decorator*. We'll see what that is next. +The only new thing is the `callbacks=invoices_callback_router.routes` as an argument to the *path operation decorator*. We'll see what that is next. ## Documenting the callback @@ -103,11 +103,11 @@ It should look just like a normal FastAPI *path operation*: There are 2 main differences from a normal *path operation*: * It doesn't need to have any actual code, because your app will never call this code. It's only used to document the *external API*. So, the function could just have `pass`. -* The *path* can contain an OpenAPI 3 expression (see more below) where it can use variables with parameters and parts of the original request sent to *your API*. +* The *path* can contain an OpenAPI 3 expression (see more below) where it can use variables with parameters and parts of the original request sent to *your API*. ### The callback path expression -The callback *path* can have an OpenAPI 3 expression that can contain parts of the original request sent to *your API*. +The callback *path* can have an OpenAPI 3 expression that can contain parts of the original request sent to *your API*. In this case, it's the `str`: diff --git a/docs/en/docs/advanced/openapi-webhooks.md b/docs/en/docs/advanced/openapi-webhooks.md new file mode 100644 index 0000000000..63cbdc6103 --- /dev/null +++ b/docs/en/docs/advanced/openapi-webhooks.md @@ -0,0 +1,51 @@ +# OpenAPI Webhooks + +There are cases where you want to tell your API **users** that your app could call *their* app (sending a request) with some data, normally to **notify** of some type of **event**. + +This means that instead of the normal process of your users sending requests to your API, it's **your API** (or your app) that could **send requests to their system** (to their API, their app). + +This is normally called a **webhook**. + +## Webhooks steps + +The process normally is that **you define** in your code what is the message that you will send, the **body of the request**. + +You also define in some way at which **moments** your app will send those requests or events. + +And **your users** define in some way (for example in a web dashboard somewhere) the **URL** where your app should send those requests. + +All the **logic** about how to register the URLs for webhooks and the code to actually send those requests is up to you. You write it however you want to in **your own code**. + +## Documenting webhooks with **FastAPI** and OpenAPI + +With **FastAPI**, using OpenAPI, you can define the names of these webhooks, the types of HTTP operations that your app can send (e.g. `POST`, `PUT`, etc.) and the request **bodies** that your app would send. + +This can make it a lot easier for your users to **implement their APIs** to receive your **webhook** requests, they might even be able to autogenerate some of their own API code. + +!!! info + Webhooks are available in OpenAPI 3.1.0 and above, supported by FastAPI `0.99.0` and above. + +## An app with webhooks + +When you create a **FastAPI** application, there is a `webhooks` attribute that you can use to define *webhooks*, the same way you would define *path operations*, for example with `@app.webhooks.post()`. + +```Python hl_lines="9-13 36-53" +{!../../../docs_src/openapi_webhooks/tutorial001.py!} +``` + +The webhooks that you define will end up in the **OpenAPI** schema and the automatic **docs UI**. + +!!! info + The `app.webhooks` object is actually just an `APIRouter`, the same type you would use when structuring your app with multiple files. + +Notice that with webhooks you are actually not declaring a *path* (like `/items/`), the text you pass there is just an **identifier** of the webhook (the name of the event), for example in `@app.webhooks.post("new-subscription")`, the webhook name is `new-subscription`. + +This is because it is expected that **your users** would define the actual **URL path** where they want to receive the webhook request in some other way (e.g. a web dashboard). + +### Check the docs + +Now you can start your app with Uvicorn and go to http://127.0.0.1:8000/docs. + +You will see your docs have the normal *path operations* and now also some **webhooks**: + + diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index a1c902ef2c..8b79bfe22a 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -97,7 +97,7 @@ And if you see the resulting OpenAPI (at `/openapi.json` in your API), you will ```JSON hl_lines="22" { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" @@ -150,9 +150,20 @@ And you could do this even if the data type in the request is not JSON. For example, in this application we don't use FastAPI's integrated functionality to extract the JSON Schema from Pydantic models nor the automatic validation for JSON. In fact, we are declaring the request content type as YAML, not JSON: -```Python hl_lines="17-22 24" -{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} -``` +=== "Pydantic v2" + + ```Python hl_lines="17-22 24" + {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} + ``` + +=== "Pydantic v1" + + ```Python hl_lines="17-22 24" + {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!} + ``` + +!!! info + In Pydantic version 1 the method to get the JSON Schema for a model was called `Item.schema()`, in Pydantic version 2, the method is called `Item.model_json_schema()`. Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML. @@ -160,9 +171,20 @@ Then we use the request directly, and extract the body as `bytes`. This means th And then in our code, we parse that YAML content directly, and then we are again using the same Pydantic model to validate the YAML content: -```Python hl_lines="26-33" -{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} -``` +=== "Pydantic v2" + + ```Python hl_lines="26-33" + {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} + ``` + +=== "Pydantic v1" + + ```Python hl_lines="26-33" + {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!} + ``` + +!!! info + In Pydantic version 1 the method to parse and validate an object was `Item.parse_obj()`, in Pydantic version 2, the method is called `Item.model_validate()`. !!! tip Here we re-use the same Pydantic model. diff --git a/docs/en/docs/advanced/response-change-status-code.md b/docs/en/docs/advanced/response-change-status-code.md index 979cef3f05..b88d74a8af 100644 --- a/docs/en/docs/advanced/response-change-status-code.md +++ b/docs/en/docs/advanced/response-change-status-code.md @@ -30,4 +30,4 @@ And if you declared a `response_model`, it will still be used to filter and conv **FastAPI** will use that *temporal* response to extract the status code (also cookies and headers), and will put them in the final response that contains the value you returned, filtered by any `response_model`. -You can also declare the `Response` parameter in dependencies, and set the status code in them. But have in mind that the last one to be set will win. +You can also declare the `Response` parameter in dependencies, and set the status code in them. But keep in mind that the last one to be set will win. diff --git a/docs/en/docs/advanced/response-cookies.md b/docs/en/docs/advanced/response-cookies.md index 9178ef8162..d53985dbba 100644 --- a/docs/en/docs/advanced/response-cookies.md +++ b/docs/en/docs/advanced/response-cookies.md @@ -31,7 +31,7 @@ Then set Cookies in it, and then return it: ``` !!! tip - Have in mind that if you return a response directly instead of using the `Response` parameter, FastAPI will return it directly. + Keep in mind that if you return a response directly instead of using the `Response` parameter, FastAPI will return it directly. So, you will have to make sure your data is of the correct type. E.g. it is compatible with JSON, if you are returning a `JSONResponse`. diff --git a/docs/en/docs/advanced/response-headers.md b/docs/en/docs/advanced/response-headers.md index 758bd64556..49b5fe4766 100644 --- a/docs/en/docs/advanced/response-headers.md +++ b/docs/en/docs/advanced/response-headers.md @@ -37,6 +37,6 @@ Create a response as described in [Return a Response Directly](response-directly ## Custom Headers -Have in mind that custom proprietary headers can be added using the 'X-' prefix. +Keep in mind that custom proprietary headers can be added using the 'X-' prefix. But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations (read more in [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), using the parameter `expose_headers` documented in Starlette's CORS docs. diff --git a/docs/en/docs/advanced/security/http-basic-auth.md b/docs/en/docs/advanced/security/http-basic-auth.md index 90c516808f..680f4dff53 100644 --- a/docs/en/docs/advanced/security/http-basic-auth.md +++ b/docs/en/docs/advanced/security/http-basic-auth.md @@ -20,9 +20,26 @@ Then, when you type that username and password, the browser sends them in the he * It returns an object of type `HTTPBasicCredentials`: * It contains the `username` and `password` sent. -```Python hl_lines="2 6 10" -{!../../../docs_src/security/tutorial006.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="4 8 12" + {!> ../../../docs_src/security/tutorial006_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="2 7 11" + {!> ../../../docs_src/security/tutorial006_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="2 6 10" + {!> ../../../docs_src/security/tutorial006.py!} + ``` When you try to open the URL for the first time (or click the "Execute" button in the docs) the browser will ask you for your username and password: @@ -42,9 +59,26 @@ To handle that, we first convert the `username` and `password` to `bytes` encodi Then we can use `secrets.compare_digest()` to ensure that `credentials.username` is `"stanleyjobson"`, and that `credentials.password` is `"swordfish"`. -```Python hl_lines="1 11-21" -{!../../../docs_src/security/tutorial007.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="1 12-24" + {!> ../../../docs_src/security/tutorial007_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 12-24" + {!> ../../../docs_src/security/tutorial007_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="1 11-21" + {!> ../../../docs_src/security/tutorial007.py!} + ``` This would be similar to: @@ -71,7 +105,7 @@ if "johndoe" == "stanleyjobson" and "love123" == "swordfish": ... ``` -But right at the moment Python compares the first `j` in `johndoe` to the first `s` in `stanleyjobson`, it will return `False`, because it already knows that those two strings are not the same, thinking that "there's no need to waste more computation comparing the rest of the letters". And your application will say "incorrect user or password". +But right at the moment Python compares the first `j` in `johndoe` to the first `s` in `stanleyjobson`, it will return `False`, because it already knows that those two strings are not the same, thinking that "there's no need to waste more computation comparing the rest of the letters". And your application will say "Incorrect username or password". But then the attackers try with username `stanleyjobsox` and password `love123`. @@ -82,11 +116,11 @@ if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": ... ``` -Python will have to compare the whole `stanleyjobso` in both `stanleyjobsox` and `stanleyjobson` before realizing that both strings are not the same. So it will take some extra microseconds to reply back "incorrect user or password". +Python will have to compare the whole `stanleyjobso` in both `stanleyjobsox` and `stanleyjobson` before realizing that both strings are not the same. So it will take some extra microseconds to reply back "Incorrect username or password". #### The time to answer helps the attackers -At that point, by noticing that the server took some microseconds longer to send the "incorrect user or password" response, the attackers will know that they got _something_ right, some of the initial letters were right. +At that point, by noticing that the server took some microseconds longer to send the "Incorrect username or password" response, the attackers will know that they got _something_ right, some of the initial letters were right. And then they can try again knowing that it's probably something more similar to `stanleyjobsox` than to `johndoe`. @@ -108,6 +142,23 @@ That way, using `secrets.compare_digest()` in your application code, it will be After detecting that the credentials are incorrect, return an `HTTPException` with a status code 401 (the same returned when no credentials are provided) and add the header `WWW-Authenticate` to make the browser show the login prompt again: -```Python hl_lines="23-27" -{!../../../docs_src/security/tutorial007.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="26-30" + {!> ../../../docs_src/security/tutorial007_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="26-30" + {!> ../../../docs_src/security/tutorial007_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="23-27" + {!> ../../../docs_src/security/tutorial007.py!} + ``` diff --git a/docs/en/docs/advanced/security/index.md b/docs/en/docs/advanced/security/index.md index 0c94986b57..c18baf64b0 100644 --- a/docs/en/docs/advanced/security/index.md +++ b/docs/en/docs/advanced/security/index.md @@ -1,4 +1,4 @@ -# Advanced Security - Intro +# Advanced Security ## Additional Features diff --git a/docs/en/docs/advanced/security/oauth2-scopes.md b/docs/en/docs/advanced/security/oauth2-scopes.md index be51325cdb..b93d2991c4 100644 --- a/docs/en/docs/advanced/security/oauth2-scopes.md +++ b/docs/en/docs/advanced/security/oauth2-scopes.md @@ -56,9 +56,50 @@ They are normally used to declare specific security permissions, for example: First, let's quickly see the parts that change from the examples in the main **Tutorial - User Guide** for [OAuth2 with Password (and hashing), Bearer with JWT tokens](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. Now using OAuth2 scopes: -```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="4 8 12 46 64 105 107-115 121-124 128-134 139 155" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 155" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="2 4 8 12 47 65 106 108-116 122-125 129-135 140 156" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="3 7 11 45 63 104 106-114 120-123 127-133 138 154" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 155" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 155" + {!> ../../../docs_src/security/tutorial005.py!} + ``` Now let's review those changes step by step. @@ -68,9 +109,51 @@ The first change is that now we are declaring the OAuth2 security scheme with tw The `scopes` parameter receives a `dict` with each scope as a key and the description as the value: -```Python hl_lines="62-65" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="63-66" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="61-64" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` + + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005.py!} + ``` Because we are now declaring those scopes, they will show up in the API docs when you log-in/authorize. @@ -93,9 +176,50 @@ And we return the scopes as part of the JWT token. But in your application, for security, you should make sure you only add the scopes that the user is actually able to have, or the ones you have predefined. -```Python hl_lines="153" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="155" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="155" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="156" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="154" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="155" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="155" + {!> ../../../docs_src/security/tutorial005.py!} + ``` ## Declare scopes in *path operations* and dependencies @@ -118,9 +242,50 @@ In this case, it requires the scope `me` (it could require more than one scope). We are doing it here to demonstrate how **FastAPI** handles scopes declared at different levels. -```Python hl_lines="4 139 166" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="4 139 170" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4 139 170" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4 140 171" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="3 138 167" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4 139 168" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4 139 168" + {!> ../../../docs_src/security/tutorial005.py!} + ``` !!! info "Technical Details" `Security` is actually a subclass of `Depends`, and it has just one extra parameter that we'll see later. @@ -143,9 +308,50 @@ We also declare a special parameter of type `SecurityScopes`, imported from `fas This `SecurityScopes` class is similar to `Request` (`Request` was used to get the request object directly). -```Python hl_lines="8 105" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8 106" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7 104" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005.py!} + ``` ## Use the `scopes` @@ -159,9 +365,50 @@ We create an `HTTPException` that we can re-use (`raise`) later at several point In this exception, we include the scopes required (if any) as a string separated by spaces (using `scope_str`). We put that string containing the scopes in the `WWW-Authenticate` header (this is part of the spec). -```Python hl_lines="105 107-115" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="106 108-116" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="104 106-114" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005.py!} + ``` ## Verify the `username` and data shape @@ -177,9 +424,50 @@ Instead of, for example, a `dict`, or something else, as it could break the appl We also verify that we have a user with that username, and if not, we raise that same exception we created before. -```Python hl_lines="46 116-127" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="47 117-128" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="45 115-126" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005.py!} + ``` ## Verify the `scopes` @@ -187,9 +475,50 @@ We now verify that all the scopes required, by this dependency and all the depen For this, we use `security_scopes.scopes`, that contains a `list` with all these scopes as `str`. -```Python hl_lines="128-134" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="129-135" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="127-133" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005.py!} + ``` ## Dependency tree and scopes diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index d5151b585c..f6db8d2b15 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -125,7 +125,34 @@ That means that any value read in Python from an environment variable will be a ## Pydantic `Settings` -Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with Pydantic: Settings management. +Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with Pydantic: Settings management. + +### Install `pydantic-settings` + +First, install the `pydantic-settings` package: + +
+ +```console +$ pip install pydantic-settings +---> 100% +``` + +
+ +It also comes included when you install the `all` extras with: + +
+ +```console +$ pip install "fastapi[all]" +---> 100% +``` + +
+ +!!! info + In Pydantic v1 it came included with the main package. Now it is distributed as this independent package so that you can choose to install it or not if you don't need that functionality. ### Create the `Settings` object @@ -135,9 +162,20 @@ The same way as with Pydantic models, you declare class attributes with type ann You can use all the same validation features and tools you use for Pydantic models, like different data types and additional validations with `Field()`. -```Python hl_lines="2 5-8 11" -{!../../../docs_src/settings/tutorial001.py!} -``` +=== "Pydantic v2" + + ```Python hl_lines="2 5-8 11" + {!> ../../../docs_src/settings/tutorial001.py!} + ``` + +=== "Pydantic v1" + + !!! info + In Pydantic v1 you would import `BaseSettings` directly from `pydantic` instead of from `pydantic_settings`. + + ```Python hl_lines="2 5-8 11" + {!> ../../../docs_src/settings/tutorial001_pv1.py!} + ``` !!! tip If you want something quick to copy and paste, don't use this example, use the last one below. @@ -216,20 +254,54 @@ Notice that now we don't create a default instance `settings = Settings()`. Now we create a dependency that returns a new `config.Settings()`. -```Python hl_lines="5 11-12" -{!../../../docs_src/settings/app02/main.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an_py39/main.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="5 11-12" + {!> ../../../docs_src/settings/app02/main.py!} + ``` !!! tip - We'll discuss the `@lru_cache()` in a bit. + We'll discuss the `@lru_cache` in a bit. For now you can assume `get_settings()` is a normal function. And then we can require it from the *path operation function* as a dependency and use it anywhere we need it. -```Python hl_lines="16 18-20" -{!../../../docs_src/settings/app02/main.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an_py39/main.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="16 18-20" + {!> ../../../docs_src/settings/app02/main.py!} + ``` ### Settings and testing @@ -254,7 +326,7 @@ This practice is common enough that it has a name, these environment variables a But a dotenv file doesn't really have to have that exact filename. -Pydantic has support for reading from these types of files using an external library. You can read more at Pydantic Settings: Dotenv (.env) support. +Pydantic has support for reading from these types of files using an external library. You can read more at Pydantic Settings: Dotenv (.env) support. !!! tip For this to work, you need to `pip install python-dotenv`. @@ -272,14 +344,28 @@ APP_NAME="ChimichangApp" And then update your `config.py` with: -```Python hl_lines="9-10" -{!../../../docs_src/settings/app03/config.py!} -``` +=== "Pydantic v2" -Here we create a class `Config` inside of your Pydantic `Settings` class, and set the `env_file` to the filename with the dotenv file we want to use. + ```Python hl_lines="9" + {!> ../../../docs_src/settings/app03_an/config.py!} + ``` -!!! tip - The `Config` class is used just for Pydantic configuration. You can read more at Pydantic Model Config + !!! tip + The `model_config` attribute is used just for Pydantic configuration. You can read more at Pydantic Model Config. + +=== "Pydantic v1" + + ```Python hl_lines="9-10" + {!> ../../../docs_src/settings/app03_an/config_pv1.py!} + ``` + + !!! tip + The `Config` class is used just for Pydantic configuration. You can read more at Pydantic Model Config. + +!!! info + In Pydantic version 1 the configuration was done in an internal class `Config`, in Pydantic version 2 it's done in an attribute `model_config`. This attribute takes a `dict`, and to get autocompletion and inline errors you can import and use `SettingsConfigDict` to define that `dict`. + +Here we define the config `env_file` inside of your Pydantic `Settings` class, and set the value to the filename with the dotenv file we want to use. ### Creating the `Settings` only once with `lru_cache` @@ -302,24 +388,41 @@ def get_settings(): we would create that object for each request, and we would be reading the `.env` file for each request. ⚠️ -But as we are using the `@lru_cache()` decorator on top, the `Settings` object will be created only once, the first time it's called. ✔️ +But as we are using the `@lru_cache` decorator on top, the `Settings` object will be created only once, the first time it's called. ✔️ -```Python hl_lines="1 10" -{!../../../docs_src/settings/app03/main.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an_py39/main.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an/main.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="1 10" + {!> ../../../docs_src/settings/app03/main.py!} + ``` Then for any subsequent calls of `get_settings()` in the dependencies for the next requests, instead of executing the internal code of `get_settings()` and creating a new `Settings` object, it will return the same object that was returned on the first call, again and again. #### `lru_cache` Technical Details -`@lru_cache()` modifies the function it decorates to return the same value that was returned the first time, instead of computing it again, executing the code of the function every time. +`@lru_cache` modifies the function it decorates to return the same value that was returned the first time, instead of computing it again, executing the code of the function every time. So, the function below it will be executed once for each combination of arguments. And then the values returned by each of those combinations of arguments will be used again and again whenever the function is called with exactly the same combination of arguments. For example, if you have a function: ```Python -@lru_cache() +@lru_cache def say_hi(name: str, salutation: str = "Ms."): return f"Hello {salutation} {name}" ``` @@ -371,7 +474,7 @@ In the case of our dependency `get_settings()`, the function doesn't even take a That way, it behaves almost as if it was just a global variable. But as it uses a dependency function, then we can override it easily for testing. -`@lru_cache()` is part of `functools` which is part of Python's standard library, you can read more about it in the Python docs for `@lru_cache()`. +`@lru_cache` is part of `functools` which is part of Python's standard library, you can read more about it in the Python docs for `@lru_cache`. ## Recap @@ -379,4 +482,4 @@ You can use Pydantic Settings to handle the settings or configurations for your * By using a dependency you can simplify testing. * You can use `.env` files with it. -* Using `@lru_cache()` lets you avoid reading the dotenv file again and again for each request, while allowing you to override it during testing. +* Using `@lru_cache` lets you avoid reading the dotenv file again and again for each request, while allowing you to override it during testing. diff --git a/docs/en/docs/advanced/templates.md b/docs/en/docs/advanced/templates.md index 38618aeeb0..6055b30170 100644 --- a/docs/en/docs/advanced/templates.md +++ b/docs/en/docs/advanced/templates.md @@ -25,14 +25,16 @@ $ pip install jinja2 * Import `Jinja2Templates`. * Create a `templates` object that you can re-use later. * Declare a `Request` parameter in the *path operation* that will return a template. -* Use the `templates` you created to render and return a `TemplateResponse`, passing the `request` as one of the key-value pairs in the Jinja2 "context". +* Use the `templates` you created to render and return a `TemplateResponse`, pass the name of the template, the request object, and a "context" dictionary with key-value pairs to be used inside of the Jinja2 template. -```Python hl_lines="4 11 15-16" +```Python hl_lines="4 11 15-18" {!../../../docs_src/templates/tutorial001.py!} ``` !!! note - Notice that you have to pass the `request` as part of the key-value pairs in the context for Jinja2. So, you also have to declare it in your *path operation*. + Before FastAPI 0.108.0, Starlette 0.29.0, the `name` was the first parameter. + + Also, before that, in previous versions, the `request` object was passed as part of the key-value pairs in the context for Jinja2. !!! tip By declaring `response_class=HTMLResponse` the docs UI will be able to know that the response will be HTML. @@ -44,21 +46,61 @@ $ pip install jinja2 ## Writing templates -Then you can write a template at `templates/item.html` with: +Then you can write a template at `templates/item.html` with, for example: ```jinja hl_lines="7" {!../../../docs_src/templates/templates/item.html!} ``` -It will show the `id` taken from the "context" `dict` you passed: +### Template Context Values + +In the HTML that contains: + +{% raw %} + +```jinja +Item ID: {{ id }} +``` + +{% endraw %} + +...it will show the `id` taken from the "context" `dict` you passed: ```Python -{"request": request, "id": id} +{"id": id} +``` + +For example, with an ID of `42`, this would render: + +```html +Item ID: 42 +``` + +### Template `url_for` Arguments + +You can also use `url_for()` inside of the template, it takes as arguments the same arguments that would be used by your *path operation function*. + +So, the section with: + +{% raw %} + +```jinja + +``` + +{% endraw %} + +...will generate a link to the same URL that would be handled by the *path operation function* `read_item(id=id)`. + +For example, with an ID of `42`, this would render: + +```html + ``` ## Templates and static files -And you can also use `url_for()` inside of the template, and use it, for example, with the `StaticFiles` you mounted. +You can also use `url_for()` inside of the template, and use it, for example, with the `StaticFiles` you mounted with the `name="static"`. ```jinja hl_lines="4" {!../../../docs_src/templates/templates/item.html!} diff --git a/docs/en/docs/advanced/testing-database.md b/docs/en/docs/advanced/testing-database.md index 16484b09af..1c0669b9ce 100644 --- a/docs/en/docs/advanced/testing-database.md +++ b/docs/en/docs/advanced/testing-database.md @@ -1,5 +1,12 @@ # Testing a Database +!!! info + These docs are about to be updated. 🎉 + + The current version assumes Pydantic v1, and SQLAlchemy versions less than 2.0. + + The new docs will include Pydantic v2 and will use SQLModel (which is also based on SQLAlchemy) once it is updated to use Pydantic v2 as well. + You can use the same dependency overrides from [Testing Dependencies with Overrides](testing-dependencies.md){.internal-link target=_blank} to alter a database for testing. You could want to set up a different database for testing, rollback the data after the tests, pre-fill it with some testing data, etc. @@ -44,7 +51,7 @@ So the new file structure looks like: First, we create a new database session with the new database. -For the tests we'll use a file `test.db` instead of `sql_app.db`. +We'll use an in-memory database that persists during the tests instead of the local file `sql_app.db`. But the rest of the session code is more or less the same, we just copy it. diff --git a/docs/en/docs/advanced/testing-dependencies.md b/docs/en/docs/advanced/testing-dependencies.md index 7bba82fb7f..57dd87f569 100644 --- a/docs/en/docs/advanced/testing-dependencies.md +++ b/docs/en/docs/advanced/testing-dependencies.md @@ -28,9 +28,41 @@ To override a dependency for testing, you put as a key the original dependency ( And then **FastAPI** will call that override instead of the original dependency. -```Python hl_lines="28-29 32" -{!../../../docs_src/dependency_testing/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="26-27 30" + {!> ../../../docs_src/dependency_testing/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="28-29 32" + {!> ../../../docs_src/dependency_testing/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="29-30 33" + {!> ../../../docs_src/dependency_testing/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="24-25 28" + {!> ../../../docs_src/dependency_testing/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="28-29 32" + {!> ../../../docs_src/dependency_testing/tutorial001.py!} + ``` !!! tip You can set a dependency override for a dependency used anywhere in your **FastAPI** application. diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index 3cf840819f..b8dfab1d1f 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -112,9 +112,41 @@ In WebSocket endpoints you can import from `fastapi` and use: They work the same way as for other FastAPI endpoints/*path operations*: -```Python hl_lines="66-77 76-91" -{!../../../docs_src/websockets/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="68-69 82" + {!> ../../../docs_src/websockets/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="68-69 82" + {!> ../../../docs_src/websockets/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="69-70 83" + {!> ../../../docs_src/websockets/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="66-67 79" + {!> ../../../docs_src/websockets/tutorial002_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="68-69 81" + {!> ../../../docs_src/websockets/tutorial002.py!} + ``` !!! info As this is a WebSocket it doesn't really make sense to raise an `HTTPException`, instead we raise a `WebSocketException`. @@ -153,9 +185,17 @@ With that you can connect the WebSocket and then send and receive messages: When a WebSocket connection is closed, the `await websocket.receive_text()` will raise a `WebSocketDisconnect` exception, which you can then catch and handle like in this example. -```Python hl_lines="81-83" -{!../../../docs_src/websockets/tutorial003.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="79-81" + {!> ../../../docs_src/websockets/tutorial003_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="81-83" + {!> ../../../docs_src/websockets/tutorial003.py!} + ``` To try it out: @@ -172,7 +212,7 @@ Client #1596980209979 left the chat !!! tip The app above is a minimal and simple example to demonstrate how to handle and broadcast messages to several WebSocket connections. - But have in mind that, as everything is handled in memory, in a single list, it will only work while the process is running, and will only work with a single process. + But keep in mind that, as everything is handled in memory, in a single list, it will only work while the process is running, and will only work with a single process. If you need something easy to integrate with FastAPI but that is more robust, supported by Redis, PostgreSQL or others, check encode/broadcaster. diff --git a/docs/en/docs/advanced/wsgi.md b/docs/en/docs/advanced/wsgi.md index df88659617..cfe3c78c11 100644 --- a/docs/en/docs/advanced/wsgi.md +++ b/docs/en/docs/advanced/wsgi.md @@ -12,7 +12,7 @@ Then wrap the WSGI (e.g. Flask) app with the middleware. And then mount that under a path. -```Python hl_lines="2-3 22" +```Python hl_lines="2-3 23" {!../../../docs_src/wsgi/tutorial001.py!} ``` diff --git a/docs/en/docs/alternatives.md b/docs/en/docs/alternatives.md index 0f074ccf32..70bbcac91c 100644 --- a/docs/en/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -185,13 +185,13 @@ It's a Flask plug-in, that ties together Webargs, Marshmallow and APISpec. It uses the information from Webargs and Marshmallow to automatically generate OpenAPI schemas, using APISpec. -It's a great tool, very under-rated. It should be way more popular than many Flask plug-ins out there. It might be due to its documentation being too concise and abstract. +It's a great tool, very underrated. It should be way more popular than many Flask plug-ins out there. It might be due to its documentation being too concise and abstract. This solved having to write YAML (another syntax) inside of Python docstrings. This combination of Flask, Flask-apispec with Marshmallow and Webargs was my favorite backend stack until building **FastAPI**. -Using it led to the creation of several Flask full-stack generators. These are the main stack I (and several external teams) have been using up to now: +Using it led to the creation of several Flask full-stack generators. These are the main stacks I (and several external teams) have been using up to now: * https://github.com/tiangolo/full-stack * https://github.com/tiangolo/full-stack-flask-couchbase @@ -211,7 +211,7 @@ This isn't even Python, NestJS is a JavaScript (TypeScript) NodeJS framework ins It achieves something somewhat similar to what can be done with Flask-apispec. -It has an integrated dependency injection system, inspired by Angular two. It requires pre-registering the "injectables" (like all the other dependency injection systems I know), so, it adds to the verbosity and code repetition. +It has an integrated dependency injection system, inspired by Angular 2. It requires pre-registering the "injectables" (like all the other dependency injection systems I know), so, it adds to the verbosity and code repetition. As the parameters are described with TypeScript types (similar to Python type hints), editor support is quite good. @@ -263,7 +263,7 @@ I discovered Molten in the first stages of building **FastAPI**. And it has quit It doesn't use a data validation, serialization and documentation third-party library like Pydantic, it has its own. So, these data type definitions would not be reusable as easily. -It requires a little bit more verbose configurations. And as it is based on WSGI (instead of ASGI), it is not designed to take advantage of the high-performance provided by tools like Uvicorn, Starlette and Sanic. +It requires a little bit more verbose configurations. And as it is based on WSGI (instead of ASGI), it is not designed to take advantage of the high performance provided by tools like Uvicorn, Starlette and Sanic. The dependency injection system requires pre-registration of the dependencies and the dependencies are solved based on the declared types. So, it's not possible to declare more than one "component" that provides a certain type. @@ -357,7 +357,7 @@ It is comparable to Marshmallow. Although it's faster than Marshmallow in benchm ### Starlette -Starlette is a lightweight ASGI framework/toolkit, which is ideal for building high-performance asyncio services. +Starlette is a lightweight ASGI framework/toolkit, which is ideal for building high-performance asyncio services. It is very simple and intuitive. It's designed to be easily extensible, and have modular components. diff --git a/docs/en/docs/async.md b/docs/en/docs/async.md index 3d4b1956af..2ead1f2db7 100644 --- a/docs/en/docs/async.md +++ b/docs/en/docs/async.md @@ -409,11 +409,11 @@ Still, in both situations, chances are that **FastAPI** will [still be faster](/ ### Dependencies -The same applies for [dependencies](/tutorial/dependencies/index.md){.internal-link target=_blank}. If a dependency is a standard `def` function instead of `async def`, it is run in the external threadpool. +The same applies for [dependencies](./tutorial/dependencies/index.md){.internal-link target=_blank}. If a dependency is a standard `def` function instead of `async def`, it is run in the external threadpool. ### Sub-dependencies -You can have multiple dependencies and [sub-dependencies](/tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} requiring each other (as parameters of the function definitions), some of them might be created with `async def` and some with normal `def`. It would still work, and the ones created with normal `def` would be called on an external thread (from the threadpool) instead of being "awaited". +You can have multiple dependencies and [sub-dependencies](./tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} requiring each other (as parameters of the function definitions), some of them might be created with `async def` and some with normal `def`. It would still work, and the ones created with normal `def` would be called on an external thread (from the threadpool) instead of being "awaited". ### Other utility functions diff --git a/docs/en/docs/benchmarks.md b/docs/en/docs/benchmarks.md index e05fec8406..d746b6d7c4 100644 --- a/docs/en/docs/benchmarks.md +++ b/docs/en/docs/benchmarks.md @@ -2,7 +2,7 @@ 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). (*) -But when checking benchmarks and comparisons you should have the following in mind. +But when checking benchmarks and comparisons you should keep the following in mind. ## Benchmarks and speed diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 58a3632202..2d308a9dbd 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -4,11 +4,11 @@ First, you might want to see the basic ways to [help FastAPI and get help](help- ## Developing -If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment. +If you already cloned the fastapi repository and you want to deep dive in the code, here are some guidelines to set up your environment. ### Virtual environment with `venv` -You can create a virtual environment in a directory using Python's `venv` module: +You can create an isolated virtual local environment in a directory using Python's `venv` module. Let's do this in the cloned repository (where the `requirements.txt` is):
@@ -18,7 +18,7 @@ $ python -m venv env
-That will create a directory `./env/` with the Python binaries and then you will be able to install packages for that isolated environment. +That will create a directory `./env/` with the Python binaries, and then you will be able to install packages for that local environment. ### Activate the environment @@ -84,7 +84,7 @@ To check it worked, use: If it shows the `pip` binary at `env/bin/pip` then it worked. 🎉 -Make sure you have the latest pip version on your virtual environment to avoid errors on the next steps: +Make sure you have the latest pip version on your local environment to avoid errors on the next steps:
@@ -101,14 +101,14 @@ $ python -m pip install --upgrade pip This makes sure that if you use a terminal program installed by that package, you use the one from your local environment and not any other that could be installed globally. -### pip +### Install requirements using pip After activating the environment as described above:
```console -$ pip install -e ."[dev,doc,test]" +$ pip install -r requirements.txt ---> 100% ``` @@ -117,15 +117,20 @@ $ pip install -e ."[dev,doc,test]" It will install all the dependencies and your local FastAPI in your local environment. -#### Using your local FastAPI +### Using your local FastAPI -If you create a Python file that imports and uses FastAPI, and run it with the Python from your local environment, it will use your local FastAPI source code. +If you create a Python file that imports and uses FastAPI, and run it with the Python from your local environment, it will use your cloned local FastAPI source code. -And if you update that local FastAPI source code, as it is installed with `-e`, when you run that Python file again, it will use the fresh version of FastAPI you just edited. +And if you update that local FastAPI source code when you run that Python file again, it will use the fresh version of FastAPI you just edited. That way, you don't have to "install" your local version to be able to test every change. -### Format +!!! note "Technical Details" + This only happens when you install using this included `requirements.txt` instead of running `pip install fastapi` directly. + + That is because inside the `requirements.txt` file, the local version of FastAPI is marked to be installed in "editable" mode, with the `-e` option. + +### Format the code There is a script that you can run that will format and clean all your code: @@ -145,6 +150,62 @@ For it to sort them correctly, you need to have FastAPI installed locally in you First, make sure you set up your environment as described above, that will install all the requirements. +### Docs live + +During local development, there is a script that builds the site and checks for any changes, live-reloading: + +
+ +```console +$ python ./scripts/docs.py live + +[INFO] Serving on http://127.0.0.1:8008 +[INFO] Start watching changes +[INFO] Start detecting changes +``` + +
+ +It will serve the documentation on `http://127.0.0.1:8008`. + +That way, you can edit the documentation/source files and see the changes live. + +!!! tip + Alternatively, you can perform the same steps that scripts does manually. + + Go into the language directory, for the main docs in English it's at `docs/en/`: + + ```console + $ cd docs/en/ + ``` + + Then run `mkdocs` in that directory: + + ```console + $ mkdocs serve --dev-addr 8008 + ``` + +#### Typer CLI (optional) + +The instructions here show you how to use the script at `./scripts/docs.py` with the `python` program directly. + +But you can also use Typer CLI, and you will get autocompletion in your terminal for the commands after installing completion. + +If you install Typer CLI, you can install completion with: + +
+ +```console +$ typer --install-completion + +zsh completion installed in /home/user/.bashrc. +Completion will take effect once you restart the terminal. +``` + +
+ +### Docs Structure + The documentation uses MkDocs. And there are extra tools/scripts in place to handle translations in `./scripts/docs.py`. @@ -166,50 +227,13 @@ And those Python files are included/injected in the documentation when generatin Most of the tests actually run against the example source files in the documentation. -This helps making sure that: +This helps to make sure that: -* The documentation is up to date. +* The documentation is up-to-date. * The documentation examples can be run as is. * Most of the features are covered by the documentation, ensured by test coverage. -During local development, there is a script that builds the site and checks for any changes, live-reloading: - -
- -```console -$ python ./scripts/docs.py live - -[INFO] Serving on http://127.0.0.1:8008 -[INFO] Start watching changes -[INFO] Start detecting changes -``` - -
- -It will serve the documentation on `http://127.0.0.1:8008`. - -That way, you can edit the documentation/source files and see the changes live. - -#### Typer CLI (optional) - -The instructions here show you how to use the script at `./scripts/docs.py` with the `python` program directly. - -But you can also use Typer CLI, and you will get autocompletion in your terminal for the commands after installing completion. - -If you install Typer CLI, you can install completion with: - -
- -```console -$ typer --install-completion - -zsh completion installed in /home/user/.bashrc. -Completion will take effect once you restart the terminal. -``` - -
- -### Apps and docs at the same time +#### Apps and docs at the same time If you run the examples with, e.g.: @@ -233,26 +257,20 @@ Here are the steps to help with translations. #### Tips and guidelines -* Check the currently existing pull requests for your language and add reviews requesting changes or approving them. +* Check the currently existing pull requests for your language. You can filter the pull requests by the ones with the label for your language. For example, for Spanish, the label is `lang-es`. + +* Review those pull requests, requesting changes or approving them. For the languages I don't speak, I'll wait for several others to review the translation before merging. !!! tip You can add comments with change suggestions to existing pull requests. Check the docs about adding a pull request review to approve it or request changes. -* Check in the issues to see if there's one coordinating translations for your language. +* Check if there's a GitHub Discussion to coordinate translations for your language. You can subscribe to it, and when there's a new pull request to review, an automatic comment will be added to the discussion. -* Add a single pull request per page translated. That will make it much easier for others to review it. +* If you translate pages, add a single pull request per page translated. That will make it much easier for others to review it. -For the languages I don't speak, I'll wait for several others to review the translation before merging. - -* You can also check if there are translations for your language and add a review to them, that will help me know that the translation is correct and I can merge it. - -* Use the same Python examples and only translate the text in the docs. You don't have to change anything for this to work. - -* Use the same images, file names, and links. You don't have to change anything for it to work. - -* To check the 2-letter code for the language you want to translate you can use the table List of ISO 639-1 codes. +* To check the 2-letter code for the language you want to translate, you can use the table List of ISO 639-1 codes. #### Existing language @@ -278,11 +296,24 @@ $ python ./scripts/docs.py live es
+!!! tip + Alternatively, you can perform the same steps that scripts does manually. + + Go into the language directory, for the Spanish translations it's at `docs/es/`: + + ```console + $ cd docs/es/ + ``` + + Then run `mkdocs` in that directory: + + ```console + $ mkdocs serve --dev-addr 8008 + ``` + Now you can go to http://127.0.0.1:8008 and see your changes live. -If you look at the FastAPI docs website, you will see that every language has all the pages. But some pages are not translated and have a notification about the missing translation. - -But when you run it locally like this, you will only see the pages that are already translated. +You will see that every language has all the pages. But some pages are not translated and have an info box at the top, about the missing translation. Now let's say that you want to add a translation for the section [Features](features.md){.internal-link target=_blank}. @@ -301,47 +332,7 @@ docs/es/docs/features.md !!! tip Notice that the only change in the path and file name is the language code, from `en` to `es`. -* Now open the MkDocs config file for English at: - -``` -docs/en/mkdocs.yml -``` - -* Find the place where that `docs/features.md` is located in the config file. Somewhere like: - -```YAML hl_lines="8" -site_name: FastAPI -# More stuff -nav: -- FastAPI: index.md -- Languages: - - en: / - - es: /es/ -- features.md -``` - -* Open the MkDocs config file for the language you are editing, e.g.: - -``` -docs/es/mkdocs.yml -``` - -* Add it there at the exact same location it was for English, e.g.: - -```YAML hl_lines="8" -site_name: FastAPI -# More stuff -nav: -- FastAPI: index.md -- Languages: - - en: / - - es: /es/ -- features.md -``` - -Make sure that if there are other entries, the new entry with your translation is exactly in the same order as in the English version. - -If you go to your browser you will see that now the docs show your new section. 🎉 +If you go to your browser you will see that now the docs show your new section (the info box at the top is gone). 🎉 Now you can translate it all and see how it looks as you save the file. @@ -362,55 +353,32 @@ The next step is to run the script to generate a new translation directory: $ python ./scripts/docs.py new-lang ht Successfully initialized: docs/ht -Updating ht -Updating en ```
Now you can check in your code editor the newly created directory `docs/ht/`. +That command created a file `docs/ht/mkdocs.yml` with a simple config that inherits everything from the `en` version: + +```yaml +INHERIT: ../en/mkdocs.yml +``` + !!! tip - Create a first pull request with just this, to set up the configuration for the new language, before adding translations. + You could also simply create that file with those contents manually. - That way others can help with other pages while you work on the first one. 🚀 +That command also created a dummy file `docs/ht/index.md` for the main page, you can start by translating that one. -Start by translating the main page, `docs/ht/index.md`. +You can continue with the previous instructions for an "Existing Language" for that process. -Then you can continue with the previous instructions, for an "Existing Language". - -##### New Language not supported - -If when running the live server script you get an error about the language not being supported, something like: - -``` - raise TemplateNotFound(template) -jinja2.exceptions.TemplateNotFound: partials/language/xx.html -``` - -That means that the theme doesn't support that language (in this case, with a fake 2-letter code of `xx`). - -But don't worry, you can set the theme language to English and then translate the content of the docs. - -If you need to do that, edit the `mkdocs.yml` for your new language, it will have something like: - -```YAML hl_lines="5" -site_name: FastAPI -# More stuff -theme: - # More stuff - language: xx -``` - -Change that language from `xx` (from your language code) to `en`. - -Then you can start the live server again. +You can make the first pull request with those two files, `docs/ht/mkdocs.yml` and `docs/ht/index.md`. 🎉 #### Preview the result -When you use the script at `./scripts/docs.py` with the `live` command it only shows the files and translations available for the current language. +As already mentioned above, you can use the `./scripts/docs.py` with the `live` command to preview the results (or `mkdocs serve`). -But once you are done, you can test it all as it would look online. +Once you are done, you can also test it all as it would look online, including all the other languages. To do that, first build all the docs: @@ -420,19 +388,14 @@ To do that, first build all the docs: // 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 ``` -That generates all the docs at `./docs_build/` for each language. This includes adding any files with missing translations, with a note saying that "this file doesn't have a translation yet". But you don't have to do anything with that directory. - -Then it builds all those independent MkDocs sites for each language, combines them, and generates the final output at `./site/`. +This builds all those independent MkDocs sites for each language, combines them, and generates the final output at `./site/`. Then you can serve that with the command `serve`: @@ -450,6 +413,25 @@ Serving at: http://127.0.0.1:8008 +#### Translation specific tips and guidelines + +* Translate only the Markdown documents (`.md`). Do not translate the code examples at `./docs_src`. + +* In code blocks within the Markdown document, translate comments (`# a comment`), but leave the rest unchanged. + +* Do not change anything enclosed in "``" (inline code). + +* In lines starting with `===` or `!!!`, translate only the ` "... Text ..."` part. Leave the rest unchanged. + +* You can translate info boxes like `!!! warning` with for example `!!! warning "Achtung"`. But do not change the word immediately after the `!!!`, it determines the color of the info box. + +* Do not change the paths in links to images, code files, Markdown documents. + +* However, when a Markdown document is translated, the `#hash-parts` in links to its headings may change. Update these links if possible. + * Search for such links in the translated document using the regex `#[^# ]`. + * Search in all documents already translated into your language for `your-translated-document.md`. For example VS Code has an option "Edit" -> "Find in Files". + * When translating a document, do not "pre-translate" `#hash-parts` that link to headings in untranslated documents. + ## Tests There is a script that you can run locally to test all the code and generate coverage reports in HTML: diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 066b51725e..187040792b 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -144,3 +144,39 @@ code { margin-top: 2em; margin-bottom: 2em; } + +/* Screenshots */ +/* +Simulate a browser window frame. +Inspired by Termynal's CSS tricks with modifications +*/ + +.screenshot { + display: block; + background-color: #d3e0de; + border-radius: 4px; + padding: 45px 5px 5px; + position: relative; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +.screenshot img { + display: block; + border-radius: 2px; +} + +.screenshot:before { + content: ''; + position: absolute; + top: 15px; + left: 15px; + display: inline-block; + width: 15px; + height: 15px; + border-radius: 50%; + /* A little hack to display the window buttons in one pseudo element. */ + background: #d9515d; + -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; + box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; +} diff --git a/docs/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md new file mode 100644 index 0000000000..b2836aeb49 --- /dev/null +++ b/docs/en/docs/deployment/cloud.md @@ -0,0 +1,17 @@ +# Deploy FastAPI on Cloud Providers + +You can use virtually **any cloud provider** to deploy your FastAPI application. + +In most of the cases, the main cloud providers have guides to deploy FastAPI with them. + +## Cloud Providers - Sponsors + +Some cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, this ensures the continued and healthy **development** of FastAPI and its **ecosystem**. + +And it shows their true commitment to FastAPI and its **community** (you), as they not only want to provide you a **good service** but also want to make sure you have a **good and healthy framework**, FastAPI. 🙇 + +You might want to try their services and follow their guides: + +* Platform.sh +* Porter +* Deta diff --git a/docs/en/docs/deployment/concepts.md b/docs/en/docs/deployment/concepts.md index 77419f8b0d..cc01fb24e1 100644 --- a/docs/en/docs/deployment/concepts.md +++ b/docs/en/docs/deployment/concepts.md @@ -258,7 +258,7 @@ And you will have to make sure that it's a single process running those previous Of course, there are some cases where there's no problem in running the previous steps multiple times, in that case, it's a lot easier to handle. !!! tip - Also, have in mind that depending on your setup, in some cases you **might not even need any previous steps** before starting your application. + Also, keep in mind that depending on your setup, in some cases you **might not even need any previous steps** before starting your application. In that case, you wouldn't have to worry about any of this. 🤷 @@ -297,7 +297,7 @@ You can use simple tools like `htop` to see the CPU and RAM used in your server ## Recap -You have been reading here some of the main concepts that you would probably need to have in mind when deciding how to deploy your application: +You have been reading here some of the main concepts that you would probably need to keep in mind when deciding how to deploy your application: * Security - HTTPS * Running on startup diff --git a/docs/en/docs/deployment/deta.md b/docs/en/docs/deployment/deta.md deleted file mode 100644 index c0dc3336a9..0000000000 --- a/docs/en/docs/deployment/deta.md +++ /dev/null @@ -1,258 +0,0 @@ -# Deploy FastAPI on Deta - -In this section you will learn how to easily deploy a **FastAPI** application on Deta using the free plan. 🎁 - -It will take you about **10 minutes**. - -!!! info - Deta is a **FastAPI** sponsor. 🎉 - -## A basic **FastAPI** app - -* Create a directory for your app, for example, `./fastapideta/` and enter into it. - -### FastAPI code - -* Create a `main.py` file 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): - return {"item_id": item_id} -``` - -### Requirements - -Now, in the same directory create a file `requirements.txt` with: - -```text -fastapi -``` - -!!! tip - You don't need to install Uvicorn to deploy on Deta, although you would probably want to install it locally to test your app. - -### Directory structure - -You will now have one directory `./fastapideta/` with two files: - -``` -. -└── main.py -└── requirements.txt -``` - -## Create a free Deta account - -Now create a free account on Deta, you just need an email and password. - -You don't even need a credit card. - -## Install the CLI - -Once you have your account, install the Deta CLI: - -=== "Linux, macOS" - -
- - ```console - $ curl -fsSL https://get.deta.dev/cli.sh | sh - ``` - -
- -=== "Windows PowerShell" - -
- - ```console - $ iwr https://get.deta.dev/cli.ps1 -useb | iex - ``` - -
- -After installing it, open a new terminal so that the installed CLI is detected. - -In a new terminal, confirm that it was correctly installed with: - -
- -```console -$ deta --help - -Deta command line interface for managing deta micros. -Complete documentation available at https://docs.deta.sh - -Usage: - deta [flags] - deta [command] - -Available Commands: - auth Change auth settings for a deta micro - -... -``` - -
- -!!! tip - If you have problems installing the CLI, check the official Deta docs. - -## Login with the CLI - -Now login to Deta from the CLI with: - -
- -```console -$ deta login - -Please, log in from the web page. Waiting.. -Logged in successfully. -``` - -
- -This will open a web browser and authenticate automatically. - -## Deploy with Deta - -Next, deploy your application with the Deta CLI: - -
- -```console -$ deta new - -Successfully created a new micro - -// Notice the "endpoint" 🔍 - -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} - -Adding dependencies... - - ----> 100% - - -Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6 -``` - -
- -You will see a JSON message similar to: - -```JSON hl_lines="4" -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} -``` - -!!! tip - Your deployment will have a different `"endpoint"` URL. - -## Check it - -Now open your browser in your `endpoint` URL. In the example above it was `https://qltnci.deta.dev`, but yours will be different. - -You will see the JSON response from your FastAPI app: - -```JSON -{ - "Hello": "World" -} -``` - -And now go to the `/docs` for your API, in the example above it would be `https://qltnci.deta.dev/docs`. - -It will show your docs like: - - - -## Enable public access - -By default, Deta will handle authentication using cookies for your account. - -But once you are ready, you can make it public with: - -
- -```console -$ deta auth disable - -Successfully disabled http auth -``` - -
- -Now you can share that URL with anyone and they will be able to access your API. 🚀 - -## HTTPS - -Congrats! You deployed your FastAPI app to Deta! 🎉 🍰 - -Also, notice that Deta correctly handles HTTPS for you, so you don't have to take care of that and can be sure that your clients will have a secure encrypted connection. ✅ 🔒 - -## Check the Visor - -From your docs UI (they will be in a URL like `https://qltnci.deta.dev/docs`) send a request to your *path operation* `/items/{item_id}`. - -For example with ID `5`. - -Now go to https://web.deta.sh. - -You will see there's a section to the left called "Micros" with each of your apps. - -You will see a tab with "Details", and also a tab "Visor", go to the tab "Visor". - -In there you can inspect the recent requests sent to your app. - -You can also edit them and re-play them. - - - -## Learn more - -At some point, you will probably want to store some data for your app in a way that persists through time. For that you can use Deta Base, it also has a generous **free tier**. - -You can also read more in the Deta Docs. - -## Deployment Concepts - -Coming back to the concepts we discussed in [Deployments Concepts](./concepts.md){.internal-link target=_blank}, here's how each of them would be handled with Deta: - -* **HTTPS**: Handled by Deta, they will give you a subdomain and handle HTTPS automatically. -* **Running on startup**: Handled by Deta, as part of their service. -* **Restarts**: Handled by Deta, as part of their service. -* **Replication**: Handled by Deta, as part of their service. -* **Memory**: Limit predefined by Deta, you could contact them to increase it. -* **Previous steps before starting**: Not directly supported, you could make it work with their Cron system or additional scripts. - -!!! note - Deta is designed to make it easy (and free) to deploy simple applications quickly. - - It can simplify several use cases, but at the same time, it doesn't support others, like using external databases (apart from Deta's own NoSQL database system), custom virtual machines, etc. - - You can read more details in the Deta docs to see if it's the right choice for you. diff --git a/docs/en/docs/deployment/https.md b/docs/en/docs/deployment/https.md index 790976a718..5cf76c1111 100644 --- a/docs/en/docs/deployment/https.md +++ b/docs/en/docs/deployment/https.md @@ -9,7 +9,7 @@ But it is way more complex than that. To **learn the basics of HTTPS**, from a consumer perspective, check https://howhttps.works/. -Now, from a **developer's perspective**, here are several things to have in mind while thinking about HTTPS: +Now, from a **developer's perspective**, here are several things to keep in mind while thinking about HTTPS: * For HTTPS, **the server** needs to **have "certificates"** generated by a **third party**. * Those certificates are actually **acquired** from the third party, not "generated". diff --git a/docs/en/docs/deployment/index.md b/docs/en/docs/deployment/index.md index f0fd001cd5..b43bd050a3 100644 --- a/docs/en/docs/deployment/index.md +++ b/docs/en/docs/deployment/index.md @@ -1,4 +1,4 @@ -# Deployment - Intro +# Deployment Deploying a **FastAPI** application is relatively easy. @@ -16,6 +16,6 @@ There are several ways to do it depending on your specific use case and the tool You could **deploy a server** yourself using a combination of tools, you could use a **cloud service** that does part of the work for you, or other possible options. -I will show you some of the main concepts you should probably have in mind when deploying a **FastAPI** application (although most of it applies to any other type of web application). +I will show you some of the main concepts you should probably keep in mind when deploying a **FastAPI** application (although most of it applies to any other type of web application). -You will see more details to have in mind and some of the techniques to do it in the next sections. ✨ +You will see more details to keep in mind and some of the techniques to do it in the next sections. ✨ diff --git a/docs/en/docs/deployment/manually.md b/docs/en/docs/deployment/manually.md index d6892b2c14..b10a3686d7 100644 --- a/docs/en/docs/deployment/manually.md +++ b/docs/en/docs/deployment/manually.md @@ -10,11 +10,11 @@ There are 3 main alternatives: ## Server Machine and Server Program -There's a small detail about names to have in mind. 💡 +There's a small detail about names to keep in mind. 💡 The word "**server**" is commonly used to refer to both the remote/cloud computer (the physical or virtual machine) and also the program that is running on that machine (e.g. Uvicorn). -Just have that in mind when you read "server" in general, it could refer to one of those two things. +Just keep in mind that when you read "server" in general, it could refer to one of those two things. When referring to the remote machine, it's common to call it **server**, but also **machine**, **VM** (virtual machine), **node**. Those all refer to some type of remote machine, normally running Linux, where you run programs. diff --git a/docs/en/docs/deployment/server-workers.md b/docs/en/docs/deployment/server-workers.md index 4ccd9d9f69..2df9f3d432 100644 --- a/docs/en/docs/deployment/server-workers.md +++ b/docs/en/docs/deployment/server-workers.md @@ -90,7 +90,9 @@ Let's see what each of those options mean: ``` * So, the colon in `main:app` would be equivalent to the Python `import` part in `from main import app`. + * `--workers`: The number of worker processes to use, each will run a Uvicorn worker, in this case, 4 workers. + * `--worker-class`: The Gunicorn-compatible worker class to use in the worker processes. * Here we pass the class that Gunicorn can import and use with: diff --git a/docs/en/docs/external-links.md b/docs/en/docs/external-links.md index 55db555994..b89021ee2c 100644 --- a/docs/en/docs/external-links.md +++ b/docs/en/docs/external-links.md @@ -9,70 +9,21 @@ Here's an incomplete list of some of them. !!! tip If you have an article, project, tool, or anything related to **FastAPI** that is not yet listed here, create a Pull Request adding it. -## Articles +{% for section_name, section_content in external_links.items() %} -### English +## {{ section_name }} -{% if external_links %} -{% for article in external_links.articles.english %} +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* {{ item.title }} by {{ item.author }}. -* {{ article.title }} by {{ article.author }}. {% endfor %} -{% endif %} - -### Japanese - -{% if external_links %} -{% for article in external_links.articles.japanese %} - -* {{ article.title }} by {{ article.author }}. {% endfor %} -{% endif %} - -### Vietnamese - -{% if external_links %} -{% for article in external_links.articles.vietnamese %} - -* {{ article.title }} by {{ article.author }}. {% endfor %} -{% endif %} - -### Russian - -{% if external_links %} -{% for article in external_links.articles.russian %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} - -### German - -{% if external_links %} -{% for article in external_links.articles.german %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} - -## Podcasts - -{% if external_links %} -{% for article in external_links.podcasts.english %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} - -## Talks - -{% if external_links %} -{% for article in external_links.talks.english %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} ## Projects diff --git a/docs/en/docs/fastapi-people.md b/docs/en/docs/fastapi-people.md index 0cbcb69c9a..7e26358d89 100644 --- a/docs/en/docs/fastapi-people.md +++ b/docs/en/docs/fastapi-people.md @@ -1,3 +1,8 @@ +--- +hide: + - navigation +--- + # FastAPI People FastAPI has an amazing community that welcomes people from all backgrounds. @@ -28,7 +33,7 @@ I'm the creator and maintainer of **FastAPI**. You can read more about that in [ These are the people that: -* [Help others with issues (questions) in GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank}. +* [Help others with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank}. * [Create Pull Requests](help-fastapi.md#create-a-pull-request){.internal-link target=_blank}. * Review Pull Requests, [especially important for translations](contributing.md#translations){.internal-link target=_blank}. @@ -36,13 +41,13 @@ A round of applause to them. 👏 🙇 ## Most active users last month -These are the users that have been [helping others the most with issues (questions) in GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} during the last month. ☕ +These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last month. ☕ {% if people %}
{% for user in people.last_month_active %} -
@{{ user.login }}
Issues replied: {{ user.count }}
+
@{{ user.login }}
Questions replied: {{ user.count }}
{% endfor %}
@@ -52,7 +57,7 @@ These are the users that have been [helping others the most with issues (questio Here are the **FastAPI Experts**. 🤓 -These are the users that have [helped others the most with issues (questions) in GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} through *all time*. +These are the users that have [helped others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} through *all time*. They have proven to be experts by helping many others. ✨ @@ -60,7 +65,7 @@ They have proven to be experts by helping many others. ✨
{% for user in people.experts %} -
@{{ user.login }}
Issues replied: {{ user.count }}
+
@{{ user.login }}
Questions replied: {{ user.count }}
{% endfor %}
@@ -169,7 +174,7 @@ They are supporting my work with **FastAPI** (and others), mainly through source code here. diff --git a/docs/en/docs/features.md b/docs/en/docs/features.md index 387ff86c99..6f13b03bb9 100644 --- a/docs/en/docs/features.md +++ b/docs/en/docs/features.md @@ -1,3 +1,8 @@ +--- +hide: + - navigation +--- + # Features ## FastAPI features @@ -189,8 +194,6 @@ With **FastAPI** you get all of **Pydantic**'s features (as FastAPI is based on * If you know Python types you know how to use Pydantic. * Plays nicely with your **IDE/linter/brain**: * Because pydantic data structures are just instances of classes you define; auto-completion, linting, mypy and your intuition should all work properly with your validated data. -* **Fast**: - * in benchmarks Pydantic is faster than all other tested libraries. * Validate **complex structures**: * Use of hierarchical Pydantic models, Python `typing`’s `List` and `Dict`, etc. * And validators allow complex data schemas to be clearly and easily defined, checked and documented as JSON Schema. diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index a7ac9415fc..71c5804097 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -69,11 +69,16 @@ I love to hear about how **FastAPI** is being used, what you have liked in it, i * Vote for **FastAPI** in AlternativeTo. * Say you use **FastAPI** on StackShare. -## Help others with issues in GitHub +## Help others with questions in GitHub -You can see existing issues and try and help others, most of the times those issues are questions that you might already know the answer for. 🤓 +You can try and help others with their questions in: -If you are helping a lot of people with issues, you might become an official [FastAPI Expert](fastapi-people.md#experts){.internal-link target=_blank}. 🎉 +* GitHub Discussions +* GitHub Issues + +In many cases you might already know the answer for those questions. 🤓 + +If you are helping a lot of people with their questions, you will become an official [FastAPI Expert](fastapi-people.md#experts){.internal-link target=_blank}. 🎉 Just remember, the most important point is: try to be kind. People come with their frustrations and in many cases don't ask in the best way, but try as best as you can to be kind. 🤗 @@ -81,7 +86,7 @@ The idea is for the **FastAPI** community to be kind and welcoming. At the same --- -Here's how to help others with issues: +Here's how to help others with questions (in discussions or issues): ### Understand the question @@ -101,7 +106,7 @@ In many cases they will only copy a fragment of the code, but that's not enough * You can ask them to provide a minimal, reproducible, example, that you can **copy-paste** and run locally to see the same error or behavior they are seeing, or to understand their use case better. -* If you are feeling too generous, you can try to **create an example** like that yourself, just based on the description of the problem. Just have in mind that this might take a lot of time and it might be better to ask them to clarify the problem first. +* If you are feeling too generous, you can try to **create an example** like that yourself, just based on the description of the problem. Just keep in mind that this might take a lot of time and it might be better to ask them to clarify the problem first. ### Suggest solutions @@ -113,24 +118,27 @@ In many cases they will only copy a fragment of the code, but that's not enough If they reply, there's a high chance you would have solved their problem, congrats, **you're a hero**! 🦸 -* Now you can ask them, if that solved their problem, to **close the issue**. +* Now, if that solved their problem, you can ask them to: + + * In GitHub Discussions: mark the comment as the **answer**. + * In GitHub Issues: **close** the issue. ## Watch the GitHub repository 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. +If you select "Watching" instead of "Releases only" you will receive notifications when someone creates a new issue or question. You can also specify that you only want to be notified about new issues, or discussions, or PRs, etc. -Then you can try and help them solve those issues. +Then you can try and help them solve those questions. -## Create issues +## Ask Questions -You can create a new issue in the GitHub repository, for example to: +You can create a new question in the GitHub repository, for example to: * Ask a **question** or ask about a **problem**. * Suggest a new **feature**. -**Note**: if you create an issue, then I'm going to ask you to also help others. 😉 +**Note**: if you do it, then I'm going to ask you to also help others. 😉 ## Review Pull Requests @@ -140,11 +148,11 @@ Again, please try your best to be kind. 🤗 --- -Here's what to have in mind and how to review a pull request: +Here's what to keep in mind and how to review a pull request: ### Understand the problem -* First, make sure you **understand the problem** that the pull request is trying to solve. It might have a longer discussion in an issue. +* First, make sure you **understand the problem** that the pull request is trying to solve. It might have a longer discussion in a GitHub Discussion or issue. * There's also a good chance that the pull request is not actually needed because the problem can be solved in a **different way**. Then you can suggest or ask about that. @@ -207,7 +215,7 @@ There's a lot of work to do, and for most of it, **YOU** can do it. The main tasks that you can do right now are: -* [Help others with issues in GitHub](#help-others-with-issues-in-github){.internal-link target=_blank} (see the section above). +* [Help others with questions in GitHub](#help-others-with-questions-in-github){.internal-link target=_blank} (see the section above). * [Review Pull Requests](#review-pull-requests){.internal-link target=_blank} (see the section above). Those two tasks are what **consume time the most**. That's the main work of maintaining FastAPI. @@ -219,19 +227,17 @@ If you can help me with that, **you are helping me maintain FastAPI** and making Join the 👥 Discord chat server 👥 and hang out with others in the FastAPI community. !!! tip - For questions, ask them in GitHub issues, there's a much better chance you will receive help by the [FastAPI Experts](fastapi-people.md#experts){.internal-link target=_blank}. + For questions, ask them in GitHub Discussions, there's a much better chance you will receive help by the [FastAPI Experts](fastapi-people.md#experts){.internal-link target=_blank}. Use the chat only for other general conversations. -There is also the previous Gitter chat, but as it doesn't have channels and advanced features, conversations are more difficult, so Discord is now the recommended system. - ### Don't use the chat for questions -Have in mind that as chats allow more "free conversation", it's easy to ask questions that are too general and more difficult to answer, so, you might not receive answers. +Keep in mind that as chats allow 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 you 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 chat systems. 😅 +In GitHub, the template will guide you 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 chat systems. 😅 -Conversations in the chat systems are also not as easily searchable as in GitHub, so questions and answers might get lost in the conversation. And only the ones in GitHub issues count to become a [FastAPI Expert](fastapi-people.md#experts){.internal-link target=_blank}, so you will most probably receive more attention in GitHub issues. +Conversations in the chat systems are also not as easily searchable as in GitHub, so questions and answers might get lost in the conversation. And only the ones in GitHub count to become a [FastAPI Expert](fastapi-people.md#experts){.internal-link target=_blank}, so you will most probably receive more attention in GitHub. On the other side, there are thousands of users in the chat systems, so there's a high chance you'll find someone to talk to there, almost all the time. 😄 diff --git a/docs/en/docs/help/index.md b/docs/en/docs/help/index.md new file mode 100644 index 0000000000..5ee7df2fef --- /dev/null +++ b/docs/en/docs/help/index.md @@ -0,0 +1,3 @@ +# Help + +Help and get help, contribute, get involved. 🤝 diff --git a/docs/en/docs/advanced/async-sql-databases.md b/docs/en/docs/how-to/async-sql-encode-databases.md similarity index 87% rename from docs/en/docs/advanced/async-sql-databases.md rename to docs/en/docs/how-to/async-sql-encode-databases.md index 93c288e1b4..0e2ccce78d 100644 --- a/docs/en/docs/advanced/async-sql-databases.md +++ b/docs/en/docs/how-to/async-sql-encode-databases.md @@ -1,4 +1,11 @@ -# Async SQL (Relational) Databases +# Async SQL (Relational) Databases with Encode/Databases + +!!! info + These docs are about to be updated. 🎉 + + The current version assumes Pydantic v1. + + The new docs will include Pydantic v2 and will use SQLModel once it is updated to use Pydantic v2 as well. You can also use `encode/databases` with **FastAPI** to connect to databases using `async` and `await`. @@ -107,6 +114,11 @@ Create the *path operation function* to create notes: {!../../../docs_src/async_sql_databases/tutorial001.py!} ``` +!!! info + In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. + + The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. + !!! Note Notice that as we communicate with the database using `await`, the *path operation function* is declared with `async`. diff --git a/docs/en/docs/advanced/conditional-openapi.md b/docs/en/docs/how-to/conditional-openapi.md similarity index 100% rename from docs/en/docs/advanced/conditional-openapi.md rename to docs/en/docs/how-to/conditional-openapi.md diff --git a/docs/en/docs/how-to/configure-swagger-ui.md b/docs/en/docs/how-to/configure-swagger-ui.md new file mode 100644 index 0000000000..f36ba5ba8c --- /dev/null +++ b/docs/en/docs/how-to/configure-swagger-ui.md @@ -0,0 +1,78 @@ +# Configure Swagger UI + +You can configure some extra Swagger UI parameters. + +To configure them, pass the `swagger_ui_parameters` argument when creating the `FastAPI()` app object or to the `get_swagger_ui_html()` function. + +`swagger_ui_parameters` receives a dictionary with the configurations passed to Swagger UI directly. + +FastAPI converts the configurations to **JSON** to make them compatible with JavaScript, as that's what Swagger UI needs. + +## Disable Syntax Highlighting + +For example, you could disable syntax highlighting in Swagger UI. + +Without changing the settings, syntax highlighting is enabled by default: + + + +But you can disable it by setting `syntaxHighlight` to `False`: + +```Python hl_lines="3" +{!../../../docs_src/configure_swagger_ui/tutorial001.py!} +``` + +...and then Swagger UI won't show the syntax highlighting anymore: + + + +## Change the Theme + +The same way you could set the syntax highlighting theme with the key `"syntaxHighlight.theme"` (notice that it has a dot in the middle): + +```Python hl_lines="3" +{!../../../docs_src/configure_swagger_ui/tutorial002.py!} +``` + +That configuration would change the syntax highlighting color theme: + + + +## Change Default Swagger UI Parameters + +FastAPI includes some default configuration parameters appropriate for most of the use cases. + +It includes these default configurations: + +```Python +{!../../../fastapi/openapi/docs.py[ln:7-13]!} +``` + +You can override any of them by setting a different value in the argument `swagger_ui_parameters`. + +For example, to disable `deepLinking` you could pass these settings to `swagger_ui_parameters`: + +```Python hl_lines="3" +{!../../../docs_src/configure_swagger_ui/tutorial003.py!} +``` + +## Other Swagger UI Parameters + +To see all the other possible configurations you can use, read the official docs for Swagger UI parameters. + +## JavaScript-only settings + +Swagger UI also allows other configurations to be **JavaScript-only** objects (for example, JavaScript functions). + +FastAPI also includes these JavaScript-only `presets` settings: + +```JavaScript +presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIBundle.SwaggerUIStandalonePreset +] +``` + +These are **JavaScript** objects, not strings, so you can't pass them from Python code directly. + +If you need to use JavaScript-only configurations like those, you can use one of the methods above. Override all the Swagger UI *path operation* and manually write any JavaScript you need. diff --git a/docs/en/docs/how-to/custom-docs-ui-assets.md b/docs/en/docs/how-to/custom-docs-ui-assets.md new file mode 100644 index 0000000000..9726be2c71 --- /dev/null +++ b/docs/en/docs/how-to/custom-docs-ui-assets.md @@ -0,0 +1,199 @@ +# Custom Docs UI Static Assets (Self-Hosting) + +The API docs use **Swagger UI** and **ReDoc**, and each of those need some JavaScript and CSS files. + +By default, those files are served from a CDN. + +But it's possible to customize it, you can set a specific CDN, or serve the files yourself. + +## Custom CDN for JavaScript and CSS + +Let's say that you want to use a different CDN, for example you want to use `https://unpkg.com/`. + +This could be useful if for example you live in a country that restricts some URLs. + +### Disable the automatic docs + +The first step is to disable the automatic docs, as by default, those use the default CDN. + +To disable them, set their URLs to `None` when creating your `FastAPI` app: + +```Python hl_lines="8" +{!../../../docs_src/custom_docs_ui/tutorial001.py!} +``` + +### Include the custom docs + +Now you can create the *path operations* for the custom docs. + +You can re-use FastAPI's internal functions to create the HTML pages for the docs, and pass them the needed arguments: + +* `openapi_url`: the URL where the HTML page for the docs can get the OpenAPI schema for your API. You can use here the attribute `app.openapi_url`. +* `title`: the title of your API. +* `oauth2_redirect_url`: you can use `app.swagger_ui_oauth2_redirect_url` here to use the default. +* `swagger_js_url`: the URL where the HTML for your Swagger UI docs can get the **JavaScript** file. This is the custom CDN URL. +* `swagger_css_url`: the URL where the HTML for your Swagger UI docs can get the **CSS** file. This is the custom CDN URL. + +And similarly for ReDoc... + +```Python hl_lines="2-6 11-19 22-24 27-33" +{!../../../docs_src/custom_docs_ui/tutorial001.py!} +``` + +!!! tip + The *path operation* for `swagger_ui_redirect` is a helper for when you use OAuth2. + + If you integrate your API with an OAuth2 provider, you will be able to authenticate and come back to the API docs with the acquired credentials. And interact with it using the real OAuth2 authentication. + + Swagger UI will handle it behind the scenes for you, but it needs this "redirect" helper. + +### Create a *path operation* to test it + +Now, to be able to test that everything works, create a *path operation*: + +```Python hl_lines="36-38" +{!../../../docs_src/custom_docs_ui/tutorial001.py!} +``` + +### Test it + +Now, you should be able to go to your docs at http://127.0.0.1:8000/docs, and reload the page, it will load those assets from the new CDN. + +## Self-hosting JavaScript and CSS for docs + +Self-hosting the JavaScript and CSS could be useful if, for example, you need your app to keep working even while offline, without open Internet access, or in a local network. + +Here you'll see how to serve those files yourself, in the same FastAPI app, and configure the docs to use them. + +### Project file structure + +Let's say your project file structure looks like this: + +``` +. +├── app +│ ├── __init__.py +│ ├── main.py +``` + +Now create a directory to store those static files. + +Your new file structure could look like this: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +└── static/ +``` + +### Download the files + +Download the static files needed for the docs and put them on that `static/` directory. + +You can probably right-click each link and select an option similar to `Save link as...`. + +**Swagger UI** uses the files: + +* `swagger-ui-bundle.js` +* `swagger-ui.css` + +And **ReDoc** uses the file: + +* `redoc.standalone.js` + +After that, your file structure could look like: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +└── static + ├── redoc.standalone.js + ├── swagger-ui-bundle.js + └── swagger-ui.css +``` + +### Serve the static files + +* Import `StaticFiles`. +* "Mount" a `StaticFiles()` instance in a specific path. + +```Python hl_lines="7 11" +{!../../../docs_src/custom_docs_ui/tutorial002.py!} +``` + +### Test the static files + +Start your application and go to http://127.0.0.1:8000/static/redoc.standalone.js. + +You should see a very long JavaScript file for **ReDoc**. + +It could start with something like: + +```JavaScript +/*! + * ReDoc - OpenAPI/Swagger-generated API Reference Documentation + * ------------------------------------------------------------- + * Version: "2.0.0-rc.18" + * Repo: https://github.com/Redocly/redoc + */ +!function(e,t){"object"==typeof exports&&"object"==typeof m + +... +``` + +That confirms that you are being able to serve static files from your app, and that you placed the static files for the docs in the correct place. + +Now we can configure the app to use those static files for the docs. + +### Disable the automatic docs for static files + +The same as when using a custom CDN, the first step is to disable the automatic docs, as those use the CDN by default. + +To disable them, set their URLs to `None` when creating your `FastAPI` app: + +```Python hl_lines="9" +{!../../../docs_src/custom_docs_ui/tutorial002.py!} +``` + +### Include the custom docs for static files + +And the same way as with a custom CDN, now you can create the *path operations* for the custom docs. + +Again, you can re-use FastAPI's internal functions to create the HTML pages for the docs, and pass them the needed arguments: + +* `openapi_url`: the URL where the HTML page for the docs can get the OpenAPI schema for your API. You can use here the attribute `app.openapi_url`. +* `title`: the title of your API. +* `oauth2_redirect_url`: you can use `app.swagger_ui_oauth2_redirect_url` here to use the default. +* `swagger_js_url`: the URL where the HTML for your Swagger UI docs can get the **JavaScript** file. **This is the one that your own app is now serving**. +* `swagger_css_url`: the URL where the HTML for your Swagger UI docs can get the **CSS** file. **This is the one that your own app is now serving**. + +And similarly for ReDoc... + +```Python hl_lines="2-6 14-22 25-27 30-36" +{!../../../docs_src/custom_docs_ui/tutorial002.py!} +``` + +!!! tip + The *path operation* for `swagger_ui_redirect` is a helper for when you use OAuth2. + + If you integrate your API with an OAuth2 provider, you will be able to authenticate and come back to the API docs with the acquired credentials. And interact with it using the real OAuth2 authentication. + + Swagger UI will handle it behind the scenes for you, but it needs this "redirect" helper. + +### Create a *path operation* to test static files + +Now, to be able to test that everything works, create a *path operation*: + +```Python hl_lines="39-41" +{!../../../docs_src/custom_docs_ui/tutorial002.py!} +``` + +### Test Static Files UI + +Now, you should be able to disconnect your WiFi, go to your docs at http://127.0.0.1:8000/docs, and reload the page. + +And even without Internet, you would be able to see the docs for your API and interact with it. diff --git a/docs/en/docs/advanced/custom-request-and-route.md b/docs/en/docs/how-to/custom-request-and-route.md similarity index 100% rename from docs/en/docs/advanced/custom-request-and-route.md rename to docs/en/docs/how-to/custom-request-and-route.md diff --git a/docs/en/docs/how-to/extending-openapi.md b/docs/en/docs/how-to/extending-openapi.md new file mode 100644 index 0000000000..a18fd737e6 --- /dev/null +++ b/docs/en/docs/how-to/extending-openapi.md @@ -0,0 +1,87 @@ +# Extending OpenAPI + +There are some cases where you might need to modify the generated OpenAPI schema. + +In this section you will see how. + +## The normal process + +The normal (default) process, is as follows. + +A `FastAPI` application (instance) has an `.openapi()` method that is expected to return the OpenAPI schema. + +As part of the application object creation, a *path operation* for `/openapi.json` (or for whatever you set your `openapi_url`) is registered. + +It just returns a JSON response with the result of the application's `.openapi()` method. + +By default, what the method `.openapi()` does is check the property `.openapi_schema` to see if it has contents and return them. + +If it doesn't, it generates them using the utility function at `fastapi.openapi.utils.get_openapi`. + +And that function `get_openapi()` receives as parameters: + +* `title`: The OpenAPI title, shown in the docs. +* `version`: The version of your API, e.g. `2.5.0`. +* `openapi_version`: The version of the OpenAPI specification used. By default, the latest: `3.1.0`. +* `summary`: A short summary of the API. +* `description`: The description of your API, this can include markdown and will be shown in the docs. +* `routes`: A list of routes, these are each of the registered *path operations*. They are taken from `app.routes`. + +!!! info + The parameter `summary` is available in OpenAPI 3.1.0 and above, supported by FastAPI 0.99.0 and above. + +## Overriding the defaults + +Using the information above, you can use the same utility function to generate the OpenAPI schema and override each part that you need. + +For example, let's add ReDoc's OpenAPI extension to include a custom logo. + +### Normal **FastAPI** + +First, write all your **FastAPI** application as normally: + +```Python hl_lines="1 4 7-9" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### Generate the OpenAPI schema + +Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function: + +```Python hl_lines="2 15-21" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 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="22-24" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### Cache the OpenAPI schema + +You can use the property `.openapi_schema` as a "cache", to store your generated schema. + +That way, your application won't have to generate the schema every time a user opens your API docs. + +It will be generated only once, and then the same cached schema will be used for the next requests. + +```Python hl_lines="13-14 25-26" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### Override the method + +Now you can replace the `.openapi()` method with your new function. + +```Python hl_lines="29" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### Check it + +Once you go to http://127.0.0.1:8000/redoc you will see that you are using your custom logo (in this example, **FastAPI**'s logo): + + diff --git a/docs/en/docs/how-to/general.md b/docs/en/docs/how-to/general.md new file mode 100644 index 0000000000..04367c6b76 --- /dev/null +++ b/docs/en/docs/how-to/general.md @@ -0,0 +1,39 @@ +# General - How To - Recipes + +Here are several pointers to other places in the docs, for general or frequent questions. + +## Filter Data - Security + +To ensure that you don't return more data than you should, read the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}. + +## Documentation Tags - OpenAPI + +To add tags to your *path operations*, and group them in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}. + +## Documentation Summary and Description - OpenAPI + +To add a summary and description to your *path operations*, and show them in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Summary and Description](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank}. + +## Documentation Response description - OpenAPI + +To define the description of the response, shown in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Response description](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank}. + +## Documentation Deprecate a *Path Operation* - OpenAPI + +To deprecate a *path operation*, and show it in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Deprecation](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank}. + +## Convert any Data to JSON-compatible + +To convert any data to JSON-compatible, read the docs for [Tutorial - JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}. + +## OpenAPI Metadata - Docs + +To add metadata to your OpenAPI schema, including a license, version, contact, etc, read the docs for [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md){.internal-link target=_blank}. + +## OpenAPI Custom URL + +To customize the OpenAPI URL (or remove it), read the docs for [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md#openapi-url){.internal-link target=_blank}. + +## OpenAPI Docs URLs + +To update the URLs used for the automatically generated docs user interfaces, read the docs for [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}. diff --git a/docs/en/docs/advanced/graphql.md b/docs/en/docs/how-to/graphql.md similarity index 100% rename from docs/en/docs/advanced/graphql.md rename to docs/en/docs/how-to/graphql.md diff --git a/docs/en/docs/how-to/index.md b/docs/en/docs/how-to/index.md new file mode 100644 index 0000000000..ec7fd38f8f --- /dev/null +++ b/docs/en/docs/how-to/index.md @@ -0,0 +1,11 @@ +# How To - Recipes + +Here you will see different recipes or "how to" guides for **several topics**. + +Most of these ideas would be more or less **independent**, and in most cases you should only need to study them if they apply directly to **your project**. + +If something seems interesting and useful to your project, go ahead and check it, but otherwise, you might probably just skip them. + +!!! tip + + If you want to **learn FastAPI** in a structured way (recommended), go and read the [Tutorial - User Guide](../tutorial/index.md){.internal-link target=_blank} chapter by chapter instead. diff --git a/docs/en/docs/advanced/nosql-databases.md b/docs/en/docs/how-to/nosql-databases-couchbase.md similarity index 94% rename from docs/en/docs/advanced/nosql-databases.md rename to docs/en/docs/how-to/nosql-databases-couchbase.md index 6cc5a93857..ae6ad604ba 100644 --- a/docs/en/docs/advanced/nosql-databases.md +++ b/docs/en/docs/how-to/nosql-databases-couchbase.md @@ -1,4 +1,11 @@ -# NoSQL (Distributed / Big Data) Databases +# NoSQL (Distributed / Big Data) Databases with Couchbase + +!!! info + These docs are about to be updated. 🎉 + + The current version assumes Pydantic v1. + + The new docs will hopefully use Pydantic v2 and will use ODMantic with MongoDB. **FastAPI** can also be integrated with any NoSQL. diff --git a/docs/en/docs/how-to/separate-openapi-schemas.md b/docs/en/docs/how-to/separate-openapi-schemas.md new file mode 100644 index 0000000000..10be1071a9 --- /dev/null +++ b/docs/en/docs/how-to/separate-openapi-schemas.md @@ -0,0 +1,231 @@ +# Separate OpenAPI Schemas for Input and Output or Not + +When using **Pydantic v2**, the generated OpenAPI is a bit more exact and **correct** than before. 😎 + +In fact, in some cases, it will even have **two JSON Schemas** in OpenAPI for the same Pydantic model, for input and output, depending on if they have **default values**. + +Let's see how that works and how to change it if you need to do that. + +## Pydantic Models for Input and Output + +Let's say you have a Pydantic model with default values, like this one: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py[ln:1-7]!} + + # Code below omitted 👇 + ``` + +
+ 👀 Full file preview + + ```Python + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py!} + ``` + +
+ +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py[ln:1-9]!} + + # Code below omitted 👇 + ``` + +
+ 👀 Full file preview + + ```Python + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!} + ``` + +
+ +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py[ln:1-9]!} + + # Code below omitted 👇 + ``` + +
+ 👀 Full file preview + + ```Python + {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!} + ``` + +
+ +### Model for Input + +If you use this model as an input like here: + +=== "Python 3.10+" + + ```Python hl_lines="14" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py[ln:1-15]!} + + # Code below omitted 👇 + ``` + +
+ 👀 Full file preview + + ```Python + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py!} + ``` + +
+ +=== "Python 3.9+" + + ```Python hl_lines="16" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py[ln:1-17]!} + + # Code below omitted 👇 + ``` + +
+ 👀 Full file preview + + ```Python + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!} + ``` + +
+ +=== "Python 3.8+" + + ```Python hl_lines="16" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py[ln:1-17]!} + + # Code below omitted 👇 + ``` + +
+ 👀 Full file preview + + ```Python + {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!} + ``` + +
+ +...then the `description` field will **not be required**. Because it has a default value of `None`. + +### Input Model in Docs + +You can confirm that in the docs, the `description` field doesn't have a **red asterisk**, it's not marked as required: + +
+ +
+ +### Model for Output + +But if you use the same model as an output, like here: + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="21" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="21" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!} + ``` + +...then because `description` has a default value, if you **don't return anything** for that field, it will still have that **default value**. + +### Model for Output Response Data + +If you interact with the docs and check the response, even though the code didn't add anything in one of the `description` fields, the JSON response contains the default value (`null`): + +
+ +
+ +This means that it will **always have a value**, it's just that sometimes the value could be `None` (or `null` in JSON). + +That means that, clients using your API don't have to check if the value exists or not, they can **assume the field will always be there**, but just that in some cases it will have the default value of `None`. + +The way to describe this in OpenAPI, is to mark that field as **required**, because it will always be there. + +Because of that, the JSON Schema for a model can be different depending on if it's used for **input or output**: + +* for **input** the `description` will **not be required** +* for **output** it will be **required** (and possibly `None`, or in JSON terms, `null`) + +### Model for Output in Docs + +You can check the output model in the docs too, **both** `name` and `description` are marked as **required** with a **red asterisk**: + +
+ +
+ +### Model for Input and Output in Docs + +And if you check all the available Schemas (JSON Schemas) in OpenAPI, you will see that there are two, one `Item-Input` and one `Item-Output`. + +For `Item-Input`, `description` is **not required**, it doesn't have a red asterisk. + +But for `Item-Output`, `description` is **required**, it has a red asterisk. + +
+ +
+ +With this feature from **Pydantic v2**, your API documentation is more **precise**, and if you have autogenerated clients and SDKs, they will be more precise too, with a better **developer experience** and consistency. 🎉 + +## Do not Separate Schemas + +Now, there are some cases where you might want to have the **same schema for input and output**. + +Probably the main use case for this is if you already have some autogenerated client code/SDKs and you don't want to update all the autogenerated client code/SDKs yet, you probably will want to do it at some point, but maybe not right now. + +In that case, you can disable this feature in **FastAPI**, with the parameter `separate_input_output_schemas=False`. + +!!! info + Support for `separate_input_output_schemas` was added in FastAPI `0.102.0`. 🤓 + +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/separate_openapi_schemas/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="12" + {!> ../../../docs_src/separate_openapi_schemas/tutorial002_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12" + {!> ../../../docs_src/separate_openapi_schemas/tutorial002.py!} + ``` + +### Same Schema for Input and Output Models in Docs + +And now there will be one single schema for input and output for the model, only `Item`, and it will have `description` as **not required**: + +
+ +
+ +This is the same behavior as in Pydantic v1. 🤓 diff --git a/docs/en/docs/advanced/sql-databases-peewee.md b/docs/en/docs/how-to/sql-databases-peewee.md similarity index 97% rename from docs/en/docs/advanced/sql-databases-peewee.md rename to docs/en/docs/how-to/sql-databases-peewee.md index b4ea61367e..74a28b170f 100644 --- a/docs/en/docs/advanced/sql-databases-peewee.md +++ b/docs/en/docs/how-to/sql-databases-peewee.md @@ -5,6 +5,15 @@ Feel free to skip this. + Peewee is not recommended with FastAPI as it doesn't play well with anything async Python. There are several better alternatives. + +!!! info + These docs assume Pydantic v1. + + Because Pewee doesn't play well with anything async and there are better alternatives, I won't update these docs for Pydantic v2, they are kept for now only for historical purposes. + + The examples here are no longer tested in CI (as they were before). + If you are starting a project from scratch, you are probably better off with SQLAlchemy ORM ([SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank}), or any other async ORM. If you already have a code base that uses Peewee ORM, you can check here how to use it with **FastAPI**. @@ -66,7 +75,7 @@ Let's first check all the normal Peewee code, create a Peewee database: ``` !!! tip - Have in mind that if you wanted to use a different database, like PostgreSQL, you couldn't just change the string. You would need to use a different Peewee database class. + Keep in mind that if you wanted to use a different database, like PostgreSQL, you couldn't just change the string. You would need to use a different Peewee database class. #### Note @@ -354,7 +363,7 @@ It will have the database connection open at the beginning and will just wait so This will easily let you test that your app with Peewee and FastAPI is behaving correctly with all the stuff about threads. -If you want to check how Peewee would break your app if used without modification, go the the `sql_app/database.py` file and comment the line: +If you want to check how Peewee would break your app if used without modification, go the `sql_app/database.py` file and comment the line: ```Python # db._state = PeeweeConnectionState() @@ -484,7 +493,7 @@ This means that, with Peewee's current implementation, multiple tasks could be u Python 3.7 has `contextvars` that can create a local variable very similar to `threading.local`, but also supporting these async features. -There are several things to have in mind. +There are several things to keep in mind. The `ContextVar` has to be created at the top of the module, like: diff --git a/docs/en/docs/img/deployment/deta/image03.png b/docs/en/docs/img/deployment/deta/image03.png new file mode 100644 index 0000000000..232355658f Binary files /dev/null and b/docs/en/docs/img/deployment/deta/image03.png differ diff --git a/docs/en/docs/img/deployment/deta/image04.png b/docs/en/docs/img/deployment/deta/image04.png new file mode 100644 index 0000000000..88898e0f26 Binary files /dev/null and b/docs/en/docs/img/deployment/deta/image04.png differ diff --git a/docs/en/docs/img/deployment/deta/image05.png b/docs/en/docs/img/deployment/deta/image05.png new file mode 100644 index 0000000000..590f6f5e40 Binary files /dev/null and b/docs/en/docs/img/deployment/deta/image05.png differ diff --git a/docs/en/docs/img/deployment/deta/image06.png b/docs/en/docs/img/deployment/deta/image06.png new file mode 100644 index 0000000000..f5828bfda6 Binary files /dev/null and b/docs/en/docs/img/deployment/deta/image06.png differ diff --git a/docs/en/docs/img/sponsors/bump-sh-banner.png b/docs/en/docs/img/sponsors/bump-sh-banner.png new file mode 100755 index 0000000000..e75c0facd3 Binary files /dev/null and b/docs/en/docs/img/sponsors/bump-sh-banner.png differ diff --git a/docs/en/docs/img/sponsors/bump-sh-banner.svg b/docs/en/docs/img/sponsors/bump-sh-banner.svg new file mode 100644 index 0000000000..c8ec7675a7 --- /dev/null +++ b/docs/en/docs/img/sponsors/bump-sh-banner.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/docs/img/sponsors/bump-sh.png b/docs/en/docs/img/sponsors/bump-sh.png new file mode 100755 index 0000000000..61817e86fa Binary files /dev/null and b/docs/en/docs/img/sponsors/bump-sh.png differ diff --git a/docs/en/docs/img/sponsors/bump-sh.svg b/docs/en/docs/img/sponsors/bump-sh.svg new file mode 100644 index 0000000000..053e54b1da --- /dev/null +++ b/docs/en/docs/img/sponsors/bump-sh.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/docs/img/sponsors/codacy.png b/docs/en/docs/img/sponsors/codacy.png new file mode 100644 index 0000000000..baa615c2a8 Binary files /dev/null and b/docs/en/docs/img/sponsors/codacy.png differ diff --git a/docs/en/docs/img/sponsors/databento.svg b/docs/en/docs/img/sponsors/databento.svg new file mode 100644 index 0000000000..dfdd9bee6d --- /dev/null +++ b/docs/en/docs/img/sponsors/databento.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/docs/img/sponsors/fern-banner.png b/docs/en/docs/img/sponsors/fern-banner.png new file mode 100644 index 0000000000..1b70ab96d9 Binary files /dev/null and b/docs/en/docs/img/sponsors/fern-banner.png differ diff --git a/docs/en/docs/img/sponsors/fern-banner.svg b/docs/en/docs/img/sponsors/fern-banner.svg new file mode 100644 index 0000000000..e05ccc3a47 --- /dev/null +++ b/docs/en/docs/img/sponsors/fern-banner.svg @@ -0,0 +1 @@ + diff --git a/docs/en/docs/img/sponsors/fern.png b/docs/en/docs/img/sponsors/fern.png new file mode 100644 index 0000000000..4c6e54b0d5 Binary files /dev/null and b/docs/en/docs/img/sponsors/fern.png differ diff --git a/docs/en/docs/img/sponsors/fern.svg b/docs/en/docs/img/sponsors/fern.svg new file mode 100644 index 0000000000..ad3842fe03 --- /dev/null +++ b/docs/en/docs/img/sponsors/fern.svg @@ -0,0 +1 @@ + diff --git a/docs/en/docs/img/sponsors/flint.png b/docs/en/docs/img/sponsors/flint.png new file mode 100644 index 0000000000..761cc334c2 Binary files /dev/null and b/docs/en/docs/img/sponsors/flint.png differ diff --git a/docs/en/docs/img/sponsors/platform-sh-banner.png b/docs/en/docs/img/sponsors/platform-sh-banner.png new file mode 100644 index 0000000000..f9f4580fac Binary files /dev/null and b/docs/en/docs/img/sponsors/platform-sh-banner.png differ diff --git a/docs/en/docs/img/sponsors/platform-sh.png b/docs/en/docs/img/sponsors/platform-sh.png new file mode 100644 index 0000000000..fb4e07becd Binary files /dev/null and b/docs/en/docs/img/sponsors/platform-sh.png differ diff --git a/docs/en/docs/img/sponsors/porter-banner.png b/docs/en/docs/img/sponsors/porter-banner.png new file mode 100755 index 0000000000..fa2e741c0c Binary files /dev/null and b/docs/en/docs/img/sponsors/porter-banner.png differ diff --git a/docs/en/docs/img/sponsors/porter.png b/docs/en/docs/img/sponsors/porter.png new file mode 100755 index 0000000000..582a151564 Binary files /dev/null and b/docs/en/docs/img/sponsors/porter.png differ diff --git a/docs/en/docs/img/sponsors/powens.png b/docs/en/docs/img/sponsors/powens.png new file mode 100644 index 0000000000..07fc6fa37f Binary files /dev/null and b/docs/en/docs/img/sponsors/powens.png differ diff --git a/docs/en/docs/img/sponsors/propelauth-banner.png b/docs/en/docs/img/sponsors/propelauth-banner.png new file mode 100644 index 0000000000..7a1bb25803 Binary files /dev/null and b/docs/en/docs/img/sponsors/propelauth-banner.png differ diff --git a/docs/en/docs/img/sponsors/propelauth.png b/docs/en/docs/img/sponsors/propelauth.png new file mode 100644 index 0000000000..8234d631f6 Binary files /dev/null and b/docs/en/docs/img/sponsors/propelauth.png differ diff --git a/docs/en/docs/img/sponsors/reflex-banner.png b/docs/en/docs/img/sponsors/reflex-banner.png new file mode 100644 index 0000000000..3095c3a7b4 Binary files /dev/null and b/docs/en/docs/img/sponsors/reflex-banner.png differ diff --git a/docs/en/docs/img/sponsors/reflex.png b/docs/en/docs/img/sponsors/reflex.png new file mode 100644 index 0000000000..59c46a1104 Binary files /dev/null and b/docs/en/docs/img/sponsors/reflex.png differ diff --git a/docs/en/docs/img/sponsors/scalar-banner.svg b/docs/en/docs/img/sponsors/scalar-banner.svg new file mode 100644 index 0000000000..bab74e2d7c --- /dev/null +++ b/docs/en/docs/img/sponsors/scalar-banner.svg @@ -0,0 +1 @@ + diff --git a/docs/en/docs/img/sponsors/scalar.svg b/docs/en/docs/img/sponsors/scalar.svg new file mode 100644 index 0000000000..174c57ee23 --- /dev/null +++ b/docs/en/docs/img/sponsors/scalar.svg @@ -0,0 +1 @@ + diff --git a/docs/en/docs/img/sponsors/speakeasy.png b/docs/en/docs/img/sponsors/speakeasy.png new file mode 100644 index 0000000000..001b4b4caf Binary files /dev/null and b/docs/en/docs/img/sponsors/speakeasy.png differ diff --git a/docs/en/docs/img/tutorial/metadata/image01.png b/docs/en/docs/img/tutorial/metadata/image01.png index b7708a3fd9..4146a8607b 100644 Binary files a/docs/en/docs/img/tutorial/metadata/image01.png and b/docs/en/docs/img/tutorial/metadata/image01.png differ diff --git a/docs/en/docs/img/tutorial/openapi-webhooks/image01.png b/docs/en/docs/img/tutorial/openapi-webhooks/image01.png new file mode 100644 index 0000000000..25ced48186 Binary files /dev/null and b/docs/en/docs/img/tutorial/openapi-webhooks/image01.png differ diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png new file mode 100644 index 0000000000..aa085f88df Binary files /dev/null and b/docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png differ diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png new file mode 100644 index 0000000000..672ef1d2b7 Binary files /dev/null and b/docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png differ diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png new file mode 100644 index 0000000000..81340fbece Binary files /dev/null and b/docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png differ diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png new file mode 100644 index 0000000000..fc2302aa77 Binary files /dev/null and b/docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png differ diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png new file mode 100644 index 0000000000..674dd0b2e2 Binary files /dev/null and b/docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png differ diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index deb8ab5d54..3660e74e3c 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -1,3 +1,12 @@ +--- +hide: + - navigation +--- + + +

FastAPI

@@ -27,7 +36,7 @@ --- -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.8+ based on standard Python type hints. The key features are: @@ -99,6 +108,12 @@ The key features are: --- +"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + ## **Typer**, the FastAPI of CLIs @@ -109,7 +124,7 @@ If you are building a CLI app to be ## Requirements -Python 3.7+ +Python 3.8+ FastAPI stands on the shoulders of giants: @@ -325,7 +340,7 @@ 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.7+**. +Just standard **Python 3.8+**. For example, for an `int`: @@ -439,8 +454,9 @@ To understand more about it, see the section ujson - for faster JSON "parsing". * email_validator - for email validation. +* pydantic-settings - for settings management. +* pydantic-extra-types - for extra types to be used with Pydantic. Used by Starlette: diff --git a/docs/en/docs/js/chat.js b/docs/en/docs/js/chat.js deleted file mode 100644 index debdef4dad..0000000000 --- a/docs/en/docs/js/chat.js +++ /dev/null @@ -1,3 +0,0 @@ -((window.gitter = {}).chat = {}).options = { - room: 'tiangolo/fastapi' -}; diff --git a/docs/en/docs/learn/index.md b/docs/en/docs/learn/index.md new file mode 100644 index 0000000000..d056fb3200 --- /dev/null +++ b/docs/en/docs/learn/index.md @@ -0,0 +1,5 @@ +# Learn + +Here are the introductory sections and the tutorials to learn **FastAPI**. + +You could consider this a **book**, a **course**, the **official** and recommended way to learn FastAPI. 😎 diff --git a/docs/en/docs/newsletter.md b/docs/en/docs/newsletter.md index 6403f31e60..782db1353c 100644 --- a/docs/en/docs/newsletter.md +++ b/docs/en/docs/newsletter.md @@ -1,5 +1,5 @@ # FastAPI and friends newsletter - + - + diff --git a/docs/en/docs/project-generation.md b/docs/en/docs/project-generation.md index 2f3d6c1d39..8ba34fa112 100644 --- a/docs/en/docs/project-generation.md +++ b/docs/en/docs/project-generation.md @@ -1,6 +1,6 @@ # Project Generation - Template -You can use a project generator to get started, as it includes a lot of the initial set up, security, database and first API endpoints already done for you. +You can use a project generator to get started, as it includes a lot of the initial set up, security, database and some API endpoints already done for you. A project generator will always have a very opinionated setup that you should update and adapt for your own needs, but it might be a good starting point for your project. diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 3d22ee620a..cdd22ea4a2 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -1,8 +1,8 @@ # Python Types Intro -Python has support for optional "type hints". +Python has support for optional "type hints" (also called "type annotations"). -These **"type hints"** are a special syntax that allow declaring the type of a variable. +These **"type hints"** or annotations are a special syntax that allow declaring the type of a variable. By declaring types for your variables, editors and tools can give you better support. @@ -158,13 +158,31 @@ The syntax using `typing` is **compatible** with all versions, from Python 3.6 t As Python advances, **newer versions** come with improved support for these type annotations and in many cases you won't even need to import and use the `typing` module to declare the type annotations. -If you can choose a more recent version of Python for your project, you will be able to take advantage of that extra simplicity. See some examples below. +If you can choose a more recent version of Python for your project, you will be able to take advantage of that extra simplicity. + +In all the docs there are examples compatible with each version of Python (when there's a difference). + +For example "**Python 3.6+**" means it's compatible with Python 3.6 or above (including 3.7, 3.8, 3.9, 3.10, etc). And "**Python 3.9+**" means it's compatible with Python 3.9 or above (including 3.10, etc). + +If you can use the **latest versions of Python**, use the examples for the latest version, those will have the **best and simplest syntax**, for example, "**Python 3.10+**". #### List For example, let's define a variable to be a `list` of `str`. -=== "Python 3.6 and above" +=== "Python 3.9+" + + Declare the variable, with the same colon (`:`) syntax. + + As the type, put `list`. + + As the list is a type that contains some internal types, you put them in square brackets: + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006_py39.py!} + ``` + +=== "Python 3.8+" From `typing`, import `List` (with a capital `L`): @@ -182,18 +200,6 @@ For example, let's define a variable to be a `list` of `str`. {!> ../../../docs_src/python_types/tutorial006.py!} ``` -=== "Python 3.9 and above" - - Declare the variable, with the same colon (`:`) syntax. - - As the type, put `list`. - - As the list is a type that contains some internal types, you put them in square brackets: - - ```Python hl_lines="1" - {!> ../../../docs_src/python_types/tutorial006_py39.py!} - ``` - !!! info Those internal types in the square brackets are called "type parameters". @@ -218,18 +224,18 @@ And still, the editor knows it is a `str`, and provides support for that. You would do the same to declare `tuple`s and `set`s: -=== "Python 3.6 and above" - - ```Python hl_lines="1 4" - {!> ../../../docs_src/python_types/tutorial007.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="1" {!> ../../../docs_src/python_types/tutorial007_py39.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial007.py!} + ``` + This means: * The variable `items_t` is a `tuple` with 3 items, an `int`, another `int`, and a `str`. @@ -243,18 +249,18 @@ The first type parameter is for the keys of the `dict`. The second type parameter is for the values of the `dict`: -=== "Python 3.6 and above" - - ```Python hl_lines="1 4" - {!> ../../../docs_src/python_types/tutorial008.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="1" {!> ../../../docs_src/python_types/tutorial008_py39.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008.py!} + ``` + This means: * The variable `prices` is a `dict`: @@ -267,20 +273,20 @@ You can declare that a variable can be any of **several types**, for example, an In Python 3.6 and above (including Python 3.10) you can use the `Union` type from `typing` and put inside the square brackets the possible types to accept. -In Python 3.10 there's also an **alternative syntax** where you can put the possible types separated by a vertical bar (`|`). +In Python 3.10 there's also a **new syntax** where you can put the possible types separated by a vertical bar (`|`). -=== "Python 3.6 and above" - - ```Python hl_lines="1 4" - {!> ../../../docs_src/python_types/tutorial008b.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="1" {!> ../../../docs_src/python_types/tutorial008b_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008b.py!} + ``` + In both cases this means that `item` could be an `int` or a `str`. #### Possibly `None` @@ -299,24 +305,24 @@ Using `Optional[str]` instead of just `str` will let the editor help you detecti This also means that in Python 3.10, you can use `Something | None`: -=== "Python 3.6 and above" +=== "Python 3.10+" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial009_py310.py!} + ``` + +=== "Python 3.8+" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial009.py!} ``` -=== "Python 3.6 and above - alternative" +=== "Python 3.8+ alternative" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial009b.py!} ``` -=== "Python 3.10 and above" - - ```Python hl_lines="1" - {!> ../../../docs_src/python_types/tutorial009_py310.py!} - ``` - #### Using `Union` or `Optional` If you are using a Python version below 3.10, here's a tip from my very **subjective** point of view: @@ -360,7 +366,39 @@ And then you won't have to worry about names like `Optional` and `Union`. 😎 These types that take type parameters in square brackets are called **Generic types** or **Generics**, for example: -=== "Python 3.6 and above" +=== "Python 3.10+" + + You can use the same builtin types as generics (with square brackets and types inside): + + * `list` + * `tuple` + * `set` + * `dict` + + And the same as with Python 3.8, from the `typing` module: + + * `Union` + * `Optional` (the same as with Python 3.8) + * ...and others. + + In Python 3.10, as an alternative to using the generics `Union` and `Optional`, you can use the vertical bar (`|`) to declare unions of types, that's a lot better and simpler. + +=== "Python 3.9+" + + You can use the same builtin types as generics (with square brackets and types inside): + + * `list` + * `tuple` + * `set` + * `dict` + + And the same as with Python 3.8, from the `typing` module: + + * `Union` + * `Optional` + * ...and others. + +=== "Python 3.8+" * `List` * `Tuple` @@ -370,38 +408,6 @@ These types that take type parameters in square brackets are called **Generic ty * `Optional` * ...and others. -=== "Python 3.9 and above" - - You can use the same builtin types as generics (with square brackets and types inside): - - * `list` - * `tuple` - * `set` - * `dict` - - And the same as with Python 3.6, from the `typing` module: - - * `Union` - * `Optional` - * ...and others. - -=== "Python 3.10 and above" - - You can use the same builtin types as generics (with square brackets and types inside): - - * `list` - * `tuple` - * `set` - * `dict` - - And the same as with Python 3.6, from the `typing` module: - - * `Union` - * `Optional` (the same as with Python 3.6) - * ...and others. - - In Python 3.10, as an alternative to using the generics `Union` and `Optional`, you can use the vertical bar (`|`) to declare unions of types. - ### Classes as types You can also declare a class as the type of a variable. @@ -422,6 +428,10 @@ And then, again, you get all the editor support: +Notice that this means "`one_person` is an **instance** of the class `Person`". + +It doesn't mean "`one_person` is the **class** called `Person`". + ## Pydantic models Pydantic is a Python library to perform data validation. @@ -436,22 +446,22 @@ And you get all the editor support with that resulting object. An example from the official Pydantic docs: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python - {!> ../../../docs_src/python_types/tutorial011.py!} + {!> ../../../docs_src/python_types/tutorial011_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python {!> ../../../docs_src/python_types/tutorial011_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" ```Python - {!> ../../../docs_src/python_types/tutorial011_py310.py!} + {!> ../../../docs_src/python_types/tutorial011.py!} ``` !!! info @@ -464,6 +474,43 @@ You will see a lot more of all this in practice in the [Tutorial - User Guide](t !!! tip Pydantic has a special behavior when you use `Optional` or `Union[Something, None]` without a default value, you can read more about it in the Pydantic docs about Required Optional fields. +## Type Hints with Metadata Annotations + +Python also has a feature that allows putting **additional metadata** in these type hints using `Annotated`. + +=== "Python 3.9+" + + In Python 3.9, `Annotated` is part of the standard library, so you can import it from `typing`. + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial013_py39.py!} + ``` + +=== "Python 3.8+" + + In versions below Python 3.9, you import `Annotated` from `typing_extensions`. + + It will already be installed with **FastAPI**. + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial013.py!} + ``` + +Python itself doesn't do anything with this `Annotated`. And for editors and other tools, the type is still `str`. + +But you can use this space in `Annotated` to provide **FastAPI** with additional metadata about how you want your application to behave. + +The important thing to remember is that **the first *type parameter*** you pass to `Annotated` is the **actual type**. The rest, is just metadata for other tools. + +For now, you just need to know that `Annotated` exists, and that it's standard Python. 😎 + +Later you will see how **powerful** it can be. + +!!! tip + The fact that this is **standard Python** means that you will still get the **best possible developer experience** in your editor, with the tools you use to analyze and refactor your code, etc. ✨ + + And also that your code will be very compatible with many other Python tools and libraries. 🚀 + ## Type hints in **FastAPI** **FastAPI** takes advantage of these type hints to do several things. diff --git a/docs/en/docs/reference/apirouter.md b/docs/en/docs/reference/apirouter.md new file mode 100644 index 0000000000..b779ad2914 --- /dev/null +++ b/docs/en/docs/reference/apirouter.md @@ -0,0 +1,25 @@ +# `APIRouter` class + +Here's the reference information for the `APIRouter` class, with all its parameters, +attributes and methods. + +You can import the `APIRouter` class directly from `fastapi`: + +```python +from fastapi import APIRouter +``` + +::: fastapi.APIRouter + options: + members: + - websocket + - include_router + - get + - put + - post + - delete + - options + - head + - patch + - trace + - on_event diff --git a/docs/en/docs/reference/background.md b/docs/en/docs/reference/background.md new file mode 100644 index 0000000000..e0c0be899f --- /dev/null +++ b/docs/en/docs/reference/background.md @@ -0,0 +1,13 @@ +# Background Tasks - `BackgroundTasks` + +You can declare a parameter in a *path operation function* or dependency function +with the type `BackgroundTasks`, and then you can use it to schedule the execution +of background tasks after the response is sent. + +You can import it directly from `fastapi`: + +```python +from fastapi import BackgroundTasks +``` + +::: fastapi.BackgroundTasks diff --git a/docs/en/docs/reference/dependencies.md b/docs/en/docs/reference/dependencies.md new file mode 100644 index 0000000000..0999682679 --- /dev/null +++ b/docs/en/docs/reference/dependencies.md @@ -0,0 +1,32 @@ +# Dependencies - `Depends()` and `Security()` + +## `Depends()` + +Dependencies are handled mainly with the special function `Depends()` that takes a +callable. + +Here is the reference for it and its parameters. + +You can import it directly from `fastapi`: + +```python +from fastapi import Depends +``` + +::: fastapi.Depends + +## `Security()` + +For many scenarios, you can handle security (authorization, authentication, etc.) with +dependencies, using `Depends()`. + +But when you want to also declare OAuth2 scopes, you can use `Security()` instead of +`Depends()`. + +You can import `Security()` directly from `fastapi`: + +```python +from fastapi import Security +``` + +::: fastapi.Security diff --git a/docs/en/docs/reference/encoders.md b/docs/en/docs/reference/encoders.md new file mode 100644 index 0000000000..28df2e43a2 --- /dev/null +++ b/docs/en/docs/reference/encoders.md @@ -0,0 +1,3 @@ +# Encoders - `jsonable_encoder` + +::: fastapi.encoders.jsonable_encoder diff --git a/docs/en/docs/reference/exceptions.md b/docs/en/docs/reference/exceptions.md new file mode 100644 index 0000000000..7c48083492 --- /dev/null +++ b/docs/en/docs/reference/exceptions.md @@ -0,0 +1,22 @@ +# Exceptions - `HTTPException` and `WebSocketException` + +These are the exceptions that you can raise to show errors to the client. + +When you raise an exception, as would happen with normal Python, the rest of the +execution is aborted. This way you can raise these exceptions from anywhere in the +code to abort a request and show the error to the client. + +You can use: + +* `HTTPException` +* `WebSocketException` + +These exceptions can be imported directly from `fastapi`: + +```python +from fastapi import HTTPException, WebSocketException +``` + +::: fastapi.HTTPException + +::: fastapi.WebSocketException diff --git a/docs/en/docs/reference/fastapi.md b/docs/en/docs/reference/fastapi.md new file mode 100644 index 0000000000..8b87664cb3 --- /dev/null +++ b/docs/en/docs/reference/fastapi.md @@ -0,0 +1,32 @@ +# `FastAPI` class + +Here's the reference information for the `FastAPI` class, with all its parameters, +attributes and methods. + +You can import the `FastAPI` class directly from `fastapi`: + +```python +from fastapi import FastAPI +``` + +::: fastapi.FastAPI + options: + members: + - openapi_version + - webhooks + - state + - dependency_overrides + - openapi + - websocket + - include_router + - get + - put + - post + - delete + - options + - head + - patch + - trace + - on_event + - middleware + - exception_handler diff --git a/docs/en/docs/reference/httpconnection.md b/docs/en/docs/reference/httpconnection.md new file mode 100644 index 0000000000..43dfc46f94 --- /dev/null +++ b/docs/en/docs/reference/httpconnection.md @@ -0,0 +1,13 @@ +# `HTTPConnection` class + +When you want to define dependencies that should be compatible with both HTTP and +WebSockets, you can define a parameter that takes an `HTTPConnection` instead of a +`Request` or a `WebSocket`. + +You can import it from `fastapi.requests`: + +```python +from fastapi.requests import HTTPConnection +``` + +::: fastapi.requests.HTTPConnection diff --git a/docs/en/docs/reference/index.md b/docs/en/docs/reference/index.md new file mode 100644 index 0000000000..512d5c25c5 --- /dev/null +++ b/docs/en/docs/reference/index.md @@ -0,0 +1,7 @@ +# Reference - Code API + +Here's the reference or code API, the classes, functions, parameters, attributes, and +all the FastAPI parts you can use in your applications. + +If you want to **learn FastAPI** you are much better off reading the +[FastAPI Tutorial](https://fastapi.tiangolo.com/tutorial/). diff --git a/docs/en/docs/reference/middleware.md b/docs/en/docs/reference/middleware.md new file mode 100644 index 0000000000..89704d3c8b --- /dev/null +++ b/docs/en/docs/reference/middleware.md @@ -0,0 +1,46 @@ +# Middleware + +There are several middlewares available provided by Starlette directly. + +Read more about them in the +[FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/). + +::: fastapi.middleware.cors.CORSMiddleware + +It can be imported from `fastapi`: + +```python +from fastapi.middleware.cors import CORSMiddleware +``` + +::: fastapi.middleware.gzip.GZipMiddleware + +It can be imported from `fastapi`: + +```python +from fastapi.middleware.gzip import GZipMiddleware +``` + +::: fastapi.middleware.httpsredirect.HTTPSRedirectMiddleware + +It can be imported from `fastapi`: + +```python +from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware +``` + +::: fastapi.middleware.trustedhost.TrustedHostMiddleware + +It can be imported from `fastapi`: + +```python +from fastapi.middleware.trustedhost import TrustedHostMiddleware +``` + +::: fastapi.middleware.wsgi.WSGIMiddleware + +It can be imported from `fastapi`: + +```python +from fastapi.middleware.wsgi import WSGIMiddleware +``` diff --git a/docs/en/docs/reference/openapi/docs.md b/docs/en/docs/reference/openapi/docs.md new file mode 100644 index 0000000000..ab620833ec --- /dev/null +++ b/docs/en/docs/reference/openapi/docs.md @@ -0,0 +1,11 @@ +# OpenAPI `docs` + +Utilities to handle OpenAPI automatic UI documentation, including Swagger UI (by default at `/docs`) and ReDoc (by default at `/redoc`). + +::: fastapi.openapi.docs.get_swagger_ui_html + +::: fastapi.openapi.docs.get_redoc_html + +::: fastapi.openapi.docs.get_swagger_ui_oauth2_redirect_html + +::: fastapi.openapi.docs.swagger_ui_default_parameters diff --git a/docs/en/docs/reference/openapi/index.md b/docs/en/docs/reference/openapi/index.md new file mode 100644 index 0000000000..e2b313f150 --- /dev/null +++ b/docs/en/docs/reference/openapi/index.md @@ -0,0 +1,5 @@ +# OpenAPI + +There are several utilities to handle OpenAPI. + +You normally don't need to use them unless you have a specific advanced use case that requires it. diff --git a/docs/en/docs/reference/openapi/models.md b/docs/en/docs/reference/openapi/models.md new file mode 100644 index 0000000000..4a6b0770ed --- /dev/null +++ b/docs/en/docs/reference/openapi/models.md @@ -0,0 +1,5 @@ +# OpenAPI `models` + +OpenAPI Pydantic models used to generate and validate the generated OpenAPI. + +::: fastapi.openapi.models diff --git a/docs/en/docs/reference/parameters.md b/docs/en/docs/reference/parameters.md new file mode 100644 index 0000000000..8f77f0161b --- /dev/null +++ b/docs/en/docs/reference/parameters.md @@ -0,0 +1,36 @@ +# Request Parameters + +Here's the reference information for the request parameters. + +These are the special functions that you can put in *path operation function* +parameters or dependency functions with `Annotated` to get data from the request. + +It includes: + +* `Query()` +* `Path()` +* `Body()` +* `Cookie()` +* `Header()` +* `Form()` +* `File()` + +You can import them all directly from `fastapi`: + +```python +from fastapi import Body, Cookie, File, Form, Header, Path, Query +``` + +::: fastapi.Query + +::: fastapi.Path + +::: fastapi.Body + +::: fastapi.Cookie + +::: fastapi.Header + +::: fastapi.Form + +::: fastapi.File diff --git a/docs/en/docs/reference/request.md b/docs/en/docs/reference/request.md new file mode 100644 index 0000000000..91ec7d37b6 --- /dev/null +++ b/docs/en/docs/reference/request.md @@ -0,0 +1,18 @@ +# `Request` class + +You can declare a parameter in a *path operation function* or dependency to be of type +`Request` and then you can access the raw request object directly, without any +validation, etc. + +You can import it directly from `fastapi`: + +```python +from fastapi import Request +``` + +!!! tip + When you want to define dependencies that should be compatible with both HTTP and + WebSockets, you can define a parameter that takes an `HTTPConnection` instead of a + `Request` or a `WebSocket`. + +::: fastapi.Request diff --git a/docs/en/docs/reference/response.md b/docs/en/docs/reference/response.md new file mode 100644 index 0000000000..9162545831 --- /dev/null +++ b/docs/en/docs/reference/response.md @@ -0,0 +1,15 @@ +# `Response` class + +You can declare a parameter in a *path operation function* or dependency to be of type +`Response` and then you can set data for the response like headers or cookies. + +You can also use it directly to create an instance of it and return it from your *path +operations*. + +You can import it directly from `fastapi`: + +```python +from fastapi import Response +``` + +::: fastapi.Response diff --git a/docs/en/docs/reference/responses.md b/docs/en/docs/reference/responses.md new file mode 100644 index 0000000000..2cbbd89632 --- /dev/null +++ b/docs/en/docs/reference/responses.md @@ -0,0 +1,166 @@ +# Custom Response Classes - File, HTML, Redirect, Streaming, etc. + +There are several custom response classes you can use to create an instance and return +them directly from your *path operations*. + +Read more about it in the +[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + +You can import them directly from `fastapi.responses`: + +```python +from fastapi.responses import ( + FileResponse, + HTMLResponse, + JSONResponse, + ORJSONResponse, + PlainTextResponse, + RedirectResponse, + Response, + StreamingResponse, + UJSONResponse, +) +``` + +## FastAPI Responses + +There are a couple of custom FastAPI response classes, you can use them to optimize JSON performance. + +::: fastapi.responses.UJSONResponse + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.ORJSONResponse + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +## Starlette Responses + +::: fastapi.responses.FileResponse + options: + members: + - chunk_size + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.HTMLResponse + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.JSONResponse + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.PlainTextResponse + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.RedirectResponse + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.Response + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.StreamingResponse + options: + members: + - body_iterator + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie diff --git a/docs/en/docs/reference/security/index.md b/docs/en/docs/reference/security/index.md new file mode 100644 index 0000000000..ff86e9e30c --- /dev/null +++ b/docs/en/docs/reference/security/index.md @@ -0,0 +1,76 @@ +# Security Tools + +When you need to declare dependencies with OAuth2 scopes you use `Security()`. + +But you still need to define what is the dependable, the callable that you pass as +a parameter to `Depends()` or `Security()`. + +There are multiple tools that you can use to create those dependables, and they get +integrated into OpenAPI so they are shown in the automatic docs UI, they can be used +by automatically generated clients and SDKs, etc. + +You can import them from `fastapi.security`: + +```python +from fastapi.security import ( + APIKeyCookie, + APIKeyHeader, + APIKeyQuery, + HTTPAuthorizationCredentials, + HTTPBasic, + HTTPBasicCredentials, + HTTPBearer, + HTTPDigest, + OAuth2, + OAuth2AuthorizationCodeBearer, + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + OAuth2PasswordRequestFormStrict, + OpenIdConnect, + SecurityScopes, +) +``` + +## API Key Security Schemes + +::: fastapi.security.APIKeyCookie + +::: fastapi.security.APIKeyHeader + +::: fastapi.security.APIKeyQuery + +## HTTP Authentication Schemes + +::: fastapi.security.HTTPBasic + +::: fastapi.security.HTTPBearer + +::: fastapi.security.HTTPDigest + +## HTTP Credentials + +::: fastapi.security.HTTPAuthorizationCredentials + +::: fastapi.security.HTTPBasicCredentials + +## OAuth2 Authentication + +::: fastapi.security.OAuth2 + +::: fastapi.security.OAuth2AuthorizationCodeBearer + +::: fastapi.security.OAuth2PasswordBearer + +## OAuth2 Password Form + +::: fastapi.security.OAuth2PasswordRequestForm + +::: fastapi.security.OAuth2PasswordRequestFormStrict + +## OAuth2 Security Scopes in Dependencies + +::: fastapi.security.SecurityScopes + +## OpenID Connect + +::: fastapi.security.OpenIdConnect diff --git a/docs/en/docs/reference/staticfiles.md b/docs/en/docs/reference/staticfiles.md new file mode 100644 index 0000000000..ce66f17b3d --- /dev/null +++ b/docs/en/docs/reference/staticfiles.md @@ -0,0 +1,14 @@ +# Static Files - `StaticFiles` + +You can use the `StaticFiles` class to serve static files, like JavaScript, CSS, images, etc. + +Read more about it in the +[FastAPI docs for Static Files](https://fastapi.tiangolo.com/tutorial/static-files/). + +You can import it directly from `fastapi.staticfiles`: + +```python +from fastapi.staticfiles import StaticFiles +``` + +::: fastapi.staticfiles.StaticFiles diff --git a/docs/en/docs/reference/status.md b/docs/en/docs/reference/status.md new file mode 100644 index 0000000000..a238007923 --- /dev/null +++ b/docs/en/docs/reference/status.md @@ -0,0 +1,39 @@ +# Status Codes + +You can import the `status` module from `fastapi`: + +```python +from fastapi import status +``` + +`status` is provided directly by Starlette. + +It contains a group of named constants (variables) with integer status codes. + +For example: + +* 200: `status.HTTP_200_OK` +* 403: `status.HTTP_403_FORBIDDEN` +* etc. + +It can be convenient to quickly access HTTP (and WebSocket) status codes in your app, +using autocompletion for the name without having to remember the integer status codes +by memory. + +Read more about it in the +[FastAPI docs about Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + +## Example + +```python +from fastapi import FastAPI, status + +app = FastAPI() + + +@app.get("/items/", status_code=status.HTTP_418_IM_A_TEAPOT) +def read_items(): + return [{"name": "Plumbus"}, {"name": "Portal Gun"}] +``` + +::: fastapi.status diff --git a/docs/en/docs/reference/templating.md b/docs/en/docs/reference/templating.md new file mode 100644 index 0000000000..c865badfcb --- /dev/null +++ b/docs/en/docs/reference/templating.md @@ -0,0 +1,14 @@ +# Templating - `Jinja2Templates` + +You can use the `Jinja2Templates` class to render Jinja templates. + +Read more about it in the +[FastAPI docs for Templates](https://fastapi.tiangolo.com/advanced/templates/). + +You can import it directly from `fastapi.templating`: + +```python +from fastapi.templating import Jinja2Templates +``` + +::: fastapi.templating.Jinja2Templates diff --git a/docs/en/docs/reference/testclient.md b/docs/en/docs/reference/testclient.md new file mode 100644 index 0000000000..e391d964a2 --- /dev/null +++ b/docs/en/docs/reference/testclient.md @@ -0,0 +1,14 @@ +# Test Client - `TestClient` + +You can use the `TestClient` class to test FastAPI applications without creating an actual HTTP and socket connection, just communicating directly with the FastAPI code. + +Read more about it in the +[FastAPI docs for Testing](https://fastapi.tiangolo.com/tutorial/testing/). + +You can import it directly from `fastapi.testclient`: + +```python +from fastapi.testclient import TestClient +``` + +::: fastapi.testclient.TestClient diff --git a/docs/en/docs/reference/uploadfile.md b/docs/en/docs/reference/uploadfile.md new file mode 100644 index 0000000000..45c644b18d --- /dev/null +++ b/docs/en/docs/reference/uploadfile.md @@ -0,0 +1,23 @@ +# `UploadFile` class + +You can define *path operation function* parameters to be of the type `UploadFile` +to receive files from the request. + +You can import it directly from `fastapi`: + +```python +from fastapi import UploadFile +``` + +::: fastapi.UploadFile + options: + members: + - file + - filename + - size + - headers + - content_type + - read + - write + - seek + - close diff --git a/docs/en/docs/reference/websockets.md b/docs/en/docs/reference/websockets.md new file mode 100644 index 0000000000..2a04694678 --- /dev/null +++ b/docs/en/docs/reference/websockets.md @@ -0,0 +1,70 @@ +# WebSockets + +When defining WebSockets, you normally declare a parameter of type `WebSocket` and +with it you can read data from the client and send data to it. + +It is provided directly by Starlette, but you can import it from `fastapi`: + +```python +from fastapi import WebSocket +``` + +!!! tip + When you want to define dependencies that should be compatible with both HTTP and + WebSockets, you can define a parameter that takes an `HTTPConnection` instead of a + `Request` or a `WebSocket`. + +::: fastapi.WebSocket + options: + members: + - scope + - app + - url + - base_url + - headers + - query_params + - path_params + - cookies + - client + - state + - url_for + - client_state + - application_state + - receive + - send + - accept + - receive_text + - receive_bytes + - receive_json + - iter_text + - iter_bytes + - iter_json + - send_text + - send_bytes + - send_json + - close + +When a client disconnects, a `WebSocketDisconnect` exception is raised, you can catch +it. + +You can import it directly form `fastapi`: + +```python +from fastapi import WebSocketDisconnect +``` + +::: fastapi.WebSocketDisconnect + +## WebSockets - additional classes + +Additional classes for handling WebSockets. + +Provided directly by Starlette, but you can import it from `fastapi`: + +```python +from fastapi.websockets import WebSocketDisconnect, WebSocketState +``` + +::: fastapi.websockets.WebSocketDisconnect + +::: fastapi.websockets.WebSocketState diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index cdafa68998..ed9fdf03b7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -1,7 +1,1173 @@ +--- +hide: + - navigation +--- + # Release Notes ## Latest Changes +* ✏️ Fix Pydantic method name in `docs/en/docs/advanced/path-operation-advanced-configuration.md`. PR [#10826](https://github.com/tiangolo/fastapi/pull/10826) by [@ahmedabdou14](https://github.com/ahmedabdou14). + +### Features + +* ✨ Include HTTP 205 in status codes with no body. PR [#10969](https://github.com/tiangolo/fastapi/pull/10969) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* ✅ Refactor tests for duplicate operation ID generation for compatibility with other tools running the FastAPI test suite. PR [#10876](https://github.com/tiangolo/fastapi/pull/10876) by [@emmettbutler](https://github.com/emmettbutler). +* ♻️ Simplify string format with f-strings in `fastapi/utils.py`. PR [#10576](https://github.com/tiangolo/fastapi/pull/10576) by [@eukub](https://github.com/eukub). +* 🔧 Fix Ruff configuration unintentionally enabling and re-disabling mccabe complexity check. PR [#10893](https://github.com/tiangolo/fastapi/pull/10893) by [@jiridanek](https://github.com/jiridanek). +* ✅ Re-enable test in `tests/test_tutorial/test_header_params/test_tutorial003.py` after fix in Starlette. PR [#10904](https://github.com/tiangolo/fastapi/pull/10904) by [@ooknimm](https://github.com/ooknimm). + +### Docs + +* ✏️ Fix typo in `fastapi/security/oauth2.py`. PR [#10972](https://github.com/tiangolo/fastapi/pull/10972) by [@RafalSkolasinski](https://github.com/RafalSkolasinski). +* 📝 Update `HTTPException` details in `docs/en/docs/tutorial/handling-errors.md`. PR [#5418](https://github.com/tiangolo/fastapi/pull/5418) by [@papb](https://github.com/papb). +* ✏️ A few tweaks in `docs/de/docs/tutorial/first-steps.md`. PR [#10959](https://github.com/tiangolo/fastapi/pull/10959) by [@nilslindemann](https://github.com/nilslindemann). +* ✏️ Fix link in `docs/en/docs/advanced/async-tests.md`. PR [#10960](https://github.com/tiangolo/fastapi/pull/10960) by [@nilslindemann](https://github.com/nilslindemann). +* ✏️ Fix typos for Spanish documentation. PR [#10957](https://github.com/tiangolo/fastapi/pull/10957) by [@jlopezlira](https://github.com/jlopezlira). +* 📝 Add warning about lifespan functions and backwards compatibility with events. PR [#10734](https://github.com/tiangolo/fastapi/pull/10734) by [@jacob-indigo](https://github.com/jacob-indigo). +* ✏️ Fix broken link in `docs/tutorial/sql-databases.md` in several languages. PR [#10716](https://github.com/tiangolo/fastapi/pull/10716) by [@theoohoho](https://github.com/theoohoho). +* ✏️ Remove broken links from `external_links.yml`. PR [#10943](https://github.com/tiangolo/fastapi/pull/10943) by [@Torabek](https://github.com/Torabek). +* 📝 Update template docs with more info about `url_for`. PR [#5937](https://github.com/tiangolo/fastapi/pull/5937) by [@EzzEddin](https://github.com/EzzEddin). +* 📝 Update usage of Token model in security docs. PR [#9313](https://github.com/tiangolo/fastapi/pull/9313) by [@piotrszacilowski](https://github.com/piotrszacilowski). +* ✏️ Update highlighted line in `docs/en/docs/tutorial/bigger-applications.md`. PR [#5490](https://github.com/tiangolo/fastapi/pull/5490) by [@papb](https://github.com/papb). +* 📝 Add External Link: Explore How to Effectively Use JWT With FastAPI. PR [#10212](https://github.com/tiangolo/fastapi/pull/10212) by [@aanchlia](https://github.com/aanchlia). +* 📝 Add hyperlink to `docs/en/docs/tutorial/static-files.md`. PR [#10243](https://github.com/tiangolo/fastapi/pull/10243) by [@hungtsetse](https://github.com/hungtsetse). +* 📝 Add External Link: Instrument a FastAPI service adding tracing with OpenTelemetry and send/show traces in Grafana Tempo. PR [#9440](https://github.com/tiangolo/fastapi/pull/9440) by [@softwarebloat](https://github.com/softwarebloat). +* 📝 Review and rewording of `en/docs/contributing.md`. PR [#10480](https://github.com/tiangolo/fastapi/pull/10480) by [@nilslindemann](https://github.com/nilslindemann). +* 📝 Add External Link: ML serving and monitoring with FastAPI and Evidently. PR [#9701](https://github.com/tiangolo/fastapi/pull/9701) by [@mnrozhkov](https://github.com/mnrozhkov). +* 📝 Reword in docs, from "have in mind" to "keep in mind". PR [#10376](https://github.com/tiangolo/fastapi/pull/10376) by [@malicious](https://github.com/malicious). +* 📝 Add External Link: Talk by Jeny Sadadia. PR [#10265](https://github.com/tiangolo/fastapi/pull/10265) by [@JenySadadia](https://github.com/JenySadadia). +* 📝 Add location info to `tutorial/bigger-applications.md`. PR [#10552](https://github.com/tiangolo/fastapi/pull/10552) by [@nilslindemann](https://github.com/nilslindemann). + +### Translations + +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#1961](https://github.com/tiangolo/fastapi/pull/1961) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#1960](https://github.com/tiangolo/fastapi/pull/1960) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/sub-dependencies.md`. PR [#1959](https://github.com/tiangolo/fastapi/pull/1959) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/background-tasks.md`. PR [#2668](https://github.com/tiangolo/fastapi/pull/2668) by [@tokusumi](https://github.com/tokusumi). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/index.md` and `docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#1958](https://github.com/tiangolo/fastapi/pull/1958) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/response-model.md`. PR [#1938](https://github.com/tiangolo/fastapi/pull/1938) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-multiple-params.md`. PR [#1903](https://github.com/tiangolo/fastapi/pull/1903) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/path-params-numeric-validations.md`. PR [#1902](https://github.com/tiangolo/fastapi/pull/1902) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/python-types.md`. PR [#1899](https://github.com/tiangolo/fastapi/pull/1899) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/handling-errors.md`. PR [#1953](https://github.com/tiangolo/fastapi/pull/1953) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/response-status-code.md`. PR [#1942](https://github.com/tiangolo/fastapi/pull/1942) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/extra-models.md`. PR [#1941](https://github.com/tiangolo/fastapi/pull/1941) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese tranlsation for `docs/ja/docs/tutorial/schema-extra-example.md`. PR [#1931](https://github.com/tiangolo/fastapi/pull/1931) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-nested-models.md`. PR [#1930](https://github.com/tiangolo/fastapi/pull/1930) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-fields.md`. PR [#1923](https://github.com/tiangolo/fastapi/pull/1923) by [@SwftAlpc](https://github.com/SwftAlpc). +* 🌐 Add German translation for `docs/de/docs/tutorial/index.md`. PR [#9502](https://github.com/tiangolo/fastapi/pull/9502) by [@fhabers21](https://github.com/fhabers21). +* 🌐 Add German translation for `docs/de/docs/tutorial/background-tasks.md`. PR [#10566](https://github.com/tiangolo/fastapi/pull/10566) by [@nilslindemann](https://github.com/nilslindemann). +* ✏️ Fix typo in `docs/ru/docs/index.md`. PR [#10672](https://github.com/tiangolo/fastapi/pull/10672) by [@Delitel-WEB](https://github.com/Delitel-WEB). +* ✏️ Fix typos in `docs/zh/docs/tutorial/extra-data-types.md`. PR [#10727](https://github.com/tiangolo/fastapi/pull/10727) by [@HiemalBeryl](https://github.com/HiemalBeryl). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#10410](https://github.com/tiangolo/fastapi/pull/10410) by [@AlertRED](https://github.com/AlertRED). + +### Internal + +* 👷 Add changes-requested handling in GitHub Action issue manager. PR [#10971](https://github.com/tiangolo/fastapi/pull/10971) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Group dependencies on dependabot updates. PR [#10952](https://github.com/tiangolo/fastapi/pull/10952) by [@Kludex](https://github.com/Kludex). +* ⬆ Bump actions/setup-python from 4 to 5. PR [#10764](https://github.com/tiangolo/fastapi/pull/10764) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.10 to 1.8.11. PR [#10731](https://github.com/tiangolo/fastapi/pull/10731) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump dawidd6/action-download-artifact from 2.28.0 to 3.0.0. PR [#10777](https://github.com/tiangolo/fastapi/pull/10777) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Add support for translations to languages with a longer code name, like `zh-hant`. PR [#10950](https://github.com/tiangolo/fastapi/pull/10950) by [@tiangolo](https://github.com/tiangolo). + +## 0.109.0 + +### Features + +* ✨ Add support for Python 3.12. PR [#10666](https://github.com/tiangolo/fastapi/pull/10666) by [@Jamim](https://github.com/Jamim). + +### Upgrades + +* ⬆️ Upgrade Starlette to >=0.35.0,<0.36.0. PR [#10938](https://github.com/tiangolo/fastapi/pull/10938) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ✏️ Fix typo in `docs/en/docs/alternatives.md`. PR [#10931](https://github.com/tiangolo/fastapi/pull/10931) by [@s111d](https://github.com/s111d). +* 📝 Replace `email` with `username` in `docs_src/security/tutorial007` code examples. PR [#10649](https://github.com/tiangolo/fastapi/pull/10649) by [@nilslindemann](https://github.com/nilslindemann). +* 📝 Add VS Code tutorial link. PR [#10592](https://github.com/tiangolo/fastapi/pull/10592) by [@nilslindemann](https://github.com/nilslindemann). +* 📝 Add notes about Pydantic v2's new `.model_dump()`. PR [#10929](https://github.com/tiangolo/fastapi/pull/10929) by [@tiangolo](https://github.com/tiangolo). +* 📝 Fix broken link in `docs/en/docs/tutorial/sql-databases.md`. PR [#10765](https://github.com/tiangolo/fastapi/pull/10765) by [@HurSungYun](https://github.com/HurSungYun). +* 📝 Add External Link: FastAPI application monitoring made easy. PR [#10917](https://github.com/tiangolo/fastapi/pull/10917) by [@tiangolo](https://github.com/tiangolo). +* ✨ Generate automatic language names for docs translations. PR [#5354](https://github.com/tiangolo/fastapi/pull/5354) by [@jakul](https://github.com/jakul). +* ✏️ Fix typos in `docs/en/docs/alternatives.md` and `docs/en/docs/tutorial/dependencies/index.md`. PR [#10906](https://github.com/tiangolo/fastapi/pull/10906) by [@s111d](https://github.com/s111d). +* ✏️ Fix typos in `docs/en/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#10834](https://github.com/tiangolo/fastapi/pull/10834) by [@Molkree](https://github.com/Molkree). +* 📝 Add article: "Building a RESTful API with FastAPI: Secure Signup and Login Functionality Included". PR [#9733](https://github.com/tiangolo/fastapi/pull/9733) by [@dxphilo](https://github.com/dxphilo). +* 📝 Add warning about lifecycle events with `AsyncClient`. PR [#4167](https://github.com/tiangolo/fastapi/pull/4167) by [@andrew-chang-dewitt](https://github.com/andrew-chang-dewitt). +* ✏️ Fix typos in `/docs/reference/exceptions.md` and `/en/docs/reference/status.md`. PR [#10809](https://github.com/tiangolo/fastapi/pull/10809) by [@clarencepenz](https://github.com/clarencepenz). +* ✏️ Fix typo in `openapi-callbacks.md`. PR [#10673](https://github.com/tiangolo/fastapi/pull/10673) by [@kayjan](https://github.com/kayjan). +* ✏️ Fix typo in `fastapi/routing.py` . PR [#10520](https://github.com/tiangolo/fastapi/pull/10520) by [@sepsh](https://github.com/sepsh). +* 📝 Replace HTTP code returned in case of existing user error in docs for testing. PR [#4482](https://github.com/tiangolo/fastapi/pull/4482) by [@TristanMarion](https://github.com/TristanMarion). +* 📝 Add blog for FastAPI & Supabase. PR [#6018](https://github.com/tiangolo/fastapi/pull/6018) by [@theinfosecguy](https://github.com/theinfosecguy). +* 📝 Update example source files for SQL databases with SQLAlchemy. PR [#9508](https://github.com/tiangolo/fastapi/pull/9508) by [@s-mustafa](https://github.com/s-mustafa). +* 📝 Update code examples in docs for body, replace name `create_item` with `update_item` when appropriate. PR [#5913](https://github.com/tiangolo/fastapi/pull/5913) by [@OttoAndrey](https://github.com/OttoAndrey). +* ✏️ Fix typo in dependencies with yield source examples. PR [#10847](https://github.com/tiangolo/fastapi/pull/10847) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Add Bengali translation for `docs/bn/docs/index.md`. PR [#9177](https://github.com/tiangolo/fastapi/pull/9177) by [@Fahad-Md-Kamal](https://github.com/Fahad-Md-Kamal). +* ✏️ Update Python version in `index.md` in several languages. PR [#10711](https://github.com/tiangolo/fastapi/pull/10711) by [@tamago3keran](https://github.com/tamago3keran). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-forms-and-files.md`. PR [#10347](https://github.com/tiangolo/fastapi/pull/10347) by [@AlertRED](https://github.com/AlertRED). +* 🌐 Add Ukrainian translation for `docs/uk/docs/index.md`. PR [#10362](https://github.com/tiangolo/fastapi/pull/10362) by [@rostik1410](https://github.com/rostik1410). +* ✏️ Update Python version in `docs/ko/docs/index.md`. PR [#10680](https://github.com/tiangolo/fastapi/pull/10680) by [@Eeap](https://github.com/Eeap). +* 🌐 Add Persian translation for `docs/fa/docs/features.md`. PR [#5887](https://github.com/tiangolo/fastapi/pull/5887) by [@amirilf](https://github.com/amirilf). +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/additional-responses.md`. PR [#10325](https://github.com/tiangolo/fastapi/pull/10325) by [@ShuibeiC](https://github.com/ShuibeiC). +* 🌐 Fix typos in Russian translations for `docs/ru/docs/tutorial/background-tasks.md`, `docs/ru/docs/tutorial/body-nested-models.md`, `docs/ru/docs/tutorial/debugging.md`, `docs/ru/docs/tutorial/testing.md`. PR [#10311](https://github.com/tiangolo/fastapi/pull/10311) by [@AlertRED](https://github.com/AlertRED). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-files.md`. PR [#10332](https://github.com/tiangolo/fastapi/pull/10332) by [@AlertRED](https://github.com/AlertRED). +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/server-workers.md`. PR [#10292](https://github.com/tiangolo/fastapi/pull/10292) by [@xzmeng](https://github.com/xzmeng). +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/cloud.md`. PR [#10291](https://github.com/tiangolo/fastapi/pull/10291) by [@xzmeng](https://github.com/xzmeng). +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/manually.md`. PR [#10279](https://github.com/tiangolo/fastapi/pull/10279) by [@xzmeng](https://github.com/xzmeng). +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/https.md`. PR [#10277](https://github.com/tiangolo/fastapi/pull/10277) by [@xzmeng](https://github.com/xzmeng). +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/index.md`. PR [#10275](https://github.com/tiangolo/fastapi/pull/10275) by [@xzmeng](https://github.com/xzmeng). +* 🌐 Add German translation for `docs/de/docs/tutorial/first-steps.md`. PR [#9530](https://github.com/tiangolo/fastapi/pull/9530) by [@fhabers21](https://github.com/fhabers21). +* 🌐 Update Turkish translation for `docs/tr/docs/index.md`. PR [#10444](https://github.com/tiangolo/fastapi/pull/10444) by [@hasansezertasan](https://github.com/hasansezertasan). +* 🌐 Add Chinese translation for `docs/zh/docs/learn/index.md`. PR [#10479](https://github.com/tiangolo/fastapi/pull/10479) by [@KAZAMA-DREAM](https://github.com/KAZAMA-DREAM). +* 🌐 Add Russian translation for `docs/ru/docs/learn/index.md`. PR [#10539](https://github.com/tiangolo/fastapi/pull/10539) by [@AlertRED](https://github.com/AlertRED). +* 🌐 Update SQLAlchemy instruction in Chinese translation `docs/zh/docs/tutorial/sql-databases.md`. PR [#9712](https://github.com/tiangolo/fastapi/pull/9712) by [@Royc30ne](https://github.com/Royc30ne). +* 🌐 Add Turkish translation for `docs/tr/docs/external-links.md`. PR [#10549](https://github.com/tiangolo/fastapi/pull/10549) by [@hasansezertasan](https://github.com/hasansezertasan). +* 🌐 Add Spanish translation for `docs/es/docs/learn/index.md`. PR [#10885](https://github.com/tiangolo/fastapi/pull/10885) by [@pablocm83](https://github.com/pablocm83). +* 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/body-fields.md`. PR [#10670](https://github.com/tiangolo/fastapi/pull/10670) by [@ArtemKhymenko](https://github.com/ArtemKhymenko). +* 🌐 Add Hungarian translation for `/docs/hu/docs/index.md`. PR [#10812](https://github.com/tiangolo/fastapi/pull/10812) by [@takacs](https://github.com/takacs). +* 🌐 Add Turkish translation for `docs/tr/docs/newsletter.md`. PR [#10550](https://github.com/tiangolo/fastapi/pull/10550) by [@hasansezertasan](https://github.com/hasansezertasan). +* 🌐 Add Spanish translation for `docs/es/docs/help/index.md`. PR [#10907](https://github.com/tiangolo/fastapi/pull/10907) by [@pablocm83](https://github.com/pablocm83). +* 🌐 Add Spanish translation for `docs/es/docs/about/index.md`. PR [#10908](https://github.com/tiangolo/fastapi/pull/10908) by [@pablocm83](https://github.com/pablocm83). +* 🌐 Add Spanish translation for `docs/es/docs/resources/index.md`. PR [#10909](https://github.com/tiangolo/fastapi/pull/10909) by [@pablocm83](https://github.com/pablocm83). + +### Internal + +* 👥 Update FastAPI People. PR [#10871](https://github.com/tiangolo/fastapi/pull/10871) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade custom GitHub Action comment-docs-preview-in-pr. PR [#10916](https://github.com/tiangolo/fastapi/pull/10916) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Upgrade GitHub Action latest-changes. PR [#10915](https://github.com/tiangolo/fastapi/pull/10915) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade GitHub Action label-approved. PR [#10913](https://github.com/tiangolo/fastapi/pull/10913) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Upgrade GitHub Action label-approved. PR [#10905](https://github.com/tiangolo/fastapi/pull/10905) by [@tiangolo](https://github.com/tiangolo). + +## 0.108.0 + +### Upgrades + +* ⬆️ Upgrade Starlette to `>=0.29.0,<0.33.0`, update docs and usage of templates with new Starlette arguments. PR [#10846](https://github.com/tiangolo/fastapi/pull/10846) by [@tiangolo](https://github.com/tiangolo). + +## 0.107.0 + +### Upgrades + +* ⬆️ Upgrade Starlette to 0.28.0. PR [#9636](https://github.com/tiangolo/fastapi/pull/9636) by [@adriangb](https://github.com/adriangb). + +### Docs + +* 📝 Add docs: Node.js script alternative to update OpenAPI for generated clients. PR [#10845](https://github.com/tiangolo/fastapi/pull/10845) by [@alejsdev](https://github.com/alejsdev). +* 📝 Restructure Docs section in Contributing page. PR [#10844](https://github.com/tiangolo/fastapi/pull/10844) by [@alejsdev](https://github.com/alejsdev). + +## 0.106.0 + +### Breaking Changes + +Using resources from dependencies with `yield` in background tasks is no longer supported. + +This change is what supports the new features, read below. 🤓 + +### Dependencies with `yield`, `HTTPException` and Background Tasks + +Dependencies with `yield` now can raise `HTTPException` and other exceptions after `yield`. 🎉 + +Read the new docs here: [Dependencies with `yield` and `HTTPException`](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#dependencies-with-yield-and-httpexception). + +```Python +from fastapi import Depends, FastAPI, HTTPException +from typing_extensions import Annotated + +app = FastAPI() + + +data = { + "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"}, + "portal-gun": {"description": "Gun to create portals", "owner": "Rick"}, +} + + +class OwnerError(Exception): + pass + + +def get_username(): + try: + yield "Rick" + except OwnerError as e: + raise HTTPException(status_code=400, detail=f"Owner error: {e}") + + +@app.get("/items/{item_id}") +def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): + if item_id not in data: + raise HTTPException(status_code=404, detail="Item not found") + item = data[item_id] + if item["owner"] != username: + raise OwnerError(username) + return item +``` + +--- + +Before FastAPI 0.106.0, raising exceptions after `yield` was not possible, the exit code in dependencies with `yield` was executed *after* the response was sent, so [Exception Handlers](https://fastapi.tiangolo.com/tutorial/handling-errors/#install-custom-exception-handlers) would have already run. + +This was designed this way mainly to allow using the same objects "yielded" by dependencies inside of background tasks, because the exit code would be executed after the background tasks were finished. + +Nevertheless, as this would mean waiting for the response to travel through the network while unnecessarily holding a resource in a dependency with yield (for example a database connection), this was changed in FastAPI 0.106.0. + +Additionally, a background task is normally an independent set of logic that should be handled separately, with its own resources (e.g. its own database connection). + +If you used to rely on this behavior, now you should create the resources for background tasks inside the background task itself, and use internally only data that doesn't depend on the resources of dependencies with `yield`. + +For example, instead of using the same database session, you would create a new database session inside of the background task, and you would obtain the objects from the database using this new session. And then instead of passing the object from the database as a parameter to the background task function, you would pass the ID of that object and then obtain the object again inside the background task function. + +The sequence of execution before FastAPI 0.106.0 was like this diagram: + +Time flows from top to bottom. And each column is one of the parts interacting or executing code. + +```mermaid +sequenceDiagram + +participant client as Client +participant handler as Exception handler +participant dep as Dep with yield +participant operation as Path Operation +participant tasks as Background tasks + + Note over client,tasks: Can raise exception for dependency, handled after response is sent + Note over client,operation: Can raise HTTPException and can change the response + client ->> dep: Start request + Note over dep: Run code up to yield + opt raise + dep -->> handler: Raise HTTPException + handler -->> client: HTTP error response + dep -->> dep: Raise other exception + end + dep ->> operation: Run dependency, e.g. DB session + opt raise + operation -->> dep: Raise HTTPException + dep -->> handler: Auto forward exception + handler -->> client: HTTP error response + operation -->> dep: Raise other exception + dep -->> handler: Auto forward exception + end + operation ->> client: Return response to client + Note over client,operation: Response is already sent, can't change it anymore + opt Tasks + operation -->> tasks: Send background tasks + end + opt Raise other exception + tasks -->> dep: Raise other exception + end + Note over dep: After yield + opt Handle other exception + dep -->> dep: Handle exception, can't change response. E.g. close DB session. + end +``` + +The new execution flow can be found in the docs: [Execution of dependencies with `yield`](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#execution-of-dependencies-with-yield). + +### Features + +* ✨ Add support for raising exceptions (including `HTTPException`) in dependencies with `yield` in the exit code, do not support them in background tasks. PR [#10831](https://github.com/tiangolo/fastapi/pull/10831) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 👥 Update FastAPI People. PR [#10567](https://github.com/tiangolo/fastapi/pull/10567) by [@tiangolo](https://github.com/tiangolo). + +## 0.105.0 + +### Features + +* ✨ Add support for multiple Annotated annotations, e.g. `Annotated[str, Field(), Query()]`. PR [#10773](https://github.com/tiangolo/fastapi/pull/10773) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* 🔥 Remove unused NoneType. PR [#10774](https://github.com/tiangolo/fastapi/pull/10774) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Tweak default suggested configs for generating clients. PR [#10736](https://github.com/tiangolo/fastapi/pull/10736) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🔧 Update sponsors, add Scalar. PR [#10728](https://github.com/tiangolo/fastapi/pull/10728) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors, add PropelAuth. PR [#10760](https://github.com/tiangolo/fastapi/pull/10760) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update build docs, verify README on CI. PR [#10750](https://github.com/tiangolo/fastapi/pull/10750) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors, remove Fern. PR [#10729](https://github.com/tiangolo/fastapi/pull/10729) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors, add Codacy. PR [#10677](https://github.com/tiangolo/fastapi/pull/10677) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors, add Reflex. PR [#10676](https://github.com/tiangolo/fastapi/pull/10676) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update release notes, move and check latest-changes. PR [#10588](https://github.com/tiangolo/fastapi/pull/10588) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade latest-changes GitHub Action. PR [#10587](https://github.com/tiangolo/fastapi/pull/10587) by [@tiangolo](https://github.com/tiangolo). + +## 0.104.1 + +### Fixes + +* 📌 Pin Swagger UI version to 5.9.0 temporarily to handle a bug crashing it in 5.9.1. PR [#10529](https://github.com/tiangolo/fastapi/pull/10529) by [@alejandraklachquin](https://github.com/alejandraklachquin). + * This is not really a bug in FastAPI but in Swagger UI, nevertheless pinning the version will work while a solution is found on the [Swagger UI side](https://github.com/swagger-api/swagger-ui/issues/9337). + +### Docs + +* 📝 Update data structure and render for external-links. PR [#10495](https://github.com/tiangolo/fastapi/pull/10495) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Fix link to SPDX license identifier in `docs/en/docs/tutorial/metadata.md`. PR [#10433](https://github.com/tiangolo/fastapi/pull/10433) by [@worldworm](https://github.com/worldworm). +* 📝 Update example validation error from Pydantic v1 to match Pydantic v2 in `docs/en/docs/tutorial/path-params.md`. PR [#10043](https://github.com/tiangolo/fastapi/pull/10043) by [@giuliowaitforitdavide](https://github.com/giuliowaitforitdavide). +* ✏️ Fix typos in emoji docs and in some source examples. PR [#10438](https://github.com/tiangolo/fastapi/pull/10438) by [@afuetterer](https://github.com/afuetterer). +* ✏️ Fix typo in `docs/en/docs/reference/dependencies.md`. PR [#10465](https://github.com/tiangolo/fastapi/pull/10465) by [@suravshresth](https://github.com/suravshresth). +* ✏️ Fix typos and rewordings in `docs/en/docs/tutorial/body-nested-models.md`. PR [#10468](https://github.com/tiangolo/fastapi/pull/10468) by [@yogabonito](https://github.com/yogabonito). +* 📝 Update docs, remove references to removed `pydantic.Required` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#10469](https://github.com/tiangolo/fastapi/pull/10469) by [@yogabonito](https://github.com/yogabonito). +* ✏️ Fix typo in `docs/en/docs/reference/index.md`. PR [#10467](https://github.com/tiangolo/fastapi/pull/10467) by [@tarsil](https://github.com/tarsil). +* 🔥 Remove unnecessary duplicated docstrings. PR [#10484](https://github.com/tiangolo/fastapi/pull/10484) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ✏️ Update Pydantic links to dotenv support. PR [#10511](https://github.com/tiangolo/fastapi/pull/10511) by [@White-Mask](https://github.com/White-Mask). +* ✏️ Update links in `docs/en/docs/async.md` and `docs/zh/docs/async.md` to make them relative. PR [#10498](https://github.com/tiangolo/fastapi/pull/10498) by [@hasnatsajid](https://github.com/hasnatsajid). +* ✏️ Fix links in `docs/em/docs/async.md`. PR [#10507](https://github.com/tiangolo/fastapi/pull/10507) by [@hasnatsajid](https://github.com/hasnatsajid). +* ✏️ Fix typo in `docs/em/docs/index.md`, Python 3.8. PR [#10521](https://github.com/tiangolo/fastapi/pull/10521) by [@kerriop](https://github.com/kerriop). +* ⬆ Bump pillow from 9.5.0 to 10.1.0. PR [#10446](https://github.com/tiangolo/fastapi/pull/10446) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update mkdocs-material requirement from <9.0.0,>=8.1.4 to >=8.1.4,<10.0.0. PR [#5862](https://github.com/tiangolo/fastapi/pull/5862) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.1.21 to 9.4.7. PR [#10545](https://github.com/tiangolo/fastapi/pull/10545) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Install MkDocs Material Insiders only when secrets are available, for Dependabot. PR [#10544](https://github.com/tiangolo/fastapi/pull/10544) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors badges, Databento. PR [#10519](https://github.com/tiangolo/fastapi/pull/10519) by [@tiangolo](https://github.com/tiangolo). +* 👷 Adopt Ruff format. PR [#10517](https://github.com/tiangolo/fastapi/pull/10517) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add `CITATION.cff` file for academic citations. PR [#10496](https://github.com/tiangolo/fastapi/pull/10496) by [@tiangolo](https://github.com/tiangolo). +* 🐛 Fix overriding MKDocs theme lang in hook. PR [#10490](https://github.com/tiangolo/fastapi/pull/10490) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Drop/close Gitter chat. Questions should go to GitHub Discussions, free conversations to Discord.. PR [#10485](https://github.com/tiangolo/fastapi/pull/10485) by [@tiangolo](https://github.com/tiangolo). + +## 0.104.0 + +## Features + +* ✨ Add reference (code API) docs with PEP 727, add subclass with custom docstrings for `BackgroundTasks`, refactor docs structure. PR [#10392](https://github.com/tiangolo/fastapi/pull/10392) by [@tiangolo](https://github.com/tiangolo). New docs at [FastAPI Reference - Code API](https://fastapi.tiangolo.com/reference/). + +## Upgrades + +* ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#10442](https://github.com/tiangolo/fastapi/pull/10442) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆ Bump dawidd6/action-download-artifact from 2.27.0 to 2.28.0. PR [#10268](https://github.com/tiangolo/fastapi/pull/10268) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/checkout from 3 to 4. PR [#10208](https://github.com/tiangolo/fastapi/pull/10208) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.6 to 1.8.10. PR [#10061](https://github.com/tiangolo/fastapi/pull/10061) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Update sponsors, Bump.sh images. PR [#10381](https://github.com/tiangolo/fastapi/pull/10381) by [@tiangolo](https://github.com/tiangolo). +* 👥 Update FastAPI People. PR [#10363](https://github.com/tiangolo/fastapi/pull/10363) by [@tiangolo](https://github.com/tiangolo). + +## 0.103.2 + +### Refactors + +* ⬆️ Upgrade compatibility with Pydantic v2.4, new renamed functions and JSON Schema input/output models with default values. PR [#10344](https://github.com/tiangolo/fastapi/pull/10344) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/extra-data-types.md`. PR [#10132](https://github.com/tiangolo/fastapi/pull/10132) by [@ArtemKhymenko](https://github.com/ArtemKhymenko). +* 🌐 Fix typos in French translations for `docs/fr/docs/advanced/path-operation-advanced-configuration.md`, `docs/fr/docs/alternatives.md`, `docs/fr/docs/async.md`, `docs/fr/docs/features.md`, `docs/fr/docs/help-fastapi.md`, `docs/fr/docs/index.md`, `docs/fr/docs/python-types.md`, `docs/fr/docs/tutorial/body.md`, `docs/fr/docs/tutorial/first-steps.md`, `docs/fr/docs/tutorial/query-params.md`. PR [#10154](https://github.com/tiangolo/fastapi/pull/10154) by [@s-rigaud](https://github.com/s-rigaud). +* 🌐 Add Chinese translation for `docs/zh/docs/async.md`. PR [#5591](https://github.com/tiangolo/fastapi/pull/5591) by [@mkdir700](https://github.com/mkdir700). +* 🌐 Update Chinese translation for `docs/tutorial/security/simple-oauth2.md`. PR [#3844](https://github.com/tiangolo/fastapi/pull/3844) by [@jaystone776](https://github.com/jaystone776). +* 🌐 Add Korean translation for `docs/ko/docs/deployment/cloud.md`. PR [#10191](https://github.com/tiangolo/fastapi/pull/10191) by [@Sion99](https://github.com/Sion99). +* 🌐 Add Japanese translation for `docs/ja/docs/deployment/https.md`. PR [#10298](https://github.com/tiangolo/fastapi/pull/10298) by [@tamtam-fitness](https://github.com/tamtam-fitness). +* 🌐 Fix typo in Russian translation for `docs/ru/docs/tutorial/body-fields.md`. PR [#10224](https://github.com/tiangolo/fastapi/pull/10224) by [@AlertRED](https://github.com/AlertRED). +* 🌐 Add Polish translation for `docs/pl/docs/help-fastapi.md`. PR [#10121](https://github.com/tiangolo/fastapi/pull/10121) by [@romabozhanovgithub](https://github.com/romabozhanovgithub). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/header-params.md`. PR [#10226](https://github.com/tiangolo/fastapi/pull/10226) by [@AlertRED](https://github.com/AlertRED). +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/versions.md`. PR [#10276](https://github.com/tiangolo/fastapi/pull/10276) by [@xzmeng](https://github.com/xzmeng). + +### Internal + +* 🔧 Update sponsors, remove Flint. PR [#10349](https://github.com/tiangolo/fastapi/pull/10349) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Rename label "awaiting review" to "awaiting-review" to simplify search queries. PR [#10343](https://github.com/tiangolo/fastapi/pull/10343) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors, enable Svix (revert #10228). PR [#10253](https://github.com/tiangolo/fastapi/pull/10253) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors, remove Svix. PR [#10228](https://github.com/tiangolo/fastapi/pull/10228) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors, add Bump.sh. PR [#10227](https://github.com/tiangolo/fastapi/pull/10227) by [@tiangolo](https://github.com/tiangolo). + +## 0.103.1 + +### Fixes + +* 📌 Pin AnyIO to < 4.0.0 to handle an incompatibility while upgrading to Starlette 0.31.1. PR [#10194](https://github.com/tiangolo/fastapi/pull/10194) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ✏️ Fix validation parameter name in docs, from `regex` to `pattern`. PR [#10085](https://github.com/tiangolo/fastapi/pull/10085) by [@pablodorrio](https://github.com/pablodorrio). +* ✏️ Fix indent format in `docs/en/docs/deployment/server-workers.md`. PR [#10066](https://github.com/tiangolo/fastapi/pull/10066) by [@tamtam-fitness](https://github.com/tamtam-fitness). +* ✏️ Fix Pydantic examples in tutorial for Python types. PR [#9961](https://github.com/tiangolo/fastapi/pull/9961) by [@rahulsalgare](https://github.com/rahulsalgare). +* ✏️ Fix link to Pydantic docs in `docs/en/docs/tutorial/extra-data-types.md`. PR [#10155](https://github.com/tiangolo/fastapi/pull/10155) by [@hasnatsajid](https://github.com/hasnatsajid). +* ✏️ Fix typo in `docs/en/docs/tutorial/handling-errors.md`. PR [#10170](https://github.com/tiangolo/fastapi/pull/10170) by [@poupapaa](https://github.com/poupapaa). +* ✏️ Fix typo in `docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#10172](https://github.com/tiangolo/fastapi/pull/10172) by [@ragul-kachiappan](https://github.com/ragul-kachiappan). + +### Translations + +* 🌐 Remove duplicate line in translation for `docs/pt/docs/tutorial/path-params.md`. PR [#10126](https://github.com/tiangolo/fastapi/pull/10126) by [@LecoOliveira](https://github.com/LecoOliveira). +* 🌐 Add Yoruba translation for `docs/yo/docs/index.md`. PR [#10033](https://github.com/tiangolo/fastapi/pull/10033) by [@AfolabiOlaoluwa](https://github.com/AfolabiOlaoluwa). +* 🌐 Add Ukrainian translation for `docs/uk/docs/python-types.md`. PR [#10080](https://github.com/tiangolo/fastapi/pull/10080) by [@rostik1410](https://github.com/rostik1410). +* 🌐 Add Vietnamese translations for `docs/vi/docs/tutorial/first-steps.md` and `docs/vi/docs/tutorial/index.md`. PR [#10088](https://github.com/tiangolo/fastapi/pull/10088) by [@magiskboy](https://github.com/magiskboy). +* 🌐 Add Ukrainian translation for `docs/uk/docs/alternatives.md`. PR [#10060](https://github.com/tiangolo/fastapi/pull/10060) by [@whysage](https://github.com/whysage). +* 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/index.md`. PR [#10079](https://github.com/tiangolo/fastapi/pull/10079) by [@rostik1410](https://github.com/rostik1410). +* ✏️ Fix typos in `docs/en/docs/how-to/separate-openapi-schemas.md` and `docs/en/docs/tutorial/schema-extra-example.md`. PR [#10189](https://github.com/tiangolo/fastapi/pull/10189) by [@xzmeng](https://github.com/xzmeng). +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/generate-clients.md`. PR [#9883](https://github.com/tiangolo/fastapi/pull/9883) by [@funny-cat-happy](https://github.com/funny-cat-happy). + +### Refactors + +* ✏️ Fix typos in comment in `fastapi/applications.py`. PR [#10045](https://github.com/tiangolo/fastapi/pull/10045) by [@AhsanSheraz](https://github.com/AhsanSheraz). +* ✅ Add missing test for OpenAPI examples, it was missing in coverage. PR [#10188](https://github.com/tiangolo/fastapi/pull/10188) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 👥 Update FastAPI People. PR [#10186](https://github.com/tiangolo/fastapi/pull/10186) by [@tiangolo](https://github.com/tiangolo). + +## 0.103.0 + +### Features + +* ✨ Add support for `openapi_examples` in all FastAPI parameters. PR [#10152](https://github.com/tiangolo/fastapi/pull/10152) by [@tiangolo](https://github.com/tiangolo). + * New docs: [OpenAPI-specific examples](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#openapi-specific-examples). + +### Docs + +* 📝 Add note to docs about Separate Input and Output Schemas with FastAPI version. PR [#10150](https://github.com/tiangolo/fastapi/pull/10150) by [@tiangolo](https://github.com/tiangolo). + +## 0.102.0 + +### Features + +* ✨ Add support for disabling the separation of input and output JSON Schemas in OpenAPI with Pydantic v2 with `separate_input_output_schemas=False`. PR [#10145](https://github.com/tiangolo/fastapi/pull/10145) by [@tiangolo](https://github.com/tiangolo). + * New docs [Separate OpenAPI Schemas for Input and Output or Not](https://fastapi.tiangolo.com/how-to/separate-openapi-schemas/). + * This PR also includes a new setup (internal tools) for generating screenshots for the docs. + +### Refactors + +* ♻️ Refactor tests for new Pydantic 2.2.1. PR [#10115](https://github.com/tiangolo/fastapi/pull/10115) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Add new docs section, How To - Recipes, move docs that don't have to be read by everyone to How To. PR [#10114](https://github.com/tiangolo/fastapi/pull/10114) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update Advanced docs, add links to sponsor courses. PR [#10113](https://github.com/tiangolo/fastapi/pull/10113) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update docs for generating clients. PR [#10112](https://github.com/tiangolo/fastapi/pull/10112) by [@tiangolo](https://github.com/tiangolo). +* 📝 Tweak MkDocs and add redirects. PR [#10111](https://github.com/tiangolo/fastapi/pull/10111) by [@tiangolo](https://github.com/tiangolo). +* 📝 Restructure docs for cloud providers, include links to sponsors. PR [#10110](https://github.com/tiangolo/fastapi/pull/10110) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🔧 Update sponsors, add Speakeasy. PR [#10098](https://github.com/tiangolo/fastapi/pull/10098) by [@tiangolo](https://github.com/tiangolo). + +## 0.101.1 + +### Fixes + +* ✨ Add `ResponseValidationError` printable details, to show up in server error logs. PR [#10078](https://github.com/tiangolo/fastapi/pull/10078) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* ✏️ Fix typo in deprecation warnings in `fastapi/params.py`. PR [#9854](https://github.com/tiangolo/fastapi/pull/9854) by [@russbiggs](https://github.com/russbiggs). +* ✏️ Fix typos in comments on internal code in `fastapi/concurrency.py` and `fastapi/routing.py`. PR [#9590](https://github.com/tiangolo/fastapi/pull/9590) by [@ElliottLarsen](https://github.com/ElliottLarsen). + +### Docs + +* ✏️ Fix typo in release notes. PR [#9835](https://github.com/tiangolo/fastapi/pull/9835) by [@francisbergin](https://github.com/francisbergin). +* 📝 Add external article: Build an SMS Spam Classifier Serverless Database with FaunaDB and FastAPI. PR [#9847](https://github.com/tiangolo/fastapi/pull/9847) by [@adejumoridwan](https://github.com/adejumoridwan). +* 📝 Fix typo in `docs/en/docs/contributing.md`. PR [#9878](https://github.com/tiangolo/fastapi/pull/9878) by [@VicenteMerino](https://github.com/VicenteMerino). +* 📝 Fix code highlighting in `docs/en/docs/tutorial/bigger-applications.md`. PR [#9806](https://github.com/tiangolo/fastapi/pull/9806) by [@theonlykingpin](https://github.com/theonlykingpin). + +### Translations + +* 🌐 Add Japanese translation for `docs/ja/docs/deployment/concepts.md`. PR [#10062](https://github.com/tiangolo/fastapi/pull/10062) by [@tamtam-fitness](https://github.com/tamtam-fitness). +* 🌐 Add Japanese translation for `docs/ja/docs/deployment/server-workers.md`. PR [#10064](https://github.com/tiangolo/fastapi/pull/10064) by [@tamtam-fitness](https://github.com/tamtam-fitness). +* 🌐 Update Japanese translation for `docs/ja/docs/deployment/docker.md`. PR [#10073](https://github.com/tiangolo/fastapi/pull/10073) by [@tamtam-fitness](https://github.com/tamtam-fitness). +* 🌐 Add Ukrainian translation for `docs/uk/docs/fastapi-people.md`. PR [#10059](https://github.com/tiangolo/fastapi/pull/10059) by [@rostik1410](https://github.com/rostik1410). +* 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/cookie-params.md`. PR [#10032](https://github.com/tiangolo/fastapi/pull/10032) by [@rostik1410](https://github.com/rostik1410). +* 🌐 Add Russian translation for `docs/ru/docs/deployment/docker.md`. PR [#9971](https://github.com/tiangolo/fastapi/pull/9971) by [@Xewus](https://github.com/Xewus). +* 🌐 Add Vietnamese translation for `docs/vi/docs/python-types.md`. PR [#10047](https://github.com/tiangolo/fastapi/pull/10047) by [@magiskboy](https://github.com/magiskboy). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/dependencies/global-dependencies.md`. PR [#9970](https://github.com/tiangolo/fastapi/pull/9970) by [@dudyaosuplayer](https://github.com/dudyaosuplayer). +* 🌐 Add Urdu translation for `docs/ur/docs/benchmarks.md`. PR [#9974](https://github.com/tiangolo/fastapi/pull/9974) by [@AhsanSheraz](https://github.com/AhsanSheraz). + +### Internal + +* 🔧 Add sponsor Porter. PR [#10051](https://github.com/tiangolo/fastapi/pull/10051) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors, add Jina back as bronze sponsor. PR [#10050](https://github.com/tiangolo/fastapi/pull/10050) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump mypy from 1.4.0 to 1.4.1. PR [#9756](https://github.com/tiangolo/fastapi/pull/9756) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.1.17 to 9.1.21. PR [#9960](https://github.com/tiangolo/fastapi/pull/9960) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.101.0 + +### Features + +* ✨ Enable Pydantic's serialization mode for responses, add support for Pydantic's `computed_field`, better OpenAPI for response models, proper required attributes, better generated clients. PR [#10011](https://github.com/tiangolo/fastapi/pull/10011) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* ✅ Fix tests for compatibility with pydantic 2.1.1. PR [#9943](https://github.com/tiangolo/fastapi/pull/9943) by [@dmontagu](https://github.com/dmontagu). +* ✅ Fix test error in Windows for `jsonable_encoder`. PR [#9840](https://github.com/tiangolo/fastapi/pull/9840) by [@iudeen](https://github.com/iudeen). + +### Upgrades + +* 📌 Do not allow Pydantic 2.1.0 that breaks (require 2.1.1). PR [#10012](https://github.com/tiangolo/fastapi/pull/10012) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/security/index.md`. PR [#9963](https://github.com/tiangolo/fastapi/pull/9963) by [@eVery1337](https://github.com/eVery1337). +* 🌐 Remove Vietnamese note about missing translation. PR [#9957](https://github.com/tiangolo/fastapi/pull/9957) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 👷 Add GitHub Actions step dump context to debug external failures. PR [#10008](https://github.com/tiangolo/fastapi/pull/10008) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Restore MkDocs Material pin after the fix. PR [#10001](https://github.com/tiangolo/fastapi/pull/10001) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update the Question template to ask for the Pydantic version. PR [#10000](https://github.com/tiangolo/fastapi/pull/10000) by [@tiangolo](https://github.com/tiangolo). +* 📍 Update MkDocs Material dependencies. PR [#9986](https://github.com/tiangolo/fastapi/pull/9986) by [@tiangolo](https://github.com/tiangolo). +* 👥 Update FastAPI People. PR [#9999](https://github.com/tiangolo/fastapi/pull/9999) by [@tiangolo](https://github.com/tiangolo). +* 🐳 Update Dockerfile with compatibility versions, to upgrade later. PR [#9998](https://github.com/tiangolo/fastapi/pull/9998) by [@tiangolo](https://github.com/tiangolo). +* ➕ Add pydantic-settings to FastAPI People dependencies. PR [#9988](https://github.com/tiangolo/fastapi/pull/9988) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Update FastAPI People logic with new Pydantic. PR [#9985](https://github.com/tiangolo/fastapi/pull/9985) by [@tiangolo](https://github.com/tiangolo). +* 🍱 Update sponsors, Fern badge. PR [#9982](https://github.com/tiangolo/fastapi/pull/9982) by [@tiangolo](https://github.com/tiangolo). +* 👷 Deploy docs to Cloudflare Pages. PR [#9978](https://github.com/tiangolo/fastapi/pull/9978) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsor Fern. PR [#9979](https://github.com/tiangolo/fastapi/pull/9979) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update CI debug mode with Tmate. PR [#9977](https://github.com/tiangolo/fastapi/pull/9977) by [@tiangolo](https://github.com/tiangolo). + +## 0.100.1 + +### Fixes + +* 🐛 Replace `MultHostUrl` to `AnyUrl` for compatibility with older versions of Pydantic v1. PR [#9852](https://github.com/tiangolo/fastapi/pull/9852) by [@Kludex](https://github.com/Kludex). + +### Docs + +* 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 31.0. PR [#9834](https://github.com/tiangolo/fastapi/pull/9834) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/body.md`. PR [#4574](https://github.com/tiangolo/fastapi/pull/4574) by [@ss-o-furda](https://github.com/ss-o-furda). +* 🌐 Add Vietnamese translation for `docs/vi/docs/features.md` and `docs/vi/docs/index.md`. PR [#3006](https://github.com/tiangolo/fastapi/pull/3006) by [@magiskboy](https://github.com/magiskboy). +* 🌐 Add Korean translation for `docs/ko/docs/async.md`. PR [#4179](https://github.com/tiangolo/fastapi/pull/4179) by [@NinaHwang](https://github.com/NinaHwang). +* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/background-tasks.md`. PR [#9812](https://github.com/tiangolo/fastapi/pull/9812) by [@wdh99](https://github.com/wdh99). +* 🌐 Add French translation for `docs/fr/docs/tutorial/query-params-str-validations.md`. PR [#4075](https://github.com/tiangolo/fastapi/pull/4075) by [@Smlep](https://github.com/Smlep). +* 🌐 Add French translation for `docs/fr/docs/tutorial/index.md`. PR [#2234](https://github.com/tiangolo/fastapi/pull/2234) by [@JulianMaurin](https://github.com/JulianMaurin). +* 🌐 Add French translation for `docs/fr/docs/contributing.md`. PR [#2132](https://github.com/tiangolo/fastapi/pull/2132) by [@JulianMaurin](https://github.com/JulianMaurin). +* 🌐 Add French translation for `docs/fr/docs/benchmarks.md`. PR [#2155](https://github.com/tiangolo/fastapi/pull/2155) by [@clemsau](https://github.com/clemsau). +* 🌐 Update Chinese translations with new source files. PR [#9738](https://github.com/tiangolo/fastapi/pull/9738) by [@mahone3297](https://github.com/mahone3297). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-forms.md`. PR [#9841](https://github.com/tiangolo/fastapi/pull/9841) by [@dedkot01](https://github.com/dedkot01). +* 🌐 Update Chinese translation for `docs/zh/docs/tutorial/handling-errors.md`. PR [#9485](https://github.com/tiangolo/fastapi/pull/9485) by [@Creat55](https://github.com/Creat55). + +### Internal + +* 🔧 Update sponsors, add Fern. PR [#9956](https://github.com/tiangolo/fastapi/pull/9956) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update FastAPI People token. PR [#9844](https://github.com/tiangolo/fastapi/pull/9844) by [@tiangolo](https://github.com/tiangolo). +* 👥 Update FastAPI People. PR [#9775](https://github.com/tiangolo/fastapi/pull/9775) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update MkDocs Material token. PR [#9843](https://github.com/tiangolo/fastapi/pull/9843) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update token for latest changes. PR [#9842](https://github.com/tiangolo/fastapi/pull/9842) by [@tiangolo](https://github.com/tiangolo). + +## 0.100.0 + +✨ Support for **Pydantic v2** ✨ + +Pydantic version 2 has the **core** re-written in **Rust** and includes a lot of improvements and features, for example: + +* Improved **correctness** in corner cases. +* **Safer** types. +* Better **performance** and **less energy** consumption. +* Better **extensibility**. +* etc. + +...all this while keeping the **same Python API**. In most of the cases, for simple models, you can simply upgrade the Pydantic version and get all the benefits. 🚀 + +In some cases, for pure data validation and processing, you can get performance improvements of **20x** or more. This means 2,000% or more. 🤯 + +When you use **FastAPI**, there's a lot more going on, processing the request and response, handling dependencies, executing **your own code**, and particularly, **waiting for the network**. But you will probably still get some nice performance improvements just from the upgrade. + +The focus of this release is **compatibility** with Pydantic v1 and v2, to make sure your current apps keep working. Later there will be more focus on refactors, correctness, code improvements, and then **performance** improvements. Some third-party early beta testers that ran benchmarks on the beta releases of FastAPI reported improvements of **2x - 3x**. Which is not bad for just doing `pip install --upgrade fastapi pydantic`. This was not an official benchmark and I didn't check it myself, but it's a good sign. + +### Migration + +Check out the [Pydantic migration guide](https://docs.pydantic.dev/2.0/migration/). + +For the things that need changes in your Pydantic models, the Pydantic team built [`bump-pydantic`](https://github.com/pydantic/bump-pydantic). + +A command line tool that will **process your code** and update most of the things **automatically** for you. Make sure you have your code in git first, and review each of the changes to make sure everything is correct before committing the changes. + +### Pydantic v1 + +**This version of FastAPI still supports Pydantic v1**. And although Pydantic v1 will be deprecated at some point, it will still be supported for a while. + +This means that you can install the new Pydantic v2, and if something fails, you can install Pydantic v1 while you fix any problems you might have, but having the latest FastAPI. + +There are **tests for both Pydantic v1 and v2**, and test **coverage** is kept at **100%**. + +### Changes + +* There are **new parameter** fields supported by Pydantic `Field()` for: + + * `Path()` + * `Query()` + * `Header()` + * `Cookie()` + * `Body()` + * `Form()` + * `File()` + +* The new parameter fields are: + + * `default_factory` + * `alias_priority` + * `validation_alias` + * `serialization_alias` + * `discriminator` + * `strict` + * `multiple_of` + * `allow_inf_nan` + * `max_digits` + * `decimal_places` + * `json_schema_extra` + +...you can read about them in the Pydantic docs. + +* The parameter `regex` has been deprecated and replaced by `pattern`. + * You can read more about it in the docs for [Query Parameters and String Validations: Add regular expressions](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#add-regular-expressions). +* New Pydantic models use an improved and simplified attribute `model_config` that takes a simple dict instead of an internal class `Config` for their configuration. + * You can read more about it in the docs for [Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/). +* The attribute `schema_extra` for the internal class `Config` has been replaced by the key `json_schema_extra` in the new `model_config` dict. + * You can read more about it in the docs for [Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/). +* When you install `"fastapi[all]"` it now also includes: + * pydantic-settings - for settings management. + * pydantic-extra-types - for extra types to be used with Pydantic. +* Now Pydantic Settings is an additional optional package (included in `"fastapi[all]"`). To use settings you should now import `from pydantic_settings import BaseSettings` instead of importing from `pydantic` directly. + * You can read more about it in the docs for [Settings and Environment Variables](https://fastapi.tiangolo.com/advanced/settings/). + +* PR [#9816](https://github.com/tiangolo/fastapi/pull/9816) by [@tiangolo](https://github.com/tiangolo), included all the work done (in multiple PRs) on the beta branch (`main-pv2`). + +## 0.99.1 + +### Fixes + +* 🐛 Fix JSON Schema accepting bools as valid JSON Schemas, e.g. `additionalProperties: false`. PR [#9781](https://github.com/tiangolo/fastapi/pull/9781) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Update source examples to use new JSON Schema examples field. PR [#9776](https://github.com/tiangolo/fastapi/pull/9776) by [@tiangolo](https://github.com/tiangolo). + +## 0.99.0 + +### Features + +* ✨ Add support for OpenAPI 3.1.0. PR [#9770](https://github.com/tiangolo/fastapi/pull/9770) by [@tiangolo](https://github.com/tiangolo). + * New support for documenting **webhooks**, read the new docs here: Advanced User Guide: OpenAPI Webhooks. + * Upgrade OpenAPI 3.1.0, this uses JSON Schema 2020-12. + * Upgrade Swagger UI to version 5.x.x, that supports OpenAPI 3.1.0. + * Updated `examples` field in `Query()`, `Cookie()`, `Body()`, etc. based on the latest JSON Schema and OpenAPI. Now it takes a list of examples and they are included directly in the JSON Schema, not outside. Read more about it (including the historical technical details) in the updated docs: Tutorial: Declare Request Example Data. + +* ✨ Add support for `deque` objects and children in `jsonable_encoder`. PR [#9433](https://github.com/tiangolo/fastapi/pull/9433) by [@cranium](https://github.com/cranium). + +### Docs + +* 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). + +### Internal + +* 🔨 Enable linenums in MkDocs Material during local live development to simplify highlighting code. PR [#9769](https://github.com/tiangolo/fastapi/pull/9769) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Update httpx requirement from <0.24.0,>=0.23.0 to >=0.23.0,<0.25.0. PR [#9724](https://github.com/tiangolo/fastapi/pull/9724) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.1.16 to 9.1.17. PR [#9746](https://github.com/tiangolo/fastapi/pull/9746) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔥 Remove missing translation dummy pages, no longer necessary. PR [#9751](https://github.com/tiangolo/fastapi/pull/9751) by [@tiangolo](https://github.com/tiangolo). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#9259](https://github.com/tiangolo/fastapi/pull/9259) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ✨ Add Material for MkDocs Insiders features and cards. PR [#9748](https://github.com/tiangolo/fastapi/pull/9748) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove languages without translations. PR [#9743](https://github.com/tiangolo/fastapi/pull/9743) by [@tiangolo](https://github.com/tiangolo). +* ✨ Refactor docs for building scripts, use MkDocs hooks, simplify (remove) configs for languages. PR [#9742](https://github.com/tiangolo/fastapi/pull/9742) by [@tiangolo](https://github.com/tiangolo). +* 🔨 Add MkDocs hook that renames sections based on the first index file. PR [#9737](https://github.com/tiangolo/fastapi/pull/9737) by [@tiangolo](https://github.com/tiangolo). +* 👷 Make cron jobs run only on main repo, not on forks, to avoid error notifications from missing tokens. PR [#9735](https://github.com/tiangolo/fastapi/pull/9735) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update MkDocs for other languages. PR [#9734](https://github.com/tiangolo/fastapi/pull/9734) by [@tiangolo](https://github.com/tiangolo). +* 👷 Refactor Docs CI, run in multiple workers with a dynamic matrix to optimize speed. PR [#9732](https://github.com/tiangolo/fastapi/pull/9732) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove old internal GitHub Action watch-previews that is no longer needed. PR [#9730](https://github.com/tiangolo/fastapi/pull/9730) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Upgrade MkDocs and MkDocs Material. PR [#9729](https://github.com/tiangolo/fastapi/pull/9729) by [@tiangolo](https://github.com/tiangolo). +* 👷 Build and deploy docs only on docs changes. PR [#9728](https://github.com/tiangolo/fastapi/pull/9728) by [@tiangolo](https://github.com/tiangolo). + +## 0.98.0 + +### Features + +* ✨ Allow disabling `redirect_slashes` at the FastAPI app level. PR [#3432](https://github.com/tiangolo/fastapi/pull/3432) by [@cyberlis](https://github.com/cyberlis). + +### Docs + +* 📝 Update docs on Pydantic using ujson internally. PR [#5804](https://github.com/tiangolo/fastapi/pull/5804) by [@mvasilkov](https://github.com/mvasilkov). +* ✏ Rewording in `docs/en/docs/tutorial/debugging.md`. PR [#9581](https://github.com/tiangolo/fastapi/pull/9581) by [@ivan-abc](https://github.com/ivan-abc). +* 📝 Add german blog post (Domain-driven Design mit Python und FastAPI). PR [#9261](https://github.com/tiangolo/fastapi/pull/9261) by [@msander](https://github.com/msander). +* ✏️ Tweak wording in `docs/en/docs/tutorial/security/index.md`. PR [#9561](https://github.com/tiangolo/fastapi/pull/9561) by [@jyothish-mohan](https://github.com/jyothish-mohan). +* 📝 Update `Annotated` notes in `docs/en/docs/tutorial/schema-extra-example.md`. PR [#9620](https://github.com/tiangolo/fastapi/pull/9620) by [@Alexandrhub](https://github.com/Alexandrhub). +* ✏️ Fix typo `Annotation` -> `Annotated` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9625](https://github.com/tiangolo/fastapi/pull/9625) by [@mccricardo](https://github.com/mccricardo). +* 📝 Use in memory database for testing SQL in docs. PR [#1223](https://github.com/tiangolo/fastapi/pull/1223) by [@HarshaLaxman](https://github.com/HarshaLaxman). + +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/metadata.md`. PR [#9681](https://github.com/tiangolo/fastapi/pull/9681) by [@TabarakoAkula](https://github.com/TabarakoAkula). +* 🌐 Fix typo in Spanish translation for `docs/es/docs/tutorial/first-steps.md`. PR [#9571](https://github.com/tiangolo/fastapi/pull/9571) by [@lilidl-nft](https://github.com/lilidl-nft). +* 🌐 Add Russian translation for `docs/tutorial/path-operation-configuration.md`. PR [#9696](https://github.com/tiangolo/fastapi/pull/9696) by [@TabarakoAkula](https://github.com/TabarakoAkula). +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/security/index.md`. PR [#9666](https://github.com/tiangolo/fastapi/pull/9666) by [@lordqyxz](https://github.com/lordqyxz). +* 🌐 Add Chinese translations for `docs/zh/docs/advanced/settings.md`. PR [#9652](https://github.com/tiangolo/fastapi/pull/9652) by [@ChoyeonChern](https://github.com/ChoyeonChern). +* 🌐 Add Chinese translations for `docs/zh/docs/advanced/websockets.md`. PR [#9651](https://github.com/tiangolo/fastapi/pull/9651) by [@ChoyeonChern](https://github.com/ChoyeonChern). +* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/testing.md`. PR [#9641](https://github.com/tiangolo/fastapi/pull/9641) by [@wdh99](https://github.com/wdh99). +* 🌐 Add Russian translation for `docs/tutorial/extra-models.md`. PR [#9619](https://github.com/tiangolo/fastapi/pull/9619) by [@ivan-abc](https://github.com/ivan-abc). +* 🌐 Add Russian translation for `docs/tutorial/cors.md`. PR [#9608](https://github.com/tiangolo/fastapi/pull/9608) by [@ivan-abc](https://github.com/ivan-abc). +* 🌐 Add Polish translation for `docs/pl/docs/features.md`. PR [#5348](https://github.com/tiangolo/fastapi/pull/5348) by [@mbroton](https://github.com/mbroton). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-nested-models.md`. PR [#9605](https://github.com/tiangolo/fastapi/pull/9605) by [@Alexandrhub](https://github.com/Alexandrhub). + +### Internal + +* ⬆ Bump ruff from 0.0.272 to 0.0.275. PR [#9721](https://github.com/tiangolo/fastapi/pull/9721) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update uvicorn[standard] requirement from <0.21.0,>=0.12.0 to >=0.12.0,<0.23.0. PR [#9463](https://github.com/tiangolo/fastapi/pull/9463) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mypy from 1.3.0 to 1.4.0. PR [#9719](https://github.com/tiangolo/fastapi/pull/9719) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update pre-commit requirement from <3.0.0,>=2.17.0 to >=2.17.0,<4.0.0. PR [#9251](https://github.com/tiangolo/fastapi/pull/9251) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6. PR [#9482](https://github.com/tiangolo/fastapi/pull/9482) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ✏️ Fix tooltips for light/dark theme toggler in docs. PR [#9588](https://github.com/tiangolo/fastapi/pull/9588) by [@pankaj1707k](https://github.com/pankaj1707k). +* 🔧 Set minimal hatchling version needed to build the package. PR [#9240](https://github.com/tiangolo/fastapi/pull/9240) by [@mgorny](https://github.com/mgorny). +* 📝 Add repo link to PyPI. PR [#9559](https://github.com/tiangolo/fastapi/pull/9559) by [@JacobCoffee](https://github.com/JacobCoffee). +* ✏️ Fix typos in data for tests. PR [#4958](https://github.com/tiangolo/fastapi/pull/4958) by [@ryanrussell](https://github.com/ryanrussell). +* 🔧 Update sponsors, add Flint. PR [#9699](https://github.com/tiangolo/fastapi/pull/9699) by [@tiangolo](https://github.com/tiangolo). +* 👷 Lint in CI only once, only with one version of Python, run tests with all of them. PR [#9686](https://github.com/tiangolo/fastapi/pull/9686) by [@tiangolo](https://github.com/tiangolo). + +## 0.97.0 + +### Features + +* ✨ Add support for `dependencies` in WebSocket routes. PR [#4534](https://github.com/tiangolo/fastapi/pull/4534) by [@paulo-raca](https://github.com/paulo-raca). +* ✨ Add exception handler for `WebSocketRequestValidationError` (which also allows to override it). PR [#6030](https://github.com/tiangolo/fastapi/pull/6030) by [@kristjanvalur](https://github.com/kristjanvalur). + +### Refactors + +* ⬆️ Upgrade and fully migrate to Ruff, remove isort, includes a couple of tweaks suggested by the new version of Ruff. PR [#9660](https://github.com/tiangolo/fastapi/pull/9660) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Update internal type annotations and upgrade mypy. PR [#9658](https://github.com/tiangolo/fastapi/pull/9658) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Simplify `AsyncExitStackMiddleware` as without Python 3.6 `AsyncExitStack` is always available. PR [#9657](https://github.com/tiangolo/fastapi/pull/9657) by [@tiangolo](https://github.com/tiangolo). + +### Upgrades + +* ⬆️ Upgrade Black. PR [#9661](https://github.com/tiangolo/fastapi/pull/9661) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 💚 Update CI cache to fix installs when dependencies change. PR [#9659](https://github.com/tiangolo/fastapi/pull/9659) by [@tiangolo](https://github.com/tiangolo). +* ⬇️ Separate requirements for development into their own requirements.txt files, they shouldn't be extras. PR [#9655](https://github.com/tiangolo/fastapi/pull/9655) by [@tiangolo](https://github.com/tiangolo). + +## 0.96.1 + +### Fixes + +* 🐛 Fix `HTTPException` header type annotations. PR [#9648](https://github.com/tiangolo/fastapi/pull/9648) by [@tiangolo](https://github.com/tiangolo). +* 🐛 Fix OpenAPI model fields int validations, `gte` to `ge`. PR [#9635](https://github.com/tiangolo/fastapi/pull/9635) by [@tiangolo](https://github.com/tiangolo). + +### Upgrades + +* 📌 Update minimum version of Pydantic to >=1.7.4. This fixes an issue when trying to use an old version of Pydantic. PR [#9567](https://github.com/tiangolo/fastapi/pull/9567) by [@Kludex](https://github.com/Kludex). + +### Refactors + +* ♻ Remove `media_type` from `ORJSONResponse` as it's inherited from the parent class. PR [#5805](https://github.com/tiangolo/fastapi/pull/5805) by [@Kludex](https://github.com/Kludex). +* ♻ Instantiate `HTTPException` only when needed, optimization refactor. PR [#5356](https://github.com/tiangolo/fastapi/pull/5356) by [@pawamoy](https://github.com/pawamoy). + +### Docs + +* 🔥 Remove link to Pydantic's benchmark, as it was removed there. PR [#5811](https://github.com/tiangolo/fastapi/pull/5811) by [@Kludex](https://github.com/Kludex). + +### Translations + +* 🌐 Fix spelling in Indonesian translation of `docs/id/docs/tutorial/index.md`. PR [#5635](https://github.com/tiangolo/fastapi/pull/5635) by [@purwowd](https://github.com/purwowd). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/index.md`. PR [#5896](https://github.com/tiangolo/fastapi/pull/5896) by [@Wilidon](https://github.com/Wilidon). +* 🌐 Add Chinese translations for `docs/zh/docs/advanced/response-change-status-code.md` and `docs/zh/docs/advanced/response-headers.md`. PR [#9544](https://github.com/tiangolo/fastapi/pull/9544) by [@ChoyeonChern](https://github.com/ChoyeonChern). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/schema-extra-example.md`. PR [#9621](https://github.com/tiangolo/fastapi/pull/9621) by [@Alexandrhub](https://github.com/Alexandrhub). + +### Internal + +* 🔧 Add sponsor Platform.sh. PR [#9650](https://github.com/tiangolo/fastapi/pull/9650) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add custom token to Smokeshow and Preview Docs for download-artifact, to prevent API rate limits. PR [#9646](https://github.com/tiangolo/fastapi/pull/9646) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add custom tokens for GitHub Actions to avoid rate limits. PR [#9647](https://github.com/tiangolo/fastapi/pull/9647) by [@tiangolo](https://github.com/tiangolo). + +## 0.96.0 + +### Features + +* ⚡ Update `create_cloned_field` to use a global cache and improve startup performance. PR [#4645](https://github.com/tiangolo/fastapi/pull/4645) by [@madkinsz](https://github.com/madkinsz) and previous original PR by [@huonw](https://github.com/huonw). + +### Docs + +* 📝 Update Deta deployment tutorial for compatibility with Deta Space. PR [#6004](https://github.com/tiangolo/fastapi/pull/6004) by [@mikBighne98](https://github.com/mikBighne98). +* ✏️ Fix typo in Deta deployment tutorial. PR [#9501](https://github.com/tiangolo/fastapi/pull/9501) by [@lemonyte](https://github.com/lemonyte). + +### Translations + +* 🌐 Add Russian translation for `docs/tutorial/body.md`. PR [#3885](https://github.com/tiangolo/fastapi/pull/3885) by [@solomein-sv](https://github.com/solomein-sv). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/static-files.md`. PR [#9580](https://github.com/tiangolo/fastapi/pull/9580) by [@Alexandrhub](https://github.com/Alexandrhub). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/query-params.md`. PR [#9584](https://github.com/tiangolo/fastapi/pull/9584) by [@Alexandrhub](https://github.com/Alexandrhub). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/first-steps.md`. PR [#9471](https://github.com/tiangolo/fastapi/pull/9471) by [@AGolicyn](https://github.com/AGolicyn). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/debugging.md`. PR [#9579](https://github.com/tiangolo/fastapi/pull/9579) by [@Alexandrhub](https://github.com/Alexandrhub). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params.md`. PR [#9519](https://github.com/tiangolo/fastapi/pull/9519) by [@AGolicyn](https://github.com/AGolicyn). +* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/static-files.md`. PR [#9436](https://github.com/tiangolo/fastapi/pull/9436) by [@wdh99](https://github.com/wdh99). +* 🌐 Update Spanish translation including new illustrations in `docs/es/docs/async.md`. PR [#9483](https://github.com/tiangolo/fastapi/pull/9483) by [@andresbermeoq](https://github.com/andresbermeoq). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params-numeric-validations.md`. PR [#9563](https://github.com/tiangolo/fastapi/pull/9563) by [@ivan-abc](https://github.com/ivan-abc). +* 🌐 Add Russian translation for `docs/ru/docs/deployment/concepts.md`. PR [#9577](https://github.com/tiangolo/fastapi/pull/9577) by [@Xewus](https://github.com/Xewus). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-multiple-params.md`. PR [#9586](https://github.com/tiangolo/fastapi/pull/9586) by [@Alexandrhub](https://github.com/Alexandrhub). + +### Internal + +* 👥 Update FastAPI People. PR [#9602](https://github.com/tiangolo/fastapi/pull/9602) by [@github-actions[bot]](https://github.com/apps/github-actions). +* 🔧 Update sponsors, remove InvestSuite. PR [#9612](https://github.com/tiangolo/fastapi/pull/9612) by [@tiangolo](https://github.com/tiangolo). + +## 0.95.2 + +* ⬆️ Upgrade Starlette version to `>=0.27.0` for a security release. PR [#9541](https://github.com/tiangolo/fastapi/pull/9541) by [@tiangolo](https://github.com/tiangolo). Details on [Starlette's security advisory](https://github.com/encode/starlette/security/advisories/GHSA-v5gw-mw7f-84px). + +### Translations + +* 🌐 Add Portuguese translation for `docs/pt/docs/advanced/events.md`. PR [#9326](https://github.com/tiangolo/fastapi/pull/9326) by [@oandersonmagalhaes](https://github.com/oandersonmagalhaes). +* 🌐 Add Russian translation for `docs/ru/docs/deployment/manually.md`. PR [#9417](https://github.com/tiangolo/fastapi/pull/9417) by [@Xewus](https://github.com/Xewus). +* 🌐 Add setup for translations to Lao. PR [#9396](https://github.com/tiangolo/fastapi/pull/9396) by [@TheBrown](https://github.com/TheBrown). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/testing.md`. PR [#9403](https://github.com/tiangolo/fastapi/pull/9403) by [@Xewus](https://github.com/Xewus). +* 🌐 Add Russian translation for `docs/ru/docs/deployment/https.md`. PR [#9428](https://github.com/tiangolo/fastapi/pull/9428) by [@Xewus](https://github.com/Xewus). +* ✏ Fix command to install requirements in Windows. PR [#9445](https://github.com/tiangolo/fastapi/pull/9445) by [@MariiaRomanuik](https://github.com/MariiaRomanuik). +* 🌐 Add French translation for `docs/fr/docs/advanced/response-directly.md`. PR [#9415](https://github.com/tiangolo/fastapi/pull/9415) by [@axel584](https://github.com/axel584). +* 🌐 Initiate Czech translation setup. PR [#9288](https://github.com/tiangolo/fastapi/pull/9288) by [@3p1463k](https://github.com/3p1463k). +* ✏ Fix typo in Portuguese docs for `docs/pt/docs/index.md`. PR [#9337](https://github.com/tiangolo/fastapi/pull/9337) by [@lucasbalieiro](https://github.com/lucasbalieiro). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-status-code.md`. PR [#9370](https://github.com/tiangolo/fastapi/pull/9370) by [@nadia3373](https://github.com/nadia3373). + +### Internal + +* 🐛 Fix `flask.escape` warning for internal tests. PR [#9468](https://github.com/tiangolo/fastapi/pull/9468) by [@samuelcolvin](https://github.com/samuelcolvin). +* ✅ Refactor 2 tests, for consistency and simplification. PR [#9504](https://github.com/tiangolo/fastapi/pull/9504) by [@tiangolo](https://github.com/tiangolo). +* ✅ Refactor OpenAPI tests, prepare for Pydantic v2. PR [#9503](https://github.com/tiangolo/fastapi/pull/9503) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump dawidd6/action-download-artifact from 2.26.0 to 2.27.0. PR [#9394](https://github.com/tiangolo/fastapi/pull/9394) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 💚 Disable setup-python pip cache in CI. PR [#9438](https://github.com/tiangolo/fastapi/pull/9438) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.6.4 to 1.8.5. PR [#9346](https://github.com/tiangolo/fastapi/pull/9346) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.95.1 + +### Fixes + +* 🐛 Fix using `Annotated` in routers or path operations decorated multiple times. PR [#9315](https://github.com/tiangolo/fastapi/pull/9315) by [@sharonyogev](https://github.com/sharonyogev). + +### Docs + +* 🌐 🔠 📄 🐢 Translate docs to Emoji 🥳 🎉 💥 🤯 🤯. PR [#5385](https://github.com/tiangolo/fastapi/pull/5385) by [@LeeeeT](https://github.com/LeeeeT). +* 📝 Add notification message warning about old versions of FastAPI not supporting `Annotated`. PR [#9298](https://github.com/tiangolo/fastapi/pull/9298) by [@grdworkin](https://github.com/grdworkin). +* 📝 Fix typo in `docs/en/docs/advanced/behind-a-proxy.md`. PR [#5681](https://github.com/tiangolo/fastapi/pull/5681) by [@Leommjr](https://github.com/Leommjr). +* ✏ Fix wrong import from typing module in Persian translations for `docs/fa/docs/index.md`. PR [#6083](https://github.com/tiangolo/fastapi/pull/6083) by [@Kimiaattaei](https://github.com/Kimiaattaei). +* ✏️ Fix format, remove unnecessary asterisks in `docs/en/docs/help-fastapi.md`. PR [#9249](https://github.com/tiangolo/fastapi/pull/9249) by [@armgabrielyan](https://github.com/armgabrielyan). +* ✏ Fix typo in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9272](https://github.com/tiangolo/fastapi/pull/9272) by [@nicornk](https://github.com/nicornk). +* ✏ Fix typo/bug in inline code example in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9273](https://github.com/tiangolo/fastapi/pull/9273) by [@tim-habitat](https://github.com/tim-habitat). +* ✏ Fix typo in `docs/en/docs/tutorial/path-params-numeric-validations.md`. PR [#9282](https://github.com/tiangolo/fastapi/pull/9282) by [@aadarsh977](https://github.com/aadarsh977). +* ✏ Fix typo: 'wll' to 'will' in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9380](https://github.com/tiangolo/fastapi/pull/9380) by [@dasstyxx](https://github.com/dasstyxx). + +### Translations + +* 🌐 Add French translation for `docs/fr/docs/advanced/index.md`. PR [#5673](https://github.com/tiangolo/fastapi/pull/5673) by [@axel584](https://github.com/axel584). +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/body-nested-models.md`. PR [#4053](https://github.com/tiangolo/fastapi/pull/4053) by [@luccasmmg](https://github.com/luccasmmg). +* 🌐 Add Russian translation for `docs/ru/docs/alternatives.md`. PR [#5994](https://github.com/tiangolo/fastapi/pull/5994) by [@Xewus](https://github.com/Xewus). +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/extra-models.md`. PR [#5912](https://github.com/tiangolo/fastapi/pull/5912) by [@LorhanSohaky](https://github.com/LorhanSohaky). +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/path-operation-configuration.md`. PR [#5936](https://github.com/tiangolo/fastapi/pull/5936) by [@LorhanSohaky](https://github.com/LorhanSohaky). +* 🌐 Add Russian translation for `docs/ru/docs/contributing.md`. PR [#6002](https://github.com/tiangolo/fastapi/pull/6002) by [@stigsanek](https://github.com/stigsanek). +* 🌐 Add Korean translation for `docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#9176](https://github.com/tiangolo/fastapi/pull/9176) by [@sehwan505](https://github.com/sehwan505). +* 🌐 Add Russian translation for `docs/ru/docs/project-generation.md`. PR [#9243](https://github.com/tiangolo/fastapi/pull/9243) by [@Xewus](https://github.com/Xewus). +* 🌐 Add French translation for `docs/fr/docs/index.md`. PR [#9265](https://github.com/tiangolo/fastapi/pull/9265) by [@frabc](https://github.com/frabc). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/query-params-str-validations.md`. PR [#9267](https://github.com/tiangolo/fastapi/pull/9267) by [@dedkot01](https://github.com/dedkot01). +* 🌐 Add Russian translation for `docs/ru/docs/benchmarks.md`. PR [#9271](https://github.com/tiangolo/fastapi/pull/9271) by [@Xewus](https://github.com/Xewus). + +### Internal + +* 🔧 Update sponsors: remove Jina. PR [#9388](https://github.com/tiangolo/fastapi/pull/9388) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors, add databento, remove Ines's course and StriveWorks. PR [#9351](https://github.com/tiangolo/fastapi/pull/9351) by [@tiangolo](https://github.com/tiangolo). + +## 0.95.0 + +### Highlights + +This release adds support for dependencies and parameters using `Annotated` and recommends its usage. ✨ + +This has **several benefits**, one of the main ones is that now the parameters of your functions with `Annotated` would **not be affected** at all. + +If you call those functions in **other places in your code**, the actual **default values** will be kept, your editor will help you notice missing **required arguments**, Python will require you to pass required arguments at **runtime**, you will be able to **use the same functions** for different things and with different libraries (e.g. **Typer** will soon support `Annotated` too, then you could use the same function for an API and a CLI), etc. + +Because `Annotated` is **standard Python**, you still get all the **benefits** from editors and tools, like **autocompletion**, **inline errors**, etc. + +One of the **biggest benefits** is that now you can create `Annotated` dependencies that are then shared by multiple *path operation functions*, this will allow you to **reduce** a lot of **code duplication** in your codebase, while keeping all the support from editors and tools. + +For example, you could have code like this: + +```Python +def get_current_user(token: str): + # authenticate user + return User() + + +@app.get("/items/") +def read_items(user: User = Depends(get_current_user)): + ... + + +@app.post("/items/") +def create_item(*, user: User = Depends(get_current_user), item: Item): + ... + + +@app.get("/items/{item_id}") +def read_item(*, user: User = Depends(get_current_user), item_id: int): + ... + + +@app.delete("/items/{item_id}") +def delete_item(*, user: User = Depends(get_current_user), item_id: int): + ... +``` + +There's a bit of code duplication for the dependency: + +```Python +user: User = Depends(get_current_user) +``` + +...the bigger the codebase, the more noticeable it is. + +Now you can create an annotated dependency once, like this: + +```Python +CurrentUser = Annotated[User, Depends(get_current_user)] +``` + +And then you can reuse this `Annotated` dependency: + +```Python +CurrentUser = Annotated[User, Depends(get_current_user)] + + +@app.get("/items/") +def read_items(user: CurrentUser): + ... + + +@app.post("/items/") +def create_item(user: CurrentUser, item: Item): + ... + + +@app.get("/items/{item_id}") +def read_item(user: CurrentUser, item_id: int): + ... + + +@app.delete("/items/{item_id}") +def delete_item(user: CurrentUser, item_id: int): + ... +``` + +...and `CurrentUser` has all the typing information as `User`, so your editor will work as expected (autocompletion and everything), and **FastAPI** will be able to understand the dependency defined in `Annotated`. 😎 + +Roughly **all the docs** have been rewritten to use `Annotated` as the main way to declare **parameters** and **dependencies**. All the **examples** in the docs now include a version with `Annotated` and a version without it, for each of the specific Python versions (when there are small differences/improvements in more recent versions). There were around 23K new lines added between docs, examples, and tests. 🚀 + +The key updated docs are: + +* Python Types Intro: + * [Type Hints with Metadata Annotations](https://fastapi.tiangolo.com/python-types/#type-hints-with-metadata-annotations). +* Tutorial: + * [Query Parameters and String Validations - Additional validation](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#additional-validation) + * [Advantages of `Annotated`](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#advantages-of-annotated) + * [Path Parameters and Numeric Validations - Order the parameters as you need, tricks](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#order-the-parameters-as-you-need-tricks) + * [Better with `Annotated`](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#better-with-annotated) + * [Dependencies - First Steps - Share `Annotated` dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/#share-annotated-dependencies) + +Special thanks to [@nzig](https://github.com/nzig) for the core implementation and to [@adriangb](https://github.com/adriangb) for the inspiration and idea with [Xpresso](https://github.com/adriangb/xpresso)! 🚀 + +### Features + +* ✨Add support for PEP-593 `Annotated` for specifying dependencies and parameters. PR [#4871](https://github.com/tiangolo/fastapi/pull/4871) by [@nzig](https://github.com/nzig). + +### Docs + +* 📝 Tweak tip recommending `Annotated` in docs. PR [#9270](https://github.com/tiangolo/fastapi/pull/9270) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update order of examples, latest Python version first, and simplify version tab names. PR [#9269](https://github.com/tiangolo/fastapi/pull/9269) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update all docs to use `Annotated` as the main recommendation, with new examples and tests. PR [#9268](https://github.com/tiangolo/fastapi/pull/9268) by [@tiangolo](https://github.com/tiangolo). + +## 0.94.1 + +### Fixes + +* 🎨 Fix types for lifespan, upgrade Starlette to 0.26.1. PR [#9245](https://github.com/tiangolo/fastapi/pull/9245) by [@tiangolo](https://github.com/tiangolo). + +## 0.94.0 + +### Upgrades + +* ⬆ Upgrade python-multipart to support 0.0.6. PR [#9212](https://github.com/tiangolo/fastapi/pull/9212) by [@musicinmybrain](https://github.com/musicinmybrain). +* ⬆️ Upgrade Starlette version, support new `lifespan` with state. PR [#9239](https://github.com/tiangolo/fastapi/pull/9239) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Update Sentry link in docs. PR [#9218](https://github.com/tiangolo/fastapi/pull/9218) by [@smeubank](https://github.com/smeubank). + +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/history-design-future.md`. PR [#5986](https://github.com/tiangolo/fastapi/pull/5986) by [@Xewus](https://github.com/Xewus). + +### Internal + +* ➕ Add `pydantic` to PyPI classifiers. PR [#5914](https://github.com/tiangolo/fastapi/pull/5914) by [@yezz123](https://github.com/yezz123). +* ⬆ Bump black from 22.10.0 to 23.1.0. PR [#5953](https://github.com/tiangolo/fastapi/pull/5953) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump types-ujson from 5.6.0.0 to 5.7.0.1. PR [#6027](https://github.com/tiangolo/fastapi/pull/6027) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump dawidd6/action-download-artifact from 2.24.3 to 2.26.0. PR [#6034](https://github.com/tiangolo/fastapi/pull/6034) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5709](https://github.com/tiangolo/fastapi/pull/5709) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + +## 0.93.0 + +### Features + +* ✨ Add support for `lifespan` async context managers (superseding `startup` and `shutdown` events). Initial PR [#2944](https://github.com/tiangolo/fastapi/pull/2944) by [@uSpike](https://github.com/uSpike). + +Now, instead of using independent `startup` and `shutdown` events, you can define that logic in a single function with `yield` decorated with `@asynccontextmanager` (an async context manager). + +For example: + +```Python +from contextlib import asynccontextmanager + +from fastapi import FastAPI + + +def fake_answer_to_everything_ml_model(x: float): + return x * 42 + + +ml_models = {} + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Load the ML model + ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model + yield + # Clean up the ML models and release the resources + ml_models.clear() + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/predict") +async def predict(x: float): + result = ml_models["answer_to_everything"](x) + return {"result": result} +``` + +**Note**: This is the recommended way going forward, instead of using `startup` and `shutdown` events. + +Read more about it in the new docs: [Advanced User Guide: Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + +### Docs + +* ✏ Fix formatting in `docs/en/docs/tutorial/metadata.md` for `ReDoc`. PR [#6005](https://github.com/tiangolo/fastapi/pull/6005) by [@eykamp](https://github.com/eykamp). + +### Translations + +* 🌐 Tamil translations - initial setup. PR [#5564](https://github.com/tiangolo/fastapi/pull/5564) by [@gusty1g](https://github.com/gusty1g). +* 🌐 Add French translation for `docs/fr/docs/advanced/path-operation-advanced-configuration.md`. PR [#9221](https://github.com/tiangolo/fastapi/pull/9221) by [@axel584](https://github.com/axel584). +* 🌐 Add French translation for `docs/tutorial/debugging.md`. PR [#9175](https://github.com/tiangolo/fastapi/pull/9175) by [@frabc](https://github.com/frabc). +* 🌐 Initiate Armenian translation setup. PR [#5844](https://github.com/tiangolo/fastapi/pull/5844) by [@har8](https://github.com/har8). +* 🌐 Add French translation for `deployment/manually.md`. PR [#3693](https://github.com/tiangolo/fastapi/pull/3693) by [@rjNemo](https://github.com/rjNemo). + +### Internal + +* 👷 Update translation bot messages. PR [#9206](https://github.com/tiangolo/fastapi/pull/9206) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update translations bot to use Discussions, and notify when a PR is done. PR [#9183](https://github.com/tiangolo/fastapi/pull/9183) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors-badges. PR [#9182](https://github.com/tiangolo/fastapi/pull/9182) by [@tiangolo](https://github.com/tiangolo). +* 👥 Update FastAPI People. PR [#9181](https://github.com/tiangolo/fastapi/pull/9181) by [@github-actions[bot]](https://github.com/apps/github-actions). +* 🔊 Log GraphQL errors in FastAPI People, because it returns 200, with a payload with an error. PR [#9171](https://github.com/tiangolo/fastapi/pull/9171) by [@tiangolo](https://github.com/tiangolo). +* 💚 Fix/workaround GitHub Actions in Docker with git for FastAPI People. PR [#9169](https://github.com/tiangolo/fastapi/pull/9169) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Refactor FastAPI Experts to use only discussions now that questions are migrated. PR [#9165](https://github.com/tiangolo/fastapi/pull/9165) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Upgrade analytics. PR [#6025](https://github.com/tiangolo/fastapi/pull/6025) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Upgrade and re-enable installing Typer-CLI. PR [#6008](https://github.com/tiangolo/fastapi/pull/6008) by [@tiangolo](https://github.com/tiangolo). + +## 0.92.0 + +🚨 This is a security fix. Please upgrade as soon as possible. + +### Upgrades + +* ⬆️ Upgrade Starlette to 0.25.0. PR [#5996](https://github.com/tiangolo/fastapi/pull/5996) by [@tiangolo](https://github.com/tiangolo). + * This solves a vulnerability that could allow denial of service attacks by using many small multipart fields/files (parts), consuming high CPU and memory. + * Only applications using forms (e.g. file uploads) could be affected. + * For most cases, upgrading won't have any breaking changes. + +## 0.91.0 + +### Upgrades + +* ⬆️ Upgrade Starlette version to `0.24.0` and refactor internals for compatibility. PR [#5985](https://github.com/tiangolo/fastapi/pull/5985) by [@tiangolo](https://github.com/tiangolo). + * This can solve nuanced errors when using middlewares. Before Starlette `0.24.0`, a new instance of each middleware class would be created when a new middleware was added. That normally was not a problem, unless the middleware class expected to be created only once, with only one instance, that happened in some cases. This upgrade would solve those cases (thanks [@adriangb](https://github.com/adriangb)! Starlette PR [#2017](https://github.com/encode/starlette/pull/2017)). Now the middleware class instances are created once, right before the first request (the first time the app is called). + * If you depended on that previous behavior, you might need to update your code. As always, make sure your tests pass before merging the upgrade. + +## 0.90.1 + +### Upgrades + +* ⬆️ Upgrade Starlette range to allow 0.23.1. PR [#5980](https://github.com/tiangolo/fastapi/pull/5980) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ✏ Tweak wording to clarify `docs/en/docs/project-generation.md`. PR [#5930](https://github.com/tiangolo/fastapi/pull/5930) by [@chandra-deb](https://github.com/chandra-deb). +* ✏ Update Pydantic GitHub URLs. PR [#5952](https://github.com/tiangolo/fastapi/pull/5952) by [@yezz123](https://github.com/yezz123). +* 📝 Add opinion from Cisco. PR [#5981](https://github.com/tiangolo/fastapi/pull/5981) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/cookie-params.md`. PR [#5890](https://github.com/tiangolo/fastapi/pull/5890) by [@bnzone](https://github.com/bnzone). + +### Internal + +* ✏ Update `zip-docs.sh` internal script, remove extra space. PR [#5931](https://github.com/tiangolo/fastapi/pull/5931) by [@JuanPerdomo00](https://github.com/JuanPerdomo00). + +## 0.90.0 + +### Upgrades + +* ⬆️ Bump Starlette from 0.22.0 to 0.23.0. Initial PR [#5739](https://github.com/tiangolo/fastapi/pull/5739) by [@Kludex](https://github.com/Kludex). + +### Docs + +* 📝 Add article "Tortoise ORM / FastAPI 整合快速筆記" to External Links. PR [#5496](https://github.com/tiangolo/fastapi/pull/5496) by [@Leon0824](https://github.com/Leon0824). +* 👥 Update FastAPI People. PR [#5954](https://github.com/tiangolo/fastapi/pull/5954) by [@github-actions[bot]](https://github.com/apps/github-actions). +* 📝 Micro-tweak help docs. PR [#5960](https://github.com/tiangolo/fastapi/pull/5960) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update new issue chooser to direct to GitHub Discussions. PR [#5948](https://github.com/tiangolo/fastapi/pull/5948) by [@tiangolo](https://github.com/tiangolo). +* 📝 Recommend GitHub Discussions for questions. PR [#5944](https://github.com/tiangolo/fastapi/pull/5944) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-fields.md`. PR [#5898](https://github.com/tiangolo/fastapi/pull/5898) by [@simatheone](https://github.com/simatheone). +* 🌐 Add Russian translation for `docs/ru/docs/help-fastapi.md`. PR [#5970](https://github.com/tiangolo/fastapi/pull/5970) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/static-files.md`. PR [#5858](https://github.com/tiangolo/fastapi/pull/5858) by [@batlopes](https://github.com/batlopes). +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/encoder.md`. PR [#5525](https://github.com/tiangolo/fastapi/pull/5525) by [@felipebpl](https://github.com/felipebpl). +* 🌐 Add Russian translation for `docs/ru/docs/contributing.md`. PR [#5870](https://github.com/tiangolo/fastapi/pull/5870) by [@Xewus](https://github.com/Xewus). + +### Internal + +* ⬆️ Upgrade Ubuntu version for docs workflow. PR [#5971](https://github.com/tiangolo/fastapi/pull/5971) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors badges. PR [#5943](https://github.com/tiangolo/fastapi/pull/5943) by [@tiangolo](https://github.com/tiangolo). +* ✨ Compute FastAPI Experts including GitHub Discussions. PR [#5941](https://github.com/tiangolo/fastapi/pull/5941) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Upgrade isort and update pre-commit. PR [#5940](https://github.com/tiangolo/fastapi/pull/5940) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add template for questions in Discussions. PR [#5920](https://github.com/tiangolo/fastapi/pull/5920) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update Sponsor Budget Insight to Powens. PR [#5916](https://github.com/tiangolo/fastapi/pull/5916) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update GitHub Sponsors badge data. PR [#5915](https://github.com/tiangolo/fastapi/pull/5915) by [@tiangolo](https://github.com/tiangolo). + +## 0.89.1 + +### Fixes + +* 🐛 Ignore Response classes on return annotation. PR [#5855](https://github.com/tiangolo/fastapi/pull/5855) by [@Kludex](https://github.com/Kludex). See the new docs in the PR below. + +### Docs + +* 📝 Update docs and examples for Response Model with Return Type Annotations, and update runtime error. PR [#5873](https://github.com/tiangolo/fastapi/pull/5873) by [@tiangolo](https://github.com/tiangolo). New docs at [Response Model - Return Type: Other Return Type Annotations](https://fastapi.tiangolo.com/tutorial/response-model/#other-return-type-annotations). +* 📝 Add External Link: FastAPI lambda container: serverless simplified. PR [#5784](https://github.com/tiangolo/fastapi/pull/5784) by [@rafrasenberg](https://github.com/rafrasenberg). + +### Translations + +* 🌐 Add Turkish translation for `docs/tr/docs/tutorial/first_steps.md`. PR [#5691](https://github.com/tiangolo/fastapi/pull/5691) by [@Kadermiyanyedi](https://github.com/Kadermiyanyedi). + ## 0.89.0 ### Features @@ -1193,7 +2359,7 @@ Thanks to [Dima Boger](https://twitter.com/b0g3r) for the security report! 🙇 ### Security fixes -* 📌 Upgrade pydantic pin, to handle security vulnerability [CVE-2021-29510](https://github.com/samuelcolvin/pydantic/security/advisories/GHSA-5jqp-qgf6-3pvh). PR [#3213](https://github.com/tiangolo/fastapi/pull/3213) by [@tiangolo](https://github.com/tiangolo). +* 📌 Upgrade pydantic pin, to handle security vulnerability [CVE-2021-29510](https://github.com/pydantic/pydantic/security/advisories/GHSA-5jqp-qgf6-3pvh). PR [#3213](https://github.com/tiangolo/fastapi/pull/3213) by [@tiangolo](https://github.com/tiangolo). ## 0.65.0 @@ -1748,11 +2914,11 @@ Note: all the previous parameters are still there, so it's still possible to dec ## 0.55.1 -* Fix handling of enums with their own schema in path parameters. To support [samuelcolvin/pydantic#1432](https://github.com/samuelcolvin/pydantic/pull/1432) in FastAPI. PR [#1463](https://github.com/tiangolo/fastapi/pull/1463). +* Fix handling of enums with their own schema in path parameters. To support [pydantic/pydantic#1432](https://github.com/pydantic/pydantic/pull/1432) in FastAPI. PR [#1463](https://github.com/tiangolo/fastapi/pull/1463). ## 0.55.0 -* Allow enums to allow them to have their own schemas in OpenAPI. To support [samuelcolvin/pydantic#1432](https://github.com/samuelcolvin/pydantic/pull/1432) in FastAPI. PR [#1461](https://github.com/tiangolo/fastapi/pull/1461). +* Allow enums to allow them to have their own schemas in OpenAPI. To support [pydantic/pydantic#1432](https://github.com/pydantic/pydantic/pull/1432) in FastAPI. PR [#1461](https://github.com/tiangolo/fastapi/pull/1461). * Add links for funding through [GitHub sponsors](https://github.com/sponsors/tiangolo). PR [#1425](https://github.com/tiangolo/fastapi/pull/1425). * Update issue template for for questions. PR [#1344](https://github.com/tiangolo/fastapi/pull/1344) by [@retnikt](https://github.com/retnikt). * Update warning about storing passwords in docs. PR [#1336](https://github.com/tiangolo/fastapi/pull/1336) by [@skorokithakis](https://github.com/skorokithakis). @@ -2275,7 +3441,7 @@ Note: all the previous parameters are still there, so it's still possible to dec * Add OAuth2 redirect page for Swagger UI. This allows having delegated authentication in the Swagger UI docs. For this to work, you need to add `{your_origin}/docs/oauth2-redirect` to the allowed callbacks in your OAuth2 provider (in Auth0, Facebook, Google, etc). * For example, during development, it could be `http://localhost:8000/docs/oauth2-redirect`. - * Have in mind that this callback URL is independent of whichever one is used by your frontend. You might also have another callback at `https://yourdomain.com/login/callback`. + * Keep in mind that this callback URL is independent of whichever one is used by your frontend. You might also have another callback at `https://yourdomain.com/login/callback`. * This is only to allow delegated authentication in the API docs with Swagger UI. * PR [#198](https://github.com/tiangolo/fastapi/pull/198) by [@steinitzu](https://github.com/steinitzu). diff --git a/docs/en/docs/resources/index.md b/docs/en/docs/resources/index.md new file mode 100644 index 0000000000..8c7cac43bd --- /dev/null +++ b/docs/en/docs/resources/index.md @@ -0,0 +1,3 @@ +# Resources + +Additional resources, external links, articles and more. ✈️ diff --git a/docs/en/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md index 191a4ca0fb..bc8e2af6a0 100644 --- a/docs/en/docs/tutorial/background-tasks.md +++ b/docs/en/docs/tutorial/background-tasks.md @@ -57,18 +57,42 @@ Using `BackgroundTasks` also works with the dependency injection system, you can **FastAPI** knows what to do in each case and how to re-use the same object, so that all the background tasks are merged together and are run in the background afterwards: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="13 15 22 25" - {!> ../../../docs_src/background_tasks/tutorial002.py!} + {!> ../../../docs_src/background_tasks/tutorial002_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="14 16 23 26" + {!> ../../../docs_src/background_tasks/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="11 13 20 23" {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + In this example, the messages will be written to the `log.txt` file *after* the response is sent. If there was a query in the request, it will be written to the log in a background task. diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index d201953df4..b2d9284052 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -79,7 +79,7 @@ You can create the *path operations* for that module using `APIRouter`. You import it and create an "instance" the same way you would with the class `FastAPI`: -```Python hl_lines="1 3" +```Python hl_lines="1 3" title="app/routers/users.py" {!../../../docs_src/bigger_applications/app/routers/users.py!} ``` @@ -89,7 +89,7 @@ And then you use it to declare your *path operations*. Use it the same way you would use the `FastAPI` class: -```Python hl_lines="6 11 16" +```Python hl_lines="6 11 16" title="app/routers/users.py" {!../../../docs_src/bigger_applications/app/routers/users.py!} ``` @@ -112,9 +112,26 @@ So we put them in their own `dependencies` module (`app/dependencies.py`). We will now use a simple dependency to read a custom `X-Token` header: -```Python hl_lines="1 4-6" -{!../../../docs_src/bigger_applications/app/dependencies.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="3 6-8" title="app/dependencies.py" + {!> ../../../docs_src/bigger_applications/app_an_py39/dependencies.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 5-7" title="app/dependencies.py" + {!> ../../../docs_src/bigger_applications/app_an/dependencies.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="1 4-6" title="app/dependencies.py" + {!> ../../../docs_src/bigger_applications/app/dependencies.py!} + ``` !!! tip We are using an invented header to simplify this example. @@ -143,7 +160,7 @@ We know all the *path operations* in this module have the same: So, instead of adding all that to each *path operation*, we can add it to the `APIRouter`. -```Python hl_lines="5-10 16 21" +```Python hl_lines="5-10 16 21" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -189,13 +206,13 @@ The end result is that the item paths are now: ### Import the dependencies -This codes lives in the module `app.routers.items`, the file `app/routers/items.py`. +This code lives in the module `app.routers.items`, the file `app/routers/items.py`. And we need to get the dependency function from the module `app.dependencies`, the file `app/dependencies.py`. So we use a relative import with `..` for the dependencies: -```Python hl_lines="3" +```Python hl_lines="3" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -265,7 +282,7 @@ We are not adding the prefix `/items` nor the `tags=["items"]` to each *path ope But we can still add _more_ `tags` that will be applied to a specific *path operation*, and also some extra `responses` specific to that *path operation*: -```Python hl_lines="30-31" +```Python hl_lines="30-31" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -290,7 +307,7 @@ You import and create a `FastAPI` class as normally. And we can even declare [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank} that will be combined with the dependencies for each `APIRouter`: -```Python hl_lines="1 3 7" +```Python hl_lines="1 3 7" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -298,7 +315,7 @@ And we can even declare [global dependencies](dependencies/global-dependencies.m Now we import the other submodules that have `APIRouter`s: -```Python hl_lines="5" +```Python hl_lines="4-5" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -360,7 +377,7 @@ The `router` from `users` would overwrite the one from `items` and we wouldn't b So, to be able to use both of them in the same file, we import the submodules directly: -```Python hl_lines="4" +```Python hl_lines="5" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -368,7 +385,7 @@ So, to be able to use both of them in the same file, we import the submodules di Now, let's include the `router`s from the submodules `users` and `items`: -```Python hl_lines="10-11" +```Python hl_lines="10-11" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -401,7 +418,7 @@ It contains an `APIRouter` with some admin *path operations* that your organizat For this example it will be super simple. But let's say that because it is shared with other projects in the organization, we cannot modify it and add a `prefix`, `dependencies`, `tags`, etc. directly to the `APIRouter`: -```Python hl_lines="3" +```Python hl_lines="3" title="app/internal/admin.py" {!../../../docs_src/bigger_applications/app/internal/admin.py!} ``` @@ -409,7 +426,7 @@ But we still want to set a custom `prefix` when including the `APIRouter` so tha We can declare all that without having to modify the original `APIRouter` by passing those parameters to `app.include_router()`: -```Python hl_lines="14-17" +```Python hl_lines="14-17" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -432,7 +449,7 @@ We can also add *path operations* directly to the `FastAPI` app. Here we do it... just to show that we can 🤷: -```Python hl_lines="21-23" +```Python hl_lines="21-23" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index 0cfe576d68..55e67fdd63 100644 --- a/docs/en/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -6,18 +6,42 @@ The same way you can declare additional validation and metadata in *path operati First, you have to import it: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="4" - {!> ../../../docs_src/body_fields/tutorial001.py!} + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="2" {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + !!! warning Notice that `Field` is imported directly from `pydantic`, not from `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc). @@ -25,18 +49,42 @@ First, you have to import it: You can then use `Field` with model attributes: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="11-14" - {!> ../../../docs_src/body_fields/tutorial001.py!} + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12-15" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9-12" {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + `Field` works the same way as `Query`, `Path` and `Body`, it has all the same parameters, etc. !!! note "Technical Details" diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index 31dd27fed4..ebef8eeaa9 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -8,18 +8,42 @@ First, of course, you can mix `Path`, `Query` and request body parameter declara And you can also declare body parameters as optional, by setting the default to `None`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="19-21" - {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17-19" {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + !!! note Notice that, in this case, the `item` that would be taken from the body is optional. As it has a `None` default value. @@ -38,18 +62,18 @@ In the previous example, the *path operations* would expect a JSON body with the But you can also declare multiple body parameters, e.g. `item` and `user`: -=== "Python 3.6 and above" - - ```Python hl_lines="22" - {!> ../../../docs_src/body_multiple_params/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="20" {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` + In this case, **FastAPI** will notice that there are more than one body parameters in the function (two parameters that are Pydantic models). So, it will then use the parameter names as keys (field names) in the body, and expect a body like: @@ -87,18 +111,42 @@ If you declare it as is, because it is a singular value, **FastAPI** will assume But you can instruct **FastAPI** to treat it as another body key using `Body`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="22" - {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="24" + {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="20" {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ``` + In this case, **FastAPI** will expect a body like: ```JSON @@ -137,18 +185,42 @@ q: str | None = None For example: -=== "Python 3.6 and above" +=== "Python 3.10+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="28" + {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="25" + {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="27" {!> ../../../docs_src/body_multiple_params/tutorial004.py!} ``` -=== "Python 3.10 and above" - - ```Python hl_lines="26" - {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} - ``` - !!! info `Body` also has all the same extra validation and metadata parameters as `Query`,`Path` and others you will see later. @@ -166,18 +238,42 @@ item: Item = Body(embed=True) as in: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="17" - {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="15" {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + In this case **FastAPI** will expect a body like: ```JSON hl_lines="2" diff --git a/docs/en/docs/tutorial/body-nested-models.md b/docs/en/docs/tutorial/body-nested-models.md index d51f171d6e..7058d4ad04 100644 --- a/docs/en/docs/tutorial/body-nested-models.md +++ b/docs/en/docs/tutorial/body-nested-models.md @@ -6,18 +6,18 @@ With **FastAPI**, you can define, validate, document, and use arbitrarily deeply You can define an attribute to be a subtype. For example, a Python `list`: -=== "Python 3.6 and above" - - ```Python hl_lines="14" - {!> ../../../docs_src/body_nested_models/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="12" {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial001.py!} + ``` + This will make `tags` be a list, although it doesn't declare the type of the elements of the list. ## List fields with type parameter @@ -61,22 +61,22 @@ Use that same standard syntax for model attributes with internal types. So, in our example, we can make `tags` be specifically a "list of strings": -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="14" - {!> ../../../docs_src/body_nested_models/tutorial002.py!} + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="14" {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="12" - {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002.py!} ``` ## Set types @@ -87,22 +87,22 @@ And Python has a special data type for sets of unique items, the `set`. Then we can declare `tags` as a set of strings: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="1 14" - {!> ../../../docs_src/body_nested_models/tutorial003.py!} + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="14" {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="12" - {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} + ```Python hl_lines="1 14" + {!> ../../../docs_src/body_nested_models/tutorial003.py!} ``` With this, even if you receive a request with duplicate data, it will be converted to a set of unique items. @@ -125,44 +125,44 @@ All that, arbitrarily nested. For example, we can define an `Image` model: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="9-11" - {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ```Python hl_lines="7-9" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9-11" {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="7-9" - {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} ``` ### Use the submodel as a type And then we can use it as the type of an attribute: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="20" - {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="20" {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="18" - {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} ``` This would mean that **FastAPI** would expect a body similar to: @@ -183,62 +183,62 @@ This would mean that **FastAPI** would expect a body similar to: Again, doing just that declaration, with **FastAPI** you get: -* Editor support (completion, etc), even for nested models +* Editor support (completion, etc.), even for nested models * Data conversion * Data validation * Automatic documentation ## Special types and validation -Apart from normal singular types like `str`, `int`, `float`, etc. You can use more complex singular types that inherit from `str`. +Apart from normal singular types like `str`, `int`, `float`, etc. you can use more complex singular types that inherit from `str`. To see all the options you have, checkout the docs for Pydantic's exotic types. You will see some examples in the next chapter. -For example, as in the `Image` model we have a `url` field, we can declare it to be instead of a `str`, a Pydantic's `HttpUrl`: +For example, as in the `Image` model we have a `url` field, we can declare it to be an instance of Pydantic's `HttpUrl` instead of a `str`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="4 10" - {!> ../../../docs_src/body_nested_models/tutorial005.py!} + ```Python hl_lines="2 8" + {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="4 10" {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="2 8" - {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005.py!} ``` The string will be checked to be a valid URL, and documented in JSON Schema / OpenAPI as such. ## Attributes with lists of submodels -You can also use Pydantic models as subtypes of `list`, `set`, etc: +You can also use Pydantic models as subtypes of `list`, `set`, etc.: -=== "Python 3.6 and above" - - ```Python hl_lines="20" - {!> ../../../docs_src/body_nested_models/tutorial006.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="20" - {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="18" {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} ``` -This will expect (convert, validate, document, etc) a JSON body like: +=== "Python 3.9+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006.py!} + ``` + +This will expect (convert, validate, document, etc.) a JSON body like: ```JSON hl_lines="11" { @@ -271,22 +271,22 @@ This will expect (convert, validate, document, etc) a JSON body like: You can define arbitrarily deeply nested models: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="9 14 20 23 27" - {!> ../../../docs_src/body_nested_models/tutorial007.py!} + ```Python hl_lines="7 12 18 21 25" + {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9 14 20 23 27" {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="7 12 18 21 25" - {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007.py!} ``` !!! info @@ -308,18 +308,18 @@ images: list[Image] as in: -=== "Python 3.6 and above" - - ```Python hl_lines="15" - {!> ../../../docs_src/body_nested_models/tutorial008.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="13" {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_nested_models/tutorial008.py!} + ``` + ## Editor support everywhere And you get editor support everywhere. @@ -334,34 +334,34 @@ But you don't have to worry about them either, incoming dicts are converted auto ## Bodies of arbitrary `dict`s -You can also declare a body as a `dict` with keys of some type and values of other type. +You can also declare a body as a `dict` with keys of some type and values of some other type. -Without having to know beforehand what are the valid field/attribute names (as would be the case with Pydantic models). +This way, you don't have to know beforehand what the valid field/attribute names are (as would be the case with Pydantic models). This would be useful if you want to receive keys that you don't already know. --- -Other useful case is when you want to have keys of other type, e.g. `int`. +Another useful case is when you want to have keys of another type (e.g., `int`). That's what we are going to see here. In this case, you would accept any `dict` as long as it has `int` keys with `float` values: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/body_nested_models/tutorial009.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="7" {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/body_nested_models/tutorial009.py!} + ``` + !!! tip - Have in mind that JSON only supports `str` as keys. + Keep in mind that JSON only supports `str` as keys. But Pydantic has automatic data conversion. diff --git a/docs/en/docs/tutorial/body-updates.md b/docs/en/docs/tutorial/body-updates.md index 7d8675060f..39d133c55f 100644 --- a/docs/en/docs/tutorial/body-updates.md +++ b/docs/en/docs/tutorial/body-updates.md @@ -6,22 +6,22 @@ To update an item you can use the ../../../docs_src/body_updates/tutorial001.py!} + ```Python hl_lines="28-33" + {!> ../../../docs_src/body_updates/tutorial001_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="30-35" {!> ../../../docs_src/body_updates/tutorial001_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="28-33" - {!> ../../../docs_src/body_updates/tutorial001_py310.py!} + ```Python hl_lines="30-35" + {!> ../../../docs_src/body_updates/tutorial001.py!} ``` `PUT` is used to receive data that should replace the existing data. @@ -59,54 +59,64 @@ This means that you can send only the data that you want to update, leaving the ### Using Pydantic's `exclude_unset` parameter -If you want to receive partial updates, it's very useful to use the parameter `exclude_unset` in Pydantic's model's `.dict()`. +If you want to receive partial updates, it's very useful to use the parameter `exclude_unset` in Pydantic's model's `.model_dump()`. -Like `item.dict(exclude_unset=True)`. +Like `item.model_dump(exclude_unset=True)`. + +!!! info + In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. + + The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values. Then you can use this to generate a `dict` with only the data that was set (sent in the request), omitting default values: -=== "Python 3.6 and above" - - ```Python hl_lines="34" - {!> ../../../docs_src/body_updates/tutorial002.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="34" - {!> ../../../docs_src/body_updates/tutorial002_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="32" {!> ../../../docs_src/body_updates/tutorial002_py310.py!} ``` -### Using Pydantic's `update` parameter +=== "Python 3.9+" -Now, you can create a copy of the existing model using `.copy()`, and pass the `update` parameter with a `dict` containing the data to update. + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` -Like `stored_item_model.copy(update=update_data)`: +=== "Python 3.8+" -=== "Python 3.6 and above" - - ```Python hl_lines="35" + ```Python hl_lines="34" {!> ../../../docs_src/body_updates/tutorial002.py!} ``` -=== "Python 3.9 and above" +### Using Pydantic's `update` parameter + +Now, you can create a copy of the existing model using `.model_copy()`, and pass the `update` parameter with a `dict` containing the data to update. + +!!! info + In Pydantic v1 the method was called `.copy()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_copy()`. + + The examples here use `.copy()` for compatibility with Pydantic v1, but you should use `.model_copy()` instead if you can use Pydantic v2. + +Like `stored_item_model.model_copy(update=update_data)`: + +=== "Python 3.10+" + + ```Python hl_lines="33" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" ```Python hl_lines="35" {!> ../../../docs_src/body_updates/tutorial002_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="33" - {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002.py!} ``` ### Partial updates recap @@ -120,26 +130,26 @@ In summary, to apply partial updates you would: * This way you can update only the values actually set by the user, instead of overriding values already stored with default values in your model. * Create a copy of the stored model, updating it's attributes with the received partial updates (using the `update` parameter). * Convert the copied model to something that can be stored in your DB (for example, using the `jsonable_encoder`). - * This is comparable to using the model's `.dict()` method again, but it makes sure (and converts) the values to data types that can be converted to JSON, for example, `datetime` to `str`. + * This is comparable to using the model's `.model_dump()` method again, but it makes sure (and converts) the values to data types that can be converted to JSON, for example, `datetime` to `str`. * Save the data to your DB. * Return the updated model. -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="30-37" - {!> ../../../docs_src/body_updates/tutorial002.py!} + ```Python hl_lines="28-35" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="30-37" {!> ../../../docs_src/body_updates/tutorial002_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="28-35" - {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ```Python hl_lines="30-37" + {!> ../../../docs_src/body_updates/tutorial002.py!} ``` !!! tip diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index 5090059360..67ba48f1e6 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -19,36 +19,36 @@ To declare a **request** body, you use ../../../docs_src/body/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="2" {!> ../../../docs_src/body/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + ## Create your data model Then you declare your data model as a class that inherits from `BaseModel`. Use standard Python types for all the attributes: -=== "Python 3.6 and above" - - ```Python hl_lines="7-11" - {!> ../../../docs_src/body/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="5-9" {!> ../../../docs_src/body/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="7-11" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + The same as when declaring query parameters, when a model attribute has a default value, it is not required. Otherwise, it is required. Use `None` to make it just optional. For example, this model above declares a JSON "`object`" (or Python `dict`) like: @@ -75,18 +75,18 @@ For example, this model above declares a JSON "`object`" (or Python `dict`) like To add it to your *path operation*, declare it the same way you declared path and query parameters: -=== "Python 3.6 and above" - - ```Python hl_lines="18" - {!> ../../../docs_src/body/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="16" {!> ../../../docs_src/body/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + ...and declare its type as the model you created, `Item`. ## Results @@ -149,54 +149,54 @@ But you would get the same editor support with ../../../docs_src/body/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="19" {!> ../../../docs_src/body/tutorial002_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="21" + {!> ../../../docs_src/body/tutorial002.py!} + ``` + ## Request body + path parameters You can declare path parameters and request body at the same time. **FastAPI** will recognize that the function parameters that match path parameters should be **taken from the path**, and that function parameters that are declared to be Pydantic models should be **taken from the request body**. -=== "Python 3.6 and above" - - ```Python hl_lines="17-18" - {!> ../../../docs_src/body/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="15-16" {!> ../../../docs_src/body/tutorial003_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} + ``` + ## Request body + path + query parameters You can also declare **body**, **path** and **query** parameters, all at the same time. **FastAPI** will recognize each of them and take the data from the correct place. -=== "Python 3.6 and above" - - ```Python hl_lines="18" - {!> ../../../docs_src/body/tutorial004.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="16" {!> ../../../docs_src/body/tutorial004_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} + ``` + The function parameters will be recognized as follows: * If the parameter is also declared in the **path**, it will be used as a path parameter. diff --git a/docs/en/docs/tutorial/cookie-params.md b/docs/en/docs/tutorial/cookie-params.md index 221cdfffba..3436a7df39 100644 --- a/docs/en/docs/tutorial/cookie-params.md +++ b/docs/en/docs/tutorial/cookie-params.md @@ -6,36 +6,84 @@ You can define Cookie parameters the same way you define `Query` and `Path` para First import `Cookie`: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="3" - {!> ../../../docs_src/cookie_params/tutorial001.py!} + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1" {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + ## Declare `Cookie` parameters Then declare the cookie parameters using the same structure as with `Path` and `Query`. The first value is the default value, you can pass all the extra validation or annotation parameters: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="9" - {!> ../../../docs_src/cookie_params/tutorial001.py!} + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + !!! note "Technical Details" `Cookie` is a "sister" class of `Path` and `Query`. It also inherits from the same common `Param` class. diff --git a/docs/en/docs/tutorial/debugging.md b/docs/en/docs/tutorial/debugging.md index bda889c45c..3deba54d56 100644 --- a/docs/en/docs/tutorial/debugging.md +++ b/docs/en/docs/tutorial/debugging.md @@ -64,7 +64,7 @@ from myapp import app # Some more code ``` -in that case, the automatic variable inside of `myapp.py` will not have the variable `__name__` with a value of `"__main__"`. +in that case, the automatically created variable inside of `myapp.py` will not have the variable `__name__` with a value of `"__main__"`. So, the line: diff --git a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index fb41ba1f67..842f2adf6b 100644 --- a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -6,18 +6,42 @@ Before diving deeper into the **Dependency Injection** system, let's upgrade the In the previous example, we were returning a `dict` from our dependency ("dependable"): -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="9" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + But then we get a `dict` in the parameter `commons` of the *path operation function*. And we know that editors can't provide a lot of support (like completion) for `dict`s, because they can't know their keys and value types. @@ -79,46 +103,118 @@ That also applies to callables with no parameters at all. The same as it would b Then, we can change the dependency "dependable" `common_parameters` from above to the class `CommonQueryParams`: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="11-15" - {!> ../../../docs_src/dependencies/tutorial002.py!} + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12-16" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9-13" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` -Pay attention to the `__init__` method used to create the instance of the class: +=== "Python 3.8+ non-Annotated" -=== "Python 3.6 and above" + !!! tip + Prefer to use the `Annotated` version if possible. - ```Python hl_lines="12" + ```Python hl_lines="11-15" {!> ../../../docs_src/dependencies/tutorial002.py!} ``` -=== "Python 3.10 and above" +Pay attention to the `__init__` method used to create the instance of the class: + +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="10" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` -...it has the same parameters as our previous `common_parameters`: +=== "Python 3.8+ non-Annotated" -=== "Python 3.6 and above" + !!! tip + Prefer to use the `Annotated` version if possible. - ```Python hl_lines="9" - {!> ../../../docs_src/dependencies/tutorial001.py!} + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} ``` -=== "Python 3.10 and above" +...it has the same parameters as our previous `common_parameters`: + +=== "Python 3.10+" + + ```Python hl_lines="8" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="6" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + Those parameters are what **FastAPI** will use to "solve" the dependency. In both cases, it will have: @@ -133,32 +229,67 @@ In both cases the data will be converted, validated, documented on the OpenAPI s Now you can declare your dependency using this class. -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial002.py!} + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + **FastAPI** calls the `CommonQueryParams` class. This creates an "instance" of that class and the instance will be passed as the parameter `commons` to your function. ## Type annotation vs `Depends` Notice how we write `CommonQueryParams` twice in the above code: -```Python -commons: CommonQueryParams = Depends(CommonQueryParams) -``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` + +=== "Python 3.8+" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` The last `CommonQueryParams`, in: ```Python -... = Depends(CommonQueryParams) +... Depends(CommonQueryParams) ``` ...is what **FastAPI** will actually use to know what is the dependency. @@ -169,32 +300,78 @@ From it is that FastAPI will extract the declared parameters and that is what Fa In this case, the first `CommonQueryParams`, in: -```Python -commons: CommonQueryParams ... -``` +=== "Python 3.8+" -...doesn't have any special meaning for **FastAPI**. FastAPI won't use it for data conversion, validation, etc. (as it is using the `= Depends(CommonQueryParams)` for that). + ```Python + commons: Annotated[CommonQueryParams, ... + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + commons: CommonQueryParams ... + ``` + +...doesn't have any special meaning for **FastAPI**. FastAPI won't use it for data conversion, validation, etc. (as it is using the `Depends(CommonQueryParams)` for that). You could actually write just: -```Python -commons = Depends(CommonQueryParams) -``` +=== "Python 3.8+" + + ```Python + commons: Annotated[Any, Depends(CommonQueryParams)] + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + commons = Depends(CommonQueryParams) + ``` ..as in: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial003.py!} + {!> ../../../docs_src/dependencies/tutorial003_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial003_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + But declaring the type is encouraged as that way your editor will know what will be passed as the parameter `commons`, and then it can help you with code completion, type checks, etc: @@ -203,9 +380,20 @@ But declaring the type is encouraged as that way your editor will know what will But you see that we are having some code repetition here, writing `CommonQueryParams` twice: -```Python -commons: CommonQueryParams = Depends(CommonQueryParams) -``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` + +=== "Python 3.8+" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` **FastAPI** provides a shortcut for these cases, in where the dependency is *specifically* a class that **FastAPI** will "call" to create an instance of the class itself. @@ -213,32 +401,78 @@ For those specific cases, you can do the following: Instead of writing: -```Python -commons: CommonQueryParams = Depends(CommonQueryParams) -``` +=== "Python 3.8+" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` ...you write: -```Python -commons: CommonQueryParams = Depends() -``` +=== "Python 3.8+" -You declare the dependency as the type of the parameter, and you use `Depends()` as its "default" value (that after the `=`) for that function's parameter, without any parameter in `Depends()`, instead of having to write the full class *again* inside of `Depends(CommonQueryParams)`. + ```Python + commons: Annotated[CommonQueryParams, Depends()] + ``` + +=== "Python 3.8 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + commons: CommonQueryParams = Depends() + ``` + +You declare the dependency as the type of the parameter, and you use `Depends()` without any parameter, instead of having to write the full class *again* inside of `Depends(CommonQueryParams)`. The same example would then look like: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial004.py!} + {!> ../../../docs_src/dependencies/tutorial004_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial004_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + ...and **FastAPI** will know what to do. !!! tip diff --git a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index a1bbcc6c70..eaab51d1b1 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -14,11 +14,28 @@ The *path operation decorator* receives an optional argument `dependencies`. It should be a `list` of `Depends()`: -```Python hl_lines="17" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.9+" -These dependencies will be executed/solved the same way normal dependencies. But their value (if they return any) won't be passed to your *path operation function*. + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.8 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial006.py!} + ``` + +These dependencies will be executed/solved the same way as normal dependencies. But their value (if they return any) won't be passed to your *path operation function*. !!! tip Some editors check for unused function parameters, and show them as errors. @@ -40,17 +57,51 @@ You can use the same dependency *functions* you use normally. They can declare request requirements (like headers) or other sub-dependencies: -```Python hl_lines="6 11" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="8 13" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="7 12" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.8 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="6 11" + {!> ../../../docs_src/dependencies/tutorial006.py!} + ``` ### Raise exceptions These dependencies can `raise` exceptions, the same as normal dependencies: -```Python hl_lines="8 13" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9 14" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.8 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="8 13" + {!> ../../../docs_src/dependencies/tutorial006.py!} + ``` ### Return values @@ -58,9 +109,26 @@ And they can return values or not, the values won't be used. So, you can re-use a normal dependency (that returns a value) you already use somewhere else, and even though the value won't be used, the dependency will be executed: -```Python hl_lines="9 14" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="11 16" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.8 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9 14" + {!> ../../../docs_src/dependencies/tutorial006.py!} + ``` ## Dependencies for a group of *path operations* diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index a7300f0b7a..de87ba3156 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -1,8 +1,8 @@ # Dependencies with yield -FastAPI supports dependencies that do some extra steps after finishing. +FastAPI supports dependencies that do some extra steps after finishing. -To do this, use `yield` instead of `return`, and write the extra steps after. +To do this, use `yield` instead of `return`, and write the extra steps (code) after. !!! tip Make sure to use `yield` one single time. @@ -21,7 +21,7 @@ To do this, use `yield` instead of `return`, and write the extra steps after. For example, you could use this to create a database session and close it after finishing. -Only the code prior to and including the `yield` statement is executed before sending a response: +Only the code prior to and including the `yield` statement is executed before creating a response: ```Python hl_lines="2-4" {!../../../docs_src/dependencies/tutorial007.py!} @@ -40,7 +40,7 @@ The code following the `yield` statement is executed after the response has been ``` !!! tip - You can use `async` or normal functions. + You can use `async` or regular functions. **FastAPI** will do the right thing with each, the same as with normal dependencies. @@ -66,9 +66,26 @@ You can have sub-dependencies and "trees" of sub-dependencies of any size and sh For example, `dependency_c` can have a dependency on `dependency_b`, and `dependency_b` on `dependency_a`: -```Python hl_lines="4 12 20" -{!../../../docs_src/dependencies/tutorial008.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="6 14 22" + {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="5 13 21" + {!> ../../../docs_src/dependencies/tutorial008_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4 12 20" + {!> ../../../docs_src/dependencies/tutorial008.py!} + ``` And all of them can use `yield`. @@ -76,11 +93,28 @@ In this case `dependency_c`, to execute its exit code, needs the value from `dep And, in turn, `dependency_b` needs the value from `dependency_a` (here named `dep_a`) to be available for its exit code. -```Python hl_lines="16-17 24-25" -{!../../../docs_src/dependencies/tutorial008.py!} -``` +=== "Python 3.9+" -The same way, you could have dependencies with `yield` and `return` mixed. + ```Python hl_lines="18-19 26-27" + {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17-18 25-26" + {!> ../../../docs_src/dependencies/tutorial008_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="16-17 24-25" + {!> ../../../docs_src/dependencies/tutorial008.py!} + ``` + +The same way, you could have some dependencies with `yield` and some other dependencies with `return`, and have some of those depend on some of the others. And you could have a single dependency that requires several other dependencies with `yield`, etc. @@ -97,24 +131,38 @@ You can have any combinations of dependencies that you want. You saw that you can use dependencies with `yield` and have `try` blocks that catch exceptions. -It might be tempting to raise an `HTTPException` or similar in the exit code, after the `yield`. But **it won't work**. - -The exit code in dependencies with `yield` is executed *after* the response is sent, so [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} will have already run. There's nothing catching exceptions thrown by your dependencies in the exit code (after the `yield`). - -So, if you raise an `HTTPException` after the `yield`, the default (or any custom) exception handler that catches `HTTPException`s and returns an HTTP 400 response won't be there to catch that exception anymore. - -This is what allows anything set in the dependency (e.g. a DB session) to, for example, be used by background tasks. - -Background tasks are run *after* the response has been sent. So there's no way to raise an `HTTPException` because there's not even a way to change the response that is *already sent*. - -But if a background task creates a DB error, at least you can rollback or cleanly close the session in the dependency with `yield`, and maybe log the error or report it to a remote tracking system. - -If you have some code that you know could raise an exception, do the most normal/"Pythonic" thing and add a `try` block in that section of the code. - -If you have custom exceptions that you would like to handle *before* returning the response and possibly modifying the response, maybe even raising an `HTTPException`, create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. +The same way, you could raise an `HTTPException` or similar in the exit code, after the `yield`. !!! tip - You can still raise exceptions including `HTTPException` *before* the `yield`. But not after. + + This is a somewhat advanced technique, and in most of the cases you won't really need it, as you can raise exceptions (including `HTTPException`) from inside of the rest of your application code, for example, in the *path operation function*. + + But it's there for you if you need it. 🤓 + +=== "Python 3.9+" + + ```Python hl_lines="18-22 31" + {!> ../../../docs_src/dependencies/tutorial008b_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17-21 30" + {!> ../../../docs_src/dependencies/tutorial008b_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="16-20 29" + {!> ../../../docs_src/dependencies/tutorial008b.py!} + ``` + +An alternative you could use to catch exceptions (and possibly also raise another `HTTPException`) is to create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. + +## Execution of dependencies with `yield` The sequence of execution is more or less like this diagram. Time flows from top to bottom. And each column is one of the parts interacting or executing code. @@ -127,34 +175,30 @@ participant dep as Dep with yield participant operation as Path Operation participant tasks as Background tasks - Note over client,tasks: Can raise exception for dependency, handled after response is sent - Note over client,operation: Can raise HTTPException and can change the response + Note over client,operation: Can raise exceptions, including HTTPException client ->> dep: Start request Note over dep: Run code up to yield - opt raise - dep -->> handler: Raise HTTPException + opt raise Exception + dep -->> handler: Raise Exception handler -->> client: HTTP error response - dep -->> dep: Raise other exception end dep ->> operation: Run dependency, e.g. DB session opt raise - operation -->> dep: Raise HTTPException - dep -->> handler: Auto forward exception + operation -->> dep: Raise Exception (e.g. HTTPException) + opt handle + dep -->> dep: Can catch exception, raise a new HTTPException, raise other exception + dep -->> handler: Auto forward exception + end handler -->> client: HTTP error response - operation -->> dep: Raise other exception - dep -->> handler: Auto forward exception end + operation ->> client: Return response to client Note over client,operation: Response is already sent, can't change it anymore opt Tasks operation -->> tasks: Send background tasks end opt Raise other exception - tasks -->> dep: Raise other exception - end - Note over dep: After yield - opt Handle other exception - dep -->> dep: Handle exception, can't change response. E.g. close DB session. + tasks -->> tasks: Handle exceptions in the background task code end ``` @@ -164,10 +208,33 @@ participant tasks as Background tasks After one of those responses is sent, no other response can be sent. !!! tip - This diagram shows `HTTPException`, but you could also raise any other exception for which you create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. + This diagram shows `HTTPException`, but you could also raise any other exception that you catch in a dependency with `yield` or with a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. If you raise any exception, it will be passed to the dependencies with yield, including `HTTPException`, and then **again** to the exception handlers. If there's no exception handler for that exception, it will then be handled by the default internal `ServerErrorMiddleware`, returning a 500 HTTP status code, to let the client know that there was an error in the server. +## Dependencies with `yield`, `HTTPException` and Background Tasks + +!!! warning + You most probably don't need these technical details, you can skip this section and continue below. + + These details are useful mainly if you were using a version of FastAPI prior to 0.106.0 and used resources from dependencies with `yield` in background tasks. + +Before FastAPI 0.106.0, raising exceptions after `yield` was not possible, the exit code in dependencies with `yield` was executed *after* the response was sent, so [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} would have already run. + +This was designed this way mainly to allow using the same objects "yielded" by dependencies inside of background tasks, because the exit code would be executed after the background tasks were finished. + +Nevertheless, as this would mean waiting for the response to travel through the network while unnecessarily holding a resource in a dependency with yield (for example a database connection), this was changed in FastAPI 0.106.0. + +!!! tip + + Additionally, a background task is normally an independent set of logic that should be handled separately, with its own resources (e.g. its own database connection). + + So, this way you will probably have cleaner code. + +If you used to rely on this behavior, now you should create the resources for background tasks inside the background task itself, and use internally only data that doesn't depend on the resources of dependencies with `yield`. + +For example, instead of using the same database session, you would create a new database session inside of the background task, and you would obtain the objects from the database using this new session. And then instead of passing the object from the database as a parameter to the background task function, you would pass the ID of that object and then obtain the object again inside the background task function. + ## Context Managers ### What are "Context Managers" @@ -182,11 +249,11 @@ with open("./somefile.txt") as f: print(contents) ``` -Underneath, the `open("./somefile.txt")` creates an object that is a called a "Context Manager". +Underneath, the `open("./somefile.txt")` creates an object that is called a "Context Manager". When the `with` block finishes, it makes sure to close the file, even if there were exceptions. -When you create a dependency with `yield`, **FastAPI** will internally convert it to a context manager, and combine it with some other related tools. +When you create a dependency with `yield`, **FastAPI** will internally create a context manager for it, and combine it with some other related tools. ### Using context managers in dependencies with `yield` diff --git a/docs/en/docs/tutorial/dependencies/global-dependencies.md b/docs/en/docs/tutorial/dependencies/global-dependencies.md index bcd61d52b0..0dcf73176f 100644 --- a/docs/en/docs/tutorial/dependencies/global-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/global-dependencies.md @@ -6,9 +6,26 @@ Similar to the way you can [add `dependencies` to the *path operation decorators In that case, they will be applied to all the *path operations* in the application: -```Python hl_lines="15" -{!../../../docs_src/dependencies/tutorial012.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial012_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial012_an.py!} + ``` + +=== "Python 3.8 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="15" + {!> ../../../docs_src/dependencies/tutorial012.py!} + ``` And all the ideas in the section about [adding `dependencies` to the *path operation decorators*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} still apply, but in this case, to all of the *path operations* in the app. diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index 5078c00968..608ced407e 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -1,4 +1,4 @@ -# Dependencies - First Steps +# Dependencies **FastAPI** has a very powerful but intuitive **Dependency Injection** system. @@ -31,18 +31,42 @@ Let's first focus on the dependency. It is just a function that can take all the same parameters that a *path operation function* can take: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="8-11" - {!> ../../../docs_src/dependencies/tutorial001.py!} + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="8-11" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9-12" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="6-7" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="8-11" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + That's it. **2 lines**. @@ -61,42 +85,99 @@ In this case, this dependency expects: And then it just returns a `dict` containing those values. +!!! info + FastAPI added support for `Annotated` (and started recommending it) in version 0.95.0. + + If you have an older version, you would get errors when trying to use `Annotated`. + + Make sure you [Upgrade the FastAPI version](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} to at least 0.95.1 before using `Annotated`. + ### Import `Depends` -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="3" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + ### Declare the dependency, in the "dependant" The same way you use `Body`, `Query`, etc. with your *path operation function* parameters, use `Depends` with a new parameter: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="15 20" - {!> ../../../docs_src/dependencies/tutorial001.py!} + ```Python hl_lines="13 18" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="15 20" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="16 21" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="11 16" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="15 20" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + Although you use `Depends` in the parameters of your function the same way you use `Body`, `Query`, etc, `Depends` works a bit differently. You only give `Depends` a single parameter. This parameter must be something like a function. +You **don't call it** directly (don't add the parenthesis at the end), you just pass it as a parameter to `Depends()`. + And that function takes parameters in the same way that *path operation functions* do. !!! tip @@ -126,6 +207,45 @@ This way you write shared code once and **FastAPI** takes care of calling it for You just pass it to `Depends` and **FastAPI** knows how to do the rest. +## Share `Annotated` dependencies + +In the examples above, you see that there's a tiny bit of **code duplication**. + +When you need to use the `common_parameters()` dependency, you have to write the whole parameter with the type annotation and `Depends()`: + +```Python +commons: Annotated[dict, Depends(common_parameters)] +``` + +But because we are using `Annotated`, we can store that `Annotated` value in a variable and use it in multiple places: + +=== "Python 3.10+" + + ```Python hl_lines="12 16 21" + {!> ../../../docs_src/dependencies/tutorial001_02_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14 18 23" + {!> ../../../docs_src/dependencies/tutorial001_02_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="15 19 24" + {!> ../../../docs_src/dependencies/tutorial001_02_an.py!} + ``` + +!!! tip + This is just standard Python, it's called a "type alias", it's actually not specific to **FastAPI**. + + But because **FastAPI** is based on the Python standards, including `Annotated`, you can use this trick in your code. 😎 + +The dependencies will keep working as expected, and the **best part** is that the **type information will be preserved**, which means that your editor will be able to keep providing you with **autocompletion**, **inline errors**, etc. The same for other tools like `mypy`. + +This will be especially useful when you use it in a **large code base** where you use **the same dependencies** over and over again in **many *path operations***. + ## To `async` or not to `async` As dependencies will also be called by **FastAPI** (the same as your *path operation functions*), the same rules apply while defining your functions. @@ -167,9 +287,9 @@ Other common terms for this same idea of "dependency injection" are: ## **FastAPI** plug-ins -Integrations and "plug-in"s can be built using the **Dependency Injection** system. But in fact, there is actually **no need to create "plug-ins"**, as by using dependencies it's possible to declare an infinite number of integrations and interactions that become available to your *path operation functions*. +Integrations and "plug-ins" can be built using the **Dependency Injection** system. But in fact, there is actually **no need to create "plug-ins"**, as by using dependencies it's possible to declare an infinite number of integrations and interactions that become available to your *path operation functions*. -And dependencies can be created in a very simple and intuitive way that allow you to just import the Python packages you need, and integrate them with your API functions in a couple of lines of code, *literally*. +And dependencies can be created in a very simple and intuitive way that allows you to just import the Python packages you need, and integrate them with your API functions in a couple of lines of code, *literally*. You will see examples of this in the next chapters, about relational and NoSQL databases, security, etc. diff --git a/docs/en/docs/tutorial/dependencies/sub-dependencies.md b/docs/en/docs/tutorial/dependencies/sub-dependencies.md index a5b40c9ad7..1cb469a805 100644 --- a/docs/en/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/sub-dependencies.md @@ -10,18 +10,42 @@ They can be as **deep** as you need them to be. You could create a first dependency ("dependable") like: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="8-9" - {!> ../../../docs_src/dependencies/tutorial005.py!} + {!> ../../../docs_src/dependencies/tutorial005_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9-10" + {!> ../../../docs_src/dependencies/tutorial005_an.py!} + ``` + +=== "Python 3.10 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="6-7" {!> ../../../docs_src/dependencies/tutorial005_py310.py!} ``` +=== "Python 3.8 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + It declares an optional query parameter `q` as a `str`, and then it just returns it. This is quite simple (not very useful), but will help us focus on how the sub-dependencies work. @@ -30,18 +54,42 @@ This is quite simple (not very useful), but will help us focus on how the sub-de Then you can create another dependency function (a "dependable") that at the same time declares a dependency of its own (so it is a "dependant" too): -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="13" - {!> ../../../docs_src/dependencies/tutorial005.py!} + {!> ../../../docs_src/dependencies/tutorial005_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="14" + {!> ../../../docs_src/dependencies/tutorial005_an.py!} + ``` + +=== "Python 3.10 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="11" {!> ../../../docs_src/dependencies/tutorial005_py310.py!} ``` +=== "Python 3.8 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + Let's focus on the parameters declared: * Even though this function is a dependency ("dependable") itself, it also declares another dependency (it "depends" on something else). @@ -53,18 +101,42 @@ Let's focus on the parameters declared: Then we can use the dependency with: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="22" - {!> ../../../docs_src/dependencies/tutorial005.py!} + ```Python hl_lines="23" + {!> ../../../docs_src/dependencies/tutorial005_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="23" + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="24" + {!> ../../../docs_src/dependencies/tutorial005_an.py!} + ``` + +=== "Python 3.10 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial005_py310.py!} ``` +=== "Python 3.8 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="22" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + !!! info Notice that we are only declaring one dependency in the *path operation function*, the `query_or_cookie_extractor`. @@ -89,10 +161,22 @@ And it will save the returned value in a ../../../docs_src/encoder/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="4 21" {!> ../../../docs_src/encoder/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + In this example, it would convert the Pydantic model to a `dict`, and the `datetime` to a `str`. The result of calling it is something that can be encoded with the Python standard `json.dumps()`. diff --git a/docs/en/docs/tutorial/extra-data-types.md b/docs/en/docs/tutorial/extra-data-types.md index fb3efd3184..fd7a99af32 100644 --- a/docs/en/docs/tutorial/extra-data-types.md +++ b/docs/en/docs/tutorial/extra-data-types.md @@ -49,34 +49,82 @@ Here are some of the additional data types you can use: * `Decimal`: * Standard Python `Decimal`. * In requests and responses, handled the same as a `float`. -* You can check all the valid pydantic data types here: Pydantic data types. +* You can check all the valid pydantic data types here: Pydantic data types. ## Example Here's an example *path operation* with parameters using some of the above types. -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="1 3 12-16" - {!> ../../../docs_src/extra_data_types/tutorial001.py!} + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 3 13-17" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1 2 11-15" {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` -Note that the parameters inside the function have their natural data type, and you can, for example, perform normal date manipulations, like: +=== "Python 3.8+ non-Annotated" -=== "Python 3.6 and above" + !!! tip + Prefer to use the `Annotated` version if possible. - ```Python hl_lines="18-19" + ```Python hl_lines="1 2 12-16" {!> ../../../docs_src/extra_data_types/tutorial001.py!} ``` -=== "Python 3.10 and above" +Note that the parameters inside the function have their natural data type, and you can, for example, perform normal date manipulations, like: + +=== "Python 3.10+" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="19-20" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17-18" {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` diff --git a/docs/en/docs/tutorial/extra-models.md b/docs/en/docs/tutorial/extra-models.md index 72fc74894c..d83b6bc859 100644 --- a/docs/en/docs/tutorial/extra-models.md +++ b/docs/en/docs/tutorial/extra-models.md @@ -17,17 +17,22 @@ This is especially the case for user models, because: Here's a general idea of how the models could look like with their password fields and the places where they are used: -=== "Python 3.6 and above" +=== "Python 3.10+" + + ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" + {!> ../../../docs_src/extra_models/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" {!> ../../../docs_src/extra_models/tutorial001.py!} ``` -=== "Python 3.10 and above" +!!! info + In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. - ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" - {!> ../../../docs_src/extra_models/tutorial001_py310.py!} - ``` + The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. ### About `**user_in.dict()` @@ -158,18 +163,18 @@ All the data conversion, validation, documentation, etc. will still work as norm That way, we can declare just the differences between the models (with plaintext `password`, with `hashed_password` and without password): -=== "Python 3.6 and above" - - ```Python hl_lines="9 15-16 19-20 23-24" - {!> ../../../docs_src/extra_models/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7 13-14 17-18 21-22" {!> ../../../docs_src/extra_models/tutorial002_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} + ``` + ## `Union` or `anyOf` You can declare a response to be the `Union` of two types, that means, that the response would be any of the two. @@ -181,18 +186,18 @@ To do that, use the standard Python type hint `Union`, include the most specific type first, followed by the less specific type. In the example below, the more specific `PlaneItem` comes before `CarItem` in `Union[PlaneItem, CarItem]`. -=== "Python 3.6 and above" - - ```Python hl_lines="1 14-15 18-20 33" - {!> ../../../docs_src/extra_models/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="1 14-15 18-20 33" {!> ../../../docs_src/extra_models/tutorial003_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003.py!} + ``` + ### `Union` in Python 3.10 In this example we pass `Union[PlaneItem, CarItem]` as the value of the argument `response_model`. @@ -213,18 +218,18 @@ The same way, you can declare responses of lists of objects. For that, use the standard Python `typing.List` (or just `list` in Python 3.9 and above): -=== "Python 3.6 and above" - - ```Python hl_lines="1 20" - {!> ../../../docs_src/extra_models/tutorial004.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="18" {!> ../../../docs_src/extra_models/tutorial004_py39.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} + ``` + ## Response with arbitrary `dict` You can also declare a response using a plain arbitrary `dict`, declaring just the type of the keys and values, without using a Pydantic model. @@ -233,18 +238,18 @@ This is useful if you don't know the valid field/attribute names (that would be In this case, you can use `typing.Dict` (or just `dict` in Python 3.9 and above): -=== "Python 3.6 and above" - - ```Python hl_lines="1 8" - {!> ../../../docs_src/extra_models/tutorial005.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="6" {!> ../../../docs_src/extra_models/tutorial005_py39.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} + ``` + ## Recap Use multiple Pydantic models and inherit freely for each case. diff --git a/docs/en/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md index 6ca5f39eb1..cfa1593294 100644 --- a/docs/en/docs/tutorial/first-steps.md +++ b/docs/en/docs/tutorial/first-steps.md @@ -99,7 +99,7 @@ It will show a JSON starting with something like: ```JSON { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" diff --git a/docs/en/docs/tutorial/handling-errors.md b/docs/en/docs/tutorial/handling-errors.md index 8c30326ced..7d521696d8 100644 --- a/docs/en/docs/tutorial/handling-errors.md +++ b/docs/en/docs/tutorial/handling-errors.md @@ -1,6 +1,6 @@ # Handling Errors -There are many situations in where you need to notify an error to a client that is using your API. +There are many situations in which you need to notify an error to a client that is using your API. This client could be a browser with a frontend, a code from someone else, an IoT device, etc. @@ -234,9 +234,7 @@ You will receive a response telling you that the data is invalid containing the And **FastAPI**'s `HTTPException` error class inherits from Starlette's `HTTPException` error class. -The only difference, is that **FastAPI**'s `HTTPException` allows you to add headers to be included in the response. - -This is needed/used internally for OAuth 2.0 and some security utilities. +The only difference is that **FastAPI**'s `HTTPException` accepts any JSON-able data for the `detail` field, while Starlette's `HTTPException` only accepts strings for it. So, you can keep raising **FastAPI**'s `HTTPException` as normally in your code. diff --git a/docs/en/docs/tutorial/header-params.md b/docs/en/docs/tutorial/header-params.md index 8e294416c6..bbba909987 100644 --- a/docs/en/docs/tutorial/header-params.md +++ b/docs/en/docs/tutorial/header-params.md @@ -6,36 +6,84 @@ You can define Header parameters the same way you define `Query`, `Path` and `Co First import `Header`: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="3" - {!> ../../../docs_src/header_params/tutorial001.py!} + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1" {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + ## Declare `Header` parameters Then declare the header parameters using the same structure as with `Path`, `Query` and `Cookie`. The first value is the default value, you can pass all the extra validation or annotation parameters: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial001.py!} + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + !!! note "Technical Details" `Header` is a "sister" class of `Path`, `Query` and `Cookie`. It also inherits from the same common `Param` class. @@ -60,18 +108,42 @@ So, you can use `user_agent` as you normally would in Python code, instead of ne If for some reason you need to disable automatic conversion of underscores to hyphens, set the parameter `convert_underscores` of `Header` to `False`: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="10" - {!> ../../../docs_src/header_params/tutorial002.py!} + {!> ../../../docs_src/header_params/tutorial002_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="11" + {!> ../../../docs_src/header_params/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12" + {!> ../../../docs_src/header_params/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8" {!> ../../../docs_src/header_params/tutorial002_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} + ``` + !!! warning Before setting `convert_underscores` to `False`, bear in mind that some HTTP proxies and servers disallow the usage of headers with underscores. @@ -85,22 +157,49 @@ You will receive all the values from the duplicate header as a Python `list`. For example, to declare a header of `X-Token` that can appear more than once, you can write: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial003.py!} + {!> ../../../docs_src/header_params/tutorial003_an_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial003_py310.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial003_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+ non-Annotated" - ```Python hl_lines="7" - {!> ../../../docs_src/header_params/tutorial003_py310.py!} + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} ``` If you communicate with that *path operation* sending two HTTP headers like: diff --git a/docs/en/docs/tutorial/index.md b/docs/en/docs/tutorial/index.md index 8b4a9df9be..75665324d9 100644 --- a/docs/en/docs/tutorial/index.md +++ b/docs/en/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Tutorial - User Guide - Intro +# Tutorial - User Guide This tutorial shows you how to use **FastAPI** with most of its features, step by step. diff --git a/docs/en/docs/tutorial/metadata.md b/docs/en/docs/tutorial/metadata.md index 78f17031a1..504204e987 100644 --- a/docs/en/docs/tutorial/metadata.md +++ b/docs/en/docs/tutorial/metadata.md @@ -9,15 +9,16 @@ You can set the following fields that are used in the OpenAPI specification and | Parameter | Type | Description | |------------|------|-------------| | `title` | `str` | The title of the API. | +| `summary` | `str` | A short summary of the API. Available since OpenAPI 3.1.0, FastAPI 0.99.0. | | `description` | `str` | A short description of the API. It can use Markdown. | | `version` | `string` | The version of the API. This is the version of your own application, not of OpenAPI. For example `2.5.0`. | | `terms_of_service` | `str` | A URL to the Terms of Service for the API. If provided, this has to be a URL. | | `contact` | `dict` | The contact information for the exposed API. It can contain several fields.
contact fields
ParameterTypeDescription
namestrThe identifying name of the contact person/organization.
urlstrThe URL pointing to the contact information. MUST be in the format of a URL.
emailstrThe email address of the contact person/organization. MUST be in the format of an email address.
| -| `license_info` | `dict` | The license information for the exposed API. It can contain several fields.
license_info fields
ParameterTypeDescription
namestrREQUIRED (if a license_info is set). The license name used for the API.
urlstrA URL to the license used for the API. MUST be in the format of a URL.
| +| `license_info` | `dict` | The license information for the exposed API. It can contain several fields.
license_info fields
ParameterTypeDescription
namestrREQUIRED (if a license_info is set). The license name used for the API.
identifierstrAn SPDX license expression for the API. The identifier field is mutually exclusive of the url field. Available since OpenAPI 3.1.0, FastAPI 0.99.0.
urlstrA URL to the license used for the API. MUST be in the format of a URL.
| You can set them as follows: -```Python hl_lines="3-16 19-31" +```Python hl_lines="3-16 19-32" {!../../../docs_src/metadata/tutorial001.py!} ``` @@ -28,6 +29,16 @@ With this configuration, the automatic API docs would look like: +## License identifier + +Since OpenAPI 3.1.0 and FastAPI 0.99.0, you can also set the `license_info` with an `identifier` instead of a `url`. + +For example: + +```Python hl_lines="31" +{!../../../docs_src/metadata/tutorial001_1.py!} +``` + ## Metadata for tags You can also add additional metadata for the different tags used to group your path operations with the parameter `openapi_tags`. @@ -101,7 +112,7 @@ You can configure the two documentation user interfaces included: * **Swagger UI**: served at `/docs`. * You can set its URL with the parameter `docs_url`. * You can disable it by setting `docs_url=None`. -* ReDoc: served at `/redoc`. +* **ReDoc**: served at `/redoc`. * You can set its URL with the parameter `redoc_url`. * You can disable it by setting `redoc_url=None`. diff --git a/docs/en/docs/tutorial/middleware.md b/docs/en/docs/tutorial/middleware.md index 3c6868fe4d..492a1b065e 100644 --- a/docs/en/docs/tutorial/middleware.md +++ b/docs/en/docs/tutorial/middleware.md @@ -33,7 +33,7 @@ The middleware function receives: ``` !!! tip - Have in mind that custom proprietary headers can be added using the 'X-' prefix. + Keep in mind that custom proprietary headers can be added using the 'X-' prefix. But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) using the parameter `expose_headers` documented in Starlette's CORS docs. diff --git a/docs/en/docs/tutorial/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md index 884a762e24..babf85acb2 100644 --- a/docs/en/docs/tutorial/path-operation-configuration.md +++ b/docs/en/docs/tutorial/path-operation-configuration.md @@ -13,22 +13,22 @@ You can pass directly the `int` code, like `404`. But if you don't remember what each number code is for, you can use the shortcut constants in `status`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="3 17" - {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} + ```Python hl_lines="1 15" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="3 17" {!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="1 15" - {!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} ``` That status code will be used in the response and will be added to the OpenAPI schema. @@ -42,22 +42,22 @@ That status code will be used in the response and will be added to the OpenAPI s You can add tags to your *path operation*, pass the parameter `tags` with a `list` of `str` (commonly just one `str`): -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="17 22 27" - {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} + ```Python hl_lines="15 20 25" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="17 22 27" {!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="15 20 25" - {!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} ``` They will be added to the OpenAPI schema and used by the automatic documentation interfaces: @@ -80,22 +80,22 @@ In these cases, it could make sense to store the tags in an `Enum`. You can add a `summary` and `description`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="20-21" - {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} + ```Python hl_lines="18-19" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="20-21" {!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="18-19" - {!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} ``` ## Description from docstring @@ -104,22 +104,22 @@ As descriptions tend to be long and cover multiple lines, you can declare the *p You can write Markdown in the docstring, it will be interpreted and displayed correctly (taking into account docstring indentation). -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="19-27" - {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} + ```Python hl_lines="17-25" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="19-27" {!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="17-25" - {!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} ``` It will be used in the interactive docs: @@ -130,22 +130,22 @@ It will be used in the interactive docs: You can specify the response description with the parameter `response_description`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="21" - {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} + ```Python hl_lines="19" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="21" {!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="19" - {!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} ``` !!! info diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index cec54b0fb1..b5b13cfbe6 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -4,19 +4,50 @@ In the same way that you can declare more validations and metadata for query par ## Import Path -First, import `Path` from `fastapi`: +First, import `Path` from `fastapi`, and import `Annotated`: -=== "Python 3.6 and above" +=== "Python 3.10+" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3-4" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="1" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="3" {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} ``` -=== "Python 3.10 and above" +!!! info + FastAPI added support for `Annotated` (and started recommending it) in version 0.95.0. - ```Python hl_lines="1" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} - ``` + If you have an older version, you would get errors when trying to use `Annotated`. + + Make sure you [Upgrade the FastAPI version](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} to at least 0.95.1 before using `Annotated`. ## Declare metadata @@ -24,18 +55,42 @@ You can declare all the same parameters as for `Query`. For example, to declare a `title` metadata value for the path parameter `item_id` you can type: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="10" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + !!! note A path parameter is always required as it has to be part of the path. @@ -45,11 +100,14 @@ For example, to declare a `title` metadata value for the path parameter `item_id ## Order the parameters as you need +!!! tip + This is probably not as important or necessary if you use `Annotated`. + Let's say that you want to declare the query parameter `q` as a required `str`. And you don't need to declare anything else for that parameter, so you don't really need to use `Query`. -But you still need to use `Path` for the `item_id` path parameter. +But you still need to use `Path` for the `item_id` path parameter. And you don't want to use `Annotated` for some reason. Python will complain if you put a value with a "default" before a value that doesn't have a "default". @@ -59,13 +117,44 @@ It doesn't matter for **FastAPI**. It will detect the parameters by their names, So, you can declare your function as: -```Python hl_lines="7" -{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} -``` +=== "Python 3.8 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002.py!} + ``` + +But keep in mind that if you use `Annotated`, you won't have this problem, it won't matter as you're not using the function parameter default values for `Query()` or `Path()`. + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an.py!} + ``` ## Order the parameters as you need, tricks -If you want to declare the `q` query parameter without a `Query` nor any default value, and the path parameter `item_id` using `Path`, and have them in a different order, Python has a little special syntax for that. +!!! tip + This is probably not as important or necessary if you use `Annotated`. + +Here's a **small trick** that can be handy, but you won't need it often. + +If you want to: + +* declare the `q` query parameter without a `Query` nor any default value +* declare the path parameter `item_id` using `Path` +* have them in a different order +* not use `Annotated` + +...Python has a little special syntax for that. Pass `*`, as the first parameter of the function. @@ -75,15 +164,48 @@ Python won't do anything with that `*`, but it will know that all the following {!../../../docs_src/path_params_numeric_validations/tutorial003.py!} ``` +### Better with `Annotated` + +Keep in mind that if you use `Annotated`, as you are not using function parameter default values, you won't have this problem, and you probably won't need to use `*`. + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an.py!} + ``` + ## Number validations: greater than or equal With `Query` and `Path` (and others you'll see later) you can declare number constraints. Here, with `ge=1`, `item_id` will need to be an integer number "`g`reater than or `e`qual" to `1`. -```Python hl_lines="8" -{!../../../docs_src/path_params_numeric_validations/tutorial004.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004.py!} + ``` ## Number validations: greater than and less than or equal @@ -92,9 +214,26 @@ The same applies for: * `gt`: `g`reater `t`han * `le`: `l`ess than or `e`qual -```Python hl_lines="9" -{!../../../docs_src/path_params_numeric_validations/tutorial005.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005.py!} + ``` ## Number validations: floats, greater than and less than @@ -106,9 +245,26 @@ So, `0.5` would be a valid value. But `0.0` or `0` would not. And the same for lt. -```Python hl_lines="11" -{!../../../docs_src/path_params_numeric_validations/tutorial006.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="13" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006.py!} + ``` ## Recap diff --git a/docs/en/docs/tutorial/path-params.md b/docs/en/docs/tutorial/path-params.md index a0d70692e5..847b563342 100644 --- a/docs/en/docs/tutorial/path-params.md +++ b/docs/en/docs/tutorial/path-params.md @@ -46,16 +46,18 @@ But if you go to the browser at OpenAPI standard, there are many compatible tools. +And because the generated schema is from the OpenAPI standard, there are many compatible tools. Because of this, **FastAPI** itself provides an alternative API documentation (using ReDoc), which you can access at http://127.0.0.1:8000/redoc: diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 060e1d58a4..7a9bc48754 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -4,18 +4,18 @@ Let's take this application as example: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} + ``` + The query parameter `q` is of type `Union[str, None]` (or `str | None` in Python 3.10), that means that it's of type `str` but could also be `None`, and indeed, the default value is `None`, so FastAPI will know it's not required. !!! note @@ -27,39 +27,124 @@ The query parameter `q` is of type `Union[str, None]` (or `str | None` in Python We are going to enforce that even though `q` is optional, whenever it is provided, **its length doesn't exceed 50 characters**. -### Import `Query` +### Import `Query` and `Annotated` -To achieve that, first import `Query` from `fastapi`: +To achieve that, first import: -=== "Python 3.6 and above" +* `Query` from `fastapi` +* `Annotated` from `typing` (or from `typing_extensions` in Python below 3.9) - ```Python hl_lines="3" - {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} +=== "Python 3.10+" + + In Python 3.9 or above, `Annotated` is part of the standard library, so you can import it from `typing`. + + ```Python hl_lines="1 3" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="1" - {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + In versions of Python below Python 3.9 you import `Annotated` from `typing_extensions`. + + It will already be installed with FastAPI. + + ```Python hl_lines="3-4" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} ``` -## Use `Query` as the default value +!!! info + FastAPI added support for `Annotated` (and started recommending it) in version 0.95.0. -And now use it as the default value of your parameter, setting the parameter `max_length` to 50: + If you have an older version, you would get errors when trying to use `Annotated`. -=== "Python 3.6 and above" + Make sure you [Upgrade the FastAPI version](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} to at least 0.95.1 before using `Annotated`. + +## Use `Annotated` in the type for the `q` parameter + +Remember I told you before that `Annotated` can be used to add metadata to your parameters in the [Python Types Intro](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? + +Now it's the time to use it with FastAPI. 🚀 + +We had this type annotation: + +=== "Python 3.10+" + + ```Python + q: str | None = None + ``` + +=== "Python 3.8+" + + ```Python + q: Union[str, None] = None + ``` + +What we will do is wrap that with `Annotated`, so it becomes: + +=== "Python 3.10+" + + ```Python + q: Annotated[str | None] = None + ``` + +=== "Python 3.8+" + + ```Python + q: Annotated[Union[str, None]] = None + ``` + +Both of those versions mean the same thing, `q` is a parameter that can be a `str` or `None`, and by default, it is `None`. + +Now let's jump to the fun stuff. 🎉 + +## Add `Query` to `Annotated` in the `q` parameter + +Now that we have this `Annotated` where we can put more metadata, add `Query` to it, and set the parameter `max_length` to 50: + +=== "Python 3.10+" ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} + ``` + +Notice that the default value is still `None`, so the parameter is still optional. + +But now, having `Query(max_length=50)` inside of `Annotated`, we are telling FastAPI that we want it to extract this value from the query parameters (this would have been the default anyway 🤷) and that we want to have **additional validation** for this value (that's why we do this, to get the additional validation). 😎 + +FastAPI will now: + +* **Validate** the data making sure that the max length is 50 characters +* Show a **clear error** for the client when the data is not valid +* **Document** the parameter in the OpenAPI schema *path operation* (so it will show up in the **automatic docs UI**) + +## Alternative (old) `Query` as the default value + +Previous versions of FastAPI (before 0.95.0) required you to use `Query` as the default value of your parameter, instead of putting it in `Annotated`, there's a high chance that you will see code using it around, so I'll explain it to you. + +!!! tip + For new code and whenever possible, use `Annotated` as explained above. There are multiple advantages (explained below) and no disadvantages. 🍰 + +This is how you would use `Query()` as the default value of your function parameter, setting the parameter `max_length` to 50: + +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} ``` -As we have to replace the default value `None` in the function with `Query()`, we can now set the default value with the parameter `Query(default=None)`, it serves the same purpose of defining that default value. +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + ``` + +As in this case (without using `Annotated`) we have to replace the default value `None` in the function with `Query()`, we now need to set the default value with the parameter `Query(default=None)`, it serves the same purpose of defining that default value (at least for FastAPI). So: @@ -67,7 +152,7 @@ So: q: Union[str, None] = Query(default=None) ``` -...makes the parameter optional, the same as: +...makes the parameter optional, with a default value of `None`, the same as: ```Python q: Union[str, None] = None @@ -79,7 +164,7 @@ And in Python 3.10 and above: q: str | None = Query(default=None) ``` -...makes the parameter optional, the same as: +...makes the parameter optional, with a default value of `None`, the same as: ```Python q: str | None = None @@ -88,7 +173,7 @@ q: str | None = None But it declares it explicitly as being a query parameter. !!! info - Have in mind that the most important part to make a parameter optional is the part: + Keep in mind that the most important part to make a parameter optional is the part: ```Python = None @@ -112,39 +197,125 @@ q: Union[str, None] = Query(default=None, max_length=50) This will validate the data, show a clear error when the data is not valid, and document the parameter in the OpenAPI schema *path operation*. +### `Query` as the default value or in `Annotated` + +Keep in mind that when using `Query` inside of `Annotated` you cannot use the `default` parameter for `Query`. + +Instead use the actual default value of the function parameter. Otherwise, it would be inconsistent. + +For example, this is not allowed: + +```Python +q: Annotated[str, Query(default="rick")] = "morty" +``` + +...because it's not clear if the default value should be `"rick"` or `"morty"`. + +So, you would use (preferably): + +```Python +q: Annotated[str, Query()] = "rick" +``` + +...or in older code bases you will find: + +```Python +q: str = Query(default="rick") +``` + +### Advantages of `Annotated` + +**Using `Annotated` is recommended** instead of the default value in function parameters, it is **better** for multiple reasons. 🤓 + +The **default** value of the **function parameter** is the **actual default** value, that's more intuitive with Python in general. 😌 + +You could **call** that same function in **other places** without FastAPI, and it would **work as expected**. If there's a **required** parameter (without a default value), your **editor** will let you know with an error, **Python** will also complain if you run it without passing the required parameter. + +When you don't use `Annotated` and instead use the **(old) default value style**, if you call that function without FastAPI in **other place**, you have to **remember** to pass the arguments to the function for it to work correctly, otherwise the values will be different from what you expect (e.g. `QueryInfo` or something similar instead of `str`). And your editor won't complain, and Python won't complain running that function, only when the operations inside error out. + +Because `Annotated` can have more than one metadata annotation, you could now even use the same function with other tools, like Typer. 🚀 + ## Add more validations You can also add a parameter `min_length`: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} ``` -## Add regular expressions +=== "Python 3.8+ non-Annotated" -You can define a regular expression that the parameter should match: + !!! tip + Prefer to use the `Annotated` version if possible. -=== "Python 3.6 and above" - - ```Python hl_lines="11" - {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} ``` -=== "Python 3.10 and above" +## Add regular expressions + +You can define a regular expression `pattern` that the parameter should match: + +=== "Python 3.10+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!} ``` -This specific regular expression checks that the received parameter value: +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} + ``` + +This specific regular expression pattern checks that the received parameter value: * `^`: starts with the following characters, doesn't have characters before. * `fixedquery`: has the exact value `fixedquery`. @@ -154,18 +325,49 @@ If you feel lost with all these **"regular expression"** ideas, don't worry. The But whenever you need them and go and learn them, know that you can already use them directly in **FastAPI**. +### Pydantic v1 `regex` instead of `pattern` + +Before Pydantic version 2 and before FastAPI 0.100.0, the parameter was called `regex` instead of `pattern`, but it's now deprecated. + +You could still see some code using it: + +=== "Python 3.10+ Pydantic v1" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py310_regex.py!} + ``` + +But know that this is deprecated and it should be updated to use the new parameter `pattern`. 🤓 + ## Default values -The same way that you can pass `None` as the value for the `default` parameter, you can pass other values. +You can, of course, use default values other than `None`. Let's say that you want to declare the `q` query parameter to have a `min_length` of `3`, and to have a default value of `"fixedquery"`: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial005.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial005.py!} + ``` !!! note - Having a default value also makes the parameter optional. + Having a default value of any type, including `None`, makes the parameter optional (not required). ## Make it required @@ -183,23 +385,70 @@ q: Union[str, None] = None But we are now declaring it with `Query`, for example like: -```Python -q: Union[str, None] = Query(default=None, min_length=3) -``` +=== "Annotated" + + ```Python + q: Annotated[Union[str, None], Query(min_length=3)] = None + ``` + +=== "non-Annotated" + + ```Python + q: Union[str, None] = Query(default=None, min_length=3) + ``` So, when you need to declare a value as required while using `Query`, you can simply not declare a default value: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial006.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006.py!} + ``` + + !!! tip + Notice that, even though in this case the `Query()` is used as the function parameter default value, we don't pass the `default=None` to `Query()`. + + Still, probably better to use the `Annotated` version. 😉 ### Required with Ellipsis (`...`) -There's an alternative way to explicitly declare that a value is required. You can set the `default` parameter to the literal value `...`: +There's an alternative way to explicitly declare that a value is required. You can set the default to the literal value `...`: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial006b.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006b_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006b_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006b.py!} + ``` !!! info If you hadn't seen that `...` before: it is a special single value, it is part of Python and is called "Ellipsis". @@ -212,33 +461,49 @@ This will let **FastAPI** know that this parameter is required. You can declare that a parameter can accept `None`, but that it's still required. This would force clients to send a value, even if the value is `None`. -To do that, you can declare that `None` is a valid type but still use `default=...`: +To do that, you can declare that `None` is a valid type but still use `...` as the default: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} + ``` + !!! tip Pydantic, which is what powers all the data validation and serialization in FastAPI, has a special behavior when you use `Optional` or `Union[Something, None]` without a default value, you can read more about it in the Pydantic docs about Required Optional fields. -### Use Pydantic's `Required` instead of Ellipsis (`...`) - -If you feel uncomfortable using `...`, you can also import and use `Required` from Pydantic: - -```Python hl_lines="2 8" -{!../../../docs_src/query_params_str_validations/tutorial006d.py!} -``` - !!! tip - Remember that in most of the cases, when something is required, you can simply omit the `default` parameter, so you normally don't have to use `...` nor `Required`. + Remember that in most of the cases, when something is required, you can simply omit the default, so you normally don't have to use `...`. ## Query parameter list / multiple values @@ -246,22 +511,49 @@ When you define a query parameter explicitly with `Query` you can also declare i For example, to declare a query parameter `q` that can appear multiple times in the URL, you can write: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+ non-Annotated" - ```Python hl_lines="7" - {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} ``` Then, with a URL like: @@ -294,18 +586,36 @@ The interactive API docs will update accordingly, to allow multiple values: And you can also define a default `list` of values if none are provided: -=== "Python 3.6 and above" +=== "Python 3.9+" ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial012.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial012_an_py39.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial012_an.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial012.py!} + ``` + If you go to: ``` @@ -327,12 +637,29 @@ the default of `q` will be: `["foo", "bar"]` and your response will be: You can also use `list` directly instead of `List[str]` (or `list[str]` in Python 3.9+): -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial013.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial013.py!} + ``` !!! note - Have in mind that in this case, FastAPI won't check the contents of the list. + Keep in mind that in this case, FastAPI won't check the contents of the list. For example, `List[int]` would check (and document) that the contents of the list are integers. But `list` alone wouldn't. @@ -343,38 +670,86 @@ You can add more information about the parameter. That information will be included in the generated OpenAPI and used by the documentation user interfaces and external tools. !!! note - Have in mind that different tools might have different levels of OpenAPI support. + Keep in mind that different tools might have different levels of OpenAPI support. Some of them might not show all the extra information declared yet, although in most of the cases, the missing feature is already planned for development. You can add a `title`: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} + ``` + And a `description`: -=== "Python 3.6 and above" +=== "Python 3.10+" + + ```Python hl_lines="14" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="15" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="13" {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} ``` -=== "Python 3.10 and above" - - ```Python hl_lines="12" - {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} - ``` - ## Alias parameters Imagine that you want the parameter to be `item-query`. @@ -393,18 +768,42 @@ But you still need it to be exactly `item-query`... Then you can declare an `alias`, and that alias is what will be used to find the parameter value: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} + ``` + ## Deprecating parameters Now let's say you don't like this parameter anymore. @@ -413,18 +812,42 @@ You have to leave it there a while because there are clients using it, but you w Then pass the parameter `deprecated=True` to `Query`: -=== "Python 3.6 and above" +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="16" + {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="18" {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} ``` -=== "Python 3.10 and above" - - ```Python hl_lines="17" - {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} - ``` - The docs will show it like this: @@ -433,18 +856,42 @@ The docs will show it like this: To exclude a query parameter from the generated OpenAPI schema (and thus, from the automatic documentation systems), set the parameter `include_in_schema` of `Query` to `False`: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} + ``` + ## Recap You can declare additional validations and metadata for your parameters. @@ -460,7 +907,7 @@ Validations specific for strings: * `min_length` * `max_length` -* `regex` +* `pattern` In these examples you saw how to declare validations for `str` values. diff --git a/docs/en/docs/tutorial/query-params.md b/docs/en/docs/tutorial/query-params.md index eec55502a3..bc3b11948a 100644 --- a/docs/en/docs/tutorial/query-params.md +++ b/docs/en/docs/tutorial/query-params.md @@ -63,18 +63,18 @@ The parameter values in your function will be: The same way, you can declare optional query parameters, by setting their default to `None`: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/query_params/tutorial002_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial002.py!} + ``` + In this case, the function parameter `q` will be optional, and will be `None` by default. !!! check @@ -84,18 +84,18 @@ In this case, the function parameter `q` will be optional, and will be `None` by You can also declare `bool` types, and they will be converted: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/query_params/tutorial003_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} + ``` + In this case, if you go to: ``` @@ -137,18 +137,18 @@ And you don't have to declare them in any specific order. They will be detected by name: -=== "Python 3.6 and above" - - ```Python hl_lines="8 10" - {!> ../../../docs_src/query_params/tutorial004.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="6 8" {!> ../../../docs_src/query_params/tutorial004_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} + ``` + ## Required query parameters When you declare a default value for non-path parameters (for now, we have only seen query parameters), then it is not required. @@ -173,16 +173,18 @@ http://127.0.0.1:8000/items/foo-item ```JSON { - "detail": [ - { - "loc": [ - "query", - "needy" - ], - "msg": "field required", - "type": "value_error.missing" - } - ] + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "needy" + ], + "msg": "Field required", + "input": null, + "url": "https://errors.pydantic.dev/2.1/v/missing" + } + ] } ``` @@ -203,18 +205,18 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy And of course, you can define some parameters as required, some as having a default value, and some entirely optional: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/query_params/tutorial006.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="8" {!> ../../../docs_src/query_params/tutorial006_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params/tutorial006.py!} + ``` + In this case, there are 3 query parameters: * `needy`, a required `str`. diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index 783783c582..8eb8ace648 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -13,17 +13,51 @@ You can define files to be uploaded by the client using `File`. Import `File` and `UploadFile` from `fastapi`: -```Python hl_lines="1" -{!../../../docs_src/request_files/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` ## Define `File` Parameters Create file parameters the same way you would for `Body` or `Form`: -```Python hl_lines="7" -{!../../../docs_src/request_files/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` !!! info `File` is a class that inherits directly from `Form`. @@ -37,7 +71,7 @@ The files will be uploaded as "form data". If you declare the type of your *path operation function* parameter as `bytes`, **FastAPI** will read the file for you and you will receive the contents as `bytes`. -Have in mind that this means that the whole contents will be stored in memory. This will work well for small files. +Keep in mind that this means that the whole contents will be stored in memory. This will work well for small files. But there are several cases in which you might benefit from using `UploadFile`. @@ -45,9 +79,26 @@ But there are several cases in which you might benefit from using `UploadFile`. Define a file parameter with a type of `UploadFile`: -```Python hl_lines="12" -{!../../../docs_src/request_files/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="13" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="12" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` Using `UploadFile` has several advantages over `bytes`: @@ -118,25 +169,66 @@ The way HTML forms (`
`) sends the data to the server normally uses You can make a file optional by using standard type annotations and setting a default value of `None`: -=== "Python 3.6 and above" +=== "Python 3.10+" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10 18" + {!> ../../../docs_src/request_files/tutorial001_02_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7 15" + {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9 17" {!> ../../../docs_src/request_files/tutorial001_02.py!} ``` -=== "Python 3.10 and above" - - ```Python hl_lines="7 14" - {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} - ``` - ## `UploadFile` with Additional Metadata You can also use `File()` with `UploadFile`, for example, to set additional metadata: -```Python hl_lines="13" -{!../../../docs_src/request_files/tutorial001_03.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="9 15" + {!> ../../../docs_src/request_files/tutorial001_03_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8 14" + {!> ../../../docs_src/request_files/tutorial001_03_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7 13" + {!> ../../../docs_src/request_files/tutorial001_03.py!} + ``` ## Multiple File Uploads @@ -146,18 +238,36 @@ They would be associated to the same "form field" sent using "form data". To use that, declare a list of `bytes` or `UploadFile`: -=== "Python 3.6 and above" +=== "Python 3.9+" ```Python hl_lines="10 15" - {!> ../../../docs_src/request_files/tutorial002.py!} + {!> ../../../docs_src/request_files/tutorial002_an_py39.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.8+" + + ```Python hl_lines="11 16" + {!> ../../../docs_src/request_files/tutorial002_an.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8 13" {!> ../../../docs_src/request_files/tutorial002_py39.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002.py!} + ``` + You will receive, as declared, a `list` of `bytes` or `UploadFile`s. !!! note "Technical Details" @@ -169,18 +279,36 @@ You will receive, as declared, a `list` of `bytes` or `UploadFile`s. And the same way as before, you can use `File()` to set additional parameters, even for `UploadFile`: -=== "Python 3.6 and above" +=== "Python 3.9+" - ```Python hl_lines="18" - {!> ../../../docs_src/request_files/tutorial003.py!} + ```Python hl_lines="11 18-20" + {!> ../../../docs_src/request_files/tutorial003_an_py39.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.8+" - ```Python hl_lines="16" + ```Python hl_lines="12 19-21" + {!> ../../../docs_src/request_files/tutorial003_an.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9 16" {!> ../../../docs_src/request_files/tutorial003_py39.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="11 18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + ## Recap Use `File`, `bytes`, and `UploadFile` to declare files to be uploaded in the request, sent as form data. diff --git a/docs/en/docs/tutorial/request-forms-and-files.md b/docs/en/docs/tutorial/request-forms-and-files.md index 6844f8c658..a58291dc81 100644 --- a/docs/en/docs/tutorial/request-forms-and-files.md +++ b/docs/en/docs/tutorial/request-forms-and-files.md @@ -9,17 +9,51 @@ You can define files and form fields at the same time using `File` and `Form`. ## Import `File` and `Form` -```Python hl_lines="1" -{!../../../docs_src/request_forms_and_files/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms_and_files/tutorial001.py!} + ``` ## Define `File` and `Form` parameters Create file and form parameters the same way you would for `Body` or `Query`: -```Python hl_lines="8" -{!../../../docs_src/request_forms_and_files/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="10-12" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="8" + {!> ../../../docs_src/request_forms_and_files/tutorial001.py!} + ``` The files and form fields will be uploaded as form data and you will receive the files and form fields. diff --git a/docs/en/docs/tutorial/request-forms.md b/docs/en/docs/tutorial/request-forms.md index 26922685a6..0e8ac5f4f9 100644 --- a/docs/en/docs/tutorial/request-forms.md +++ b/docs/en/docs/tutorial/request-forms.md @@ -11,17 +11,51 @@ When you need to receive form fields instead of JSON, you can use `Form`. Import `Form` from `fastapi`: -```Python hl_lines="1" -{!../../../docs_src/request_forms/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms/tutorial001.py!} + ``` ## Define `Form` parameters Create form parameters the same way you would for `Body` or `Query`: -```Python hl_lines="7" -{!../../../docs_src/request_forms/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/request_forms/tutorial001.py!} + ``` For example, in one of the ways the OAuth2 specification can be used (called "password flow") it is required to send a `username` and `password` as form fields. diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index 69c02052d6..d5683ac7f2 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -4,22 +4,22 @@ You can declare the type used for the response by annotating the *path operation You can use **type annotations** the same way you would for input data in function **parameters**, you can use Pydantic models, lists, dictionaries, scalar values like integers, booleans, etc. -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="18 23" - {!> ../../../docs_src/response_model/tutorial001_01.py!} + ```Python hl_lines="16 21" + {!> ../../../docs_src/response_model/tutorial001_01_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="18 23" {!> ../../../docs_src/response_model/tutorial001_01_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="16 21" - {!> ../../../docs_src/response_model/tutorial001_01_py310.py!} + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01.py!} ``` FastAPI will use this return type to: @@ -53,22 +53,22 @@ You can use the `response_model` parameter in any of the *path operations*: * `@app.delete()` * etc. -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="17 22 24-27" - {!> ../../../docs_src/response_model/tutorial001.py!} + {!> ../../../docs_src/response_model/tutorial001_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="17 22 24-27" {!> ../../../docs_src/response_model/tutorial001_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" ```Python hl_lines="17 22 24-27" - {!> ../../../docs_src/response_model/tutorial001_py310.py!} + {!> ../../../docs_src/response_model/tutorial001.py!} ``` !!! note @@ -89,22 +89,24 @@ If you declare both a return type and a `response_model`, the `response_model` w This way you can add correct type annotations to your functions even when you are returning a type different than the response model, to be used by the editor and tools like mypy. And still you can have FastAPI do the data validation, documentation, etc. using the `response_model`. +You can also use `response_model=None` to disable creating a response model for that *path operation*, you might need to do it if you are adding type annotations for things that are not valid Pydantic fields, you will see an example of that in one of the sections below. + ## Return the same input data Here we are declaring a `UserIn` model, it will contain a plaintext password: -=== "Python 3.6 and above" - - ```Python hl_lines="9 11" - {!> ../../../docs_src/response_model/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7 9" {!> ../../../docs_src/response_model/tutorial002_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9 11" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + !!! info To use `EmailStr`, first install `email_validator`. @@ -113,18 +115,18 @@ Here we are declaring a `UserIn` model, it will contain a plaintext password: And we are using this model to declare our input and the same model to declare our output: -=== "Python 3.6 and above" - - ```Python hl_lines="18" - {!> ../../../docs_src/response_model/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="16" {!> ../../../docs_src/response_model/tutorial002_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + Now, whenever a browser is creating a user with a password, the API will return the same password in the response. In this case, it might not be a problem, because it's the same user sending the password. @@ -138,46 +140,46 @@ But if we use the same model for another *path operation*, we could be sending o We can instead create an input model with the plaintext password and an output model without it: -=== "Python 3.6 and above" - - ```Python hl_lines="9 11 16" - {!> ../../../docs_src/response_model/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="9 11 16" {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + Here, even though our *path operation function* is returning the same input user that contains the password: -=== "Python 3.6 and above" - - ```Python hl_lines="24" - {!> ../../../docs_src/response_model/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="24" {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + ...we declared the `response_model` to be our model `UserOut`, that doesn't include the password: -=== "Python 3.6 and above" - - ```Python hl_lines="22" - {!> ../../../docs_src/response_model/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="22" {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + So, **FastAPI** will take care of filtering out all the data that is not declared in the output model (using Pydantic). ### `response_model` or Return Type @@ -200,18 +202,18 @@ But in most of the cases where we need to do something like this, we want the mo And in those cases, we can use classes and inheritance to take advantage of function **type annotations** to get better support in the editor and tools, and still get the FastAPI **data filtering**. -=== "Python 3.6 and above" - - ```Python hl_lines="9-13 15-16 20" - {!> ../../../docs_src/response_model/tutorial003_01.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7-10 13-14 18" {!> ../../../docs_src/response_model/tutorial003_01_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9-13 15-16 20" + {!> ../../../docs_src/response_model/tutorial003_01.py!} + ``` + With this, we get tooling support, from editors and mypy as this code is correct in terms of types, but we also get the data filtering from FastAPI. How does this work? Let's check that out. 🤓 @@ -244,26 +246,94 @@ And both models will be used for the interactive API documentation: +## Other Return Type Annotations + +There might be cases where you return something that is not a valid Pydantic field and you annotate it in the function, only to get the support provided by tooling (the editor, mypy, etc). + +### Return a Response Directly + +The most common case would be [returning a Response directly as explained later in the advanced docs](../advanced/response-directly.md){.internal-link target=_blank}. + +```Python hl_lines="8 10-11" +{!> ../../../docs_src/response_model/tutorial003_02.py!} +``` + +This simple case is handled automatically by FastAPI because the return type annotation is the class (or a subclass) of `Response`. + +And tools will also be happy because both `RedirectResponse` and `JSONResponse` are subclasses of `Response`, so the type annotation is correct. + +### Annotate a Response Subclass + +You can also use a subclass of `Response` in the type annotation: + +```Python hl_lines="8-9" +{!> ../../../docs_src/response_model/tutorial003_03.py!} +``` + +This will also work because `RedirectResponse` is a subclass of `Response`, and FastAPI will automatically handle this simple case. + +### Invalid Return Type Annotations + +But when you return some other arbitrary object that is not a valid Pydantic type (e.g. a database object) and you annotate it like that in the function, FastAPI will try to create a Pydantic response model from that type annotation, and will fail. + +The same would happen if you had something like a union between different types where one or more of them are not valid Pydantic types, for example this would fail 💥: + +=== "Python 3.10+" + + ```Python hl_lines="8" + {!> ../../../docs_src/response_model/tutorial003_04_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/response_model/tutorial003_04.py!} + ``` + +...this fails because the type annotation is not a Pydantic type and is not just a single `Response` class or subclass, it's a union (any of the two) between a `Response` and a `dict`. + +### Disable Response Model + +Continuing from the example above, you might not want to have the default data validation, documentation, filtering, etc. that is performed by FastAPI. + +But you might want to still keep the return type annotation in the function to get the support from tools like editors and type checkers (e.g. mypy). + +In this case, you can disable the response model generation by setting `response_model=None`: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/response_model/tutorial003_05_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/response_model/tutorial003_05.py!} + ``` + +This will make FastAPI skip the response model generation and that way you can have any return type annotations you need without it affecting your FastAPI application. 🤓 + ## Response Model encoding parameters Your response model could have default values, like: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="11 13-14" - {!> ../../../docs_src/response_model/tutorial004.py!} + ```Python hl_lines="9 11-12" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="11 13-14" {!> ../../../docs_src/response_model/tutorial004_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="9 11-12" - {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004.py!} ``` * `description: Union[str, None] = None` (or `str | None = None` in Python 3.10) has a default of `None`. @@ -278,22 +348,22 @@ For example, if you have models with many optional attributes in a NoSQL databas You can set the *path operation decorator* parameter `response_model_exclude_unset=True`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="24" - {!> ../../../docs_src/response_model/tutorial004.py!} + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="24" {!> ../../../docs_src/response_model/tutorial004_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="22" - {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004.py!} ``` and those default values won't be included in the response, only the values actually set. @@ -307,6 +377,11 @@ So, if you send a request to that *path operation* for the item with ID `foo`, t } ``` +!!! info + In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. + + The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. + !!! info FastAPI uses Pydantic model's `.dict()` with its `exclude_unset` parameter to achieve this. @@ -371,18 +446,18 @@ This can be used as a quick shortcut if you have only one Pydantic model and wan This also applies to `response_model_by_alias` that works similarly. -=== "Python 3.6 and above" - - ```Python hl_lines="31 37" - {!> ../../../docs_src/response_model/tutorial005.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="29 35" {!> ../../../docs_src/response_model/tutorial005_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial005.py!} + ``` + !!! tip The syntax `{"name", "description"}` creates a `set` with those two values. @@ -392,18 +467,18 @@ This can be used as a quick shortcut if you have only one Pydantic model and wan If you forget to use a `set` and use a `list` or `tuple` instead, FastAPI will still convert it to a `set` and it will work correctly: -=== "Python 3.6 and above" - - ```Python hl_lines="31 37" - {!> ../../../docs_src/response_model/tutorial006.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="29 35" {!> ../../../docs_src/response_model/tutorial006_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial006.py!} + ``` + ## Recap Use the *path operation decorator's* parameter `response_model` to define response models and especially to ensure private data is filtered out. diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 94347018d0..9bb9ba4e31 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -4,51 +4,77 @@ You can declare examples of the data your app can receive. Here are several ways to do it. -## Pydantic `schema_extra` +## Extra JSON Schema data in Pydantic models -You can declare an `example` for a Pydantic model using `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization: +You can declare `examples` for a Pydantic model that will be added to the generated JSON Schema. -=== "Python 3.6 and above" +=== "Python 3.10+ Pydantic v2" - ```Python hl_lines="15-23" - {!> ../../../docs_src/schema_extra_example/tutorial001.py!} - ``` - -=== "Python 3.10 and above" - - ```Python hl_lines="13-21" + ```Python hl_lines="13-24" {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} ``` +=== "Python 3.10+ Pydantic v1" + + ```Python hl_lines="13-23" + {!> ../../../docs_src/schema_extra_example/tutorial001_py310_pv1.py!} + ``` + +=== "Python 3.8+ Pydantic v2" + + ```Python hl_lines="15-26" + {!> ../../../docs_src/schema_extra_example/tutorial001.py!} + ``` + +=== "Python 3.8+ Pydantic v1" + + ```Python hl_lines="15-25" + {!> ../../../docs_src/schema_extra_example/tutorial001_pv1.py!} + ``` + That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs. +=== "Pydantic v2" + + In Pydantic version 2, you would use the attribute `model_config`, that takes a `dict` as described in Pydantic's docs: Model Config. + + You can set `"json_schema_extra"` with a `dict` containing any additional data you would like to show up in the generated JSON Schema, including `examples`. + +=== "Pydantic v1" + + In Pydantic version 1, you would use an internal class `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization. + + You can set `schema_extra` with a `dict` containing any additional data you would like to show up in the generated JSON Schema, including `examples`. + !!! tip You could use the same technique to extend the JSON Schema and add your own custom extra info. For example you could use it to add metadata for a frontend user interface, etc. +!!! info + OpenAPI 3.1.0 (used since FastAPI 0.99.0) added support for `examples`, which is part of the **JSON Schema** standard. + + Before that, it only supported the keyword `example` with a single example. That is still supported by OpenAPI 3.1.0, but is deprecated and is not part of the JSON Schema standard. So you are encouraged to migrate `example` to `examples`. 🤓 + + You can read more at the end of this page. + ## `Field` additional arguments -When using `Field()` with Pydantic models, you can also declare extra info for the **JSON Schema** by passing any other arbitrary arguments to the function. +When using `Field()` with Pydantic models, you can also declare additional `examples`: -You can use this to add `example` for each field: - -=== "Python 3.6 and above" - - ```Python hl_lines="4 10-13" - {!> ../../../docs_src/schema_extra_example/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="2 8-11" {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} ``` -!!! warning - Keep in mind that those extra arguments passed won't add any validation, only extra information, for documentation purposes. +=== "Python 3.8+" -## `example` and `examples` in OpenAPI + ```Python hl_lines="4 10-13" + {!> ../../../docs_src/schema_extra_example/tutorial002.py!} + ``` + +## `examples` in JSON Schema - OpenAPI When using any of: @@ -60,24 +86,48 @@ When using any of: * `Form()` * `File()` -you can also declare a data `example` or a group of `examples` with additional information that will be added to **OpenAPI**. +you can also declare a group of `examples` with additional information that will be added to their **JSON Schemas** inside of **OpenAPI**. -### `Body` with `example` +### `Body` with `examples` -Here we pass an `example` of the data expected in `Body()`: +Here we pass `examples` containing one example of the data expected in `Body()`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="20-25" - {!> ../../../docs_src/schema_extra_example/tutorial003.py!} + ```Python hl_lines="22-29" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" - ```Python hl_lines="18-23" + ```Python hl_lines="22-29" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="23-30" + {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="18-25" {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="20-27" + {!> ../../../docs_src/schema_extra_example/tutorial003.py!} + ``` + ### Example in the docs UI With any of the methods above it would look like this in the `/docs`: @@ -86,7 +136,71 @@ With any of the methods above it would look like this in the `/docs`: ### `Body` with multiple `examples` -Alternatively to the single `example`, you can pass `examples` using a `dict` with **multiple examples**, each with extra information that will be added to **OpenAPI** too. +You can of course also pass multiple `examples`: + +=== "Python 3.10+" + + ```Python hl_lines="23-38" + {!> ../../../docs_src/schema_extra_example/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="23-38" + {!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="24-39" + {!> ../../../docs_src/schema_extra_example/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="19-34" + {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="21-36" + {!> ../../../docs_src/schema_extra_example/tutorial004.py!} + ``` + +When you do this, the examples will be part of the internal **JSON Schema** for that body data. + +Nevertheless, at the time of writing this, Swagger UI, the tool in charge of showing the docs UI, doesn't support showing multiple examples for the data in **JSON Schema**. But read below for a workaround. + +### OpenAPI-specific `examples` + +Since before **JSON Schema** supported `examples` OpenAPI had support for a different field also called `examples`. + +This **OpenAPI-specific** `examples` goes in another section in the OpenAPI specification. It goes in the **details for each *path operation***, not inside each JSON Schema. + +And Swagger UI has supported this particular `examples` field for a while. So, you can use it to **show** different **examples in the docs UI**. + +The shape of this OpenAPI-specific field `examples` is a `dict` with **multiple examples** (instead of a `list`), each with extra information that will be added to **OpenAPI** too. + +This doesn't go inside of each JSON Schema contained in OpenAPI, this goes outside, in the *path operation* directly. + +### Using the `openapi_examples` Parameter + +You can declare the OpenAPI-specific `examples` in FastAPI with the parameter `openapi_examples` for: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` The keys of the `dict` identify each example, and each value is another `dict`. @@ -97,45 +211,116 @@ Each specific example `dict` in the `examples` can contain: * `value`: This is the actual example shown, e.g. a `dict`. * `externalValue`: alternative to `value`, a URL pointing to the example. Although this might not be supported by as many tools as `value`. -=== "Python 3.6 and above" +You can use it like this: - ```Python hl_lines="21-47" - {!> ../../../docs_src/schema_extra_example/tutorial004.py!} +=== "Python 3.10+" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial005_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="24-50" + {!> ../../../docs_src/schema_extra_example/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="19-45" - {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} + {!> ../../../docs_src/schema_extra_example/tutorial005_py310.py!} ``` -### Examples in the docs UI +=== "Python 3.8+ non-Annotated" -With `examples` added to `Body()` the `/docs` would look like: + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="21-47" + {!> ../../../docs_src/schema_extra_example/tutorial005.py!} + ``` + +### OpenAPI Examples in the Docs UI + +With `openapi_examples` added to `Body()` the `/docs` would look like: ## Technical Details +!!! tip + If you are already using **FastAPI** version **0.99.0 or above**, you can probably **skip** these details. + + They are more relevant for older versions, before OpenAPI 3.1.0 was available. + + You can consider this a brief OpenAPI and JSON Schema **history lesson**. 🤓 + !!! warning These are very technical details about the standards **JSON Schema** and **OpenAPI**. If the ideas above already work for you, that might be enough, and you probably don't need these details, feel free to skip them. -When you add an example inside of a Pydantic model, using `schema_extra` or `Field(example="something")` that example is added to the **JSON Schema** for that Pydantic model. +Before OpenAPI 3.1.0, OpenAPI used an older and modified version of **JSON Schema**. + +JSON Schema didn't have `examples`, so OpenAPI added it's own `example` field to its own modified version. + +OpenAPI also added `example` and `examples` fields to other parts of the specification: + +* `Parameter Object` (in the specification) that was used by FastAPI's: + * `Path()` + * `Query()` + * `Header()` + * `Cookie()` +* `Request Body Object`, in the field `content`, on the `Media Type Object` (in the specification) that was used by FastAPI's: + * `Body()` + * `File()` + * `Form()` + +!!! info + This old OpenAPI-specific `examples` parameter is now `openapi_examples` since FastAPI `0.103.0`. + +### JSON Schema's `examples` field + +But then JSON Schema added an `examples` field to a new version of the specification. + +And then the new OpenAPI 3.1.0 was based on the latest version (JSON Schema 2020-12) that included this new field `examples`. + +And now this new `examples` field takes precedence over the old single (and custom) `example` field, that is now deprecated. + +This new `examples` field in JSON Schema is **just a `list`** of examples, not a dict with extra metadata as in the other places in OpenAPI (described above). + +!!! info + Even after OpenAPI 3.1.0 was released with this new simpler integration with JSON Schema, for a while, Swagger UI, the tool that provides the automatic docs, didn't support OpenAPI 3.1.0 (it does since version 5.0.0 🎉). + + Because of that, versions of FastAPI previous to 0.99.0 still used versions of OpenAPI lower than 3.1.0. + +### Pydantic and FastAPI `examples` + +When you add `examples` inside of a Pydantic model, using `schema_extra` or `Field(examples=["something"])` that example is added to the **JSON Schema** for that Pydantic model. And that **JSON Schema** of the Pydantic model is included in the **OpenAPI** of your API, and then it's used in the docs UI. -**JSON Schema** doesn't really have a field `example` in the standards. Recent versions of JSON Schema define a field `examples`, but OpenAPI 3.0.3 is based on an older version of JSON Schema that didn't have `examples`. +In versions of FastAPI before 0.99.0 (0.99.0 and above use the newer OpenAPI 3.1.0) when you used `example` or `examples` with any of the other utilities (`Query()`, `Body()`, etc.) those examples were not added to the JSON Schema that describes that data (not even to OpenAPI's own version of JSON Schema), they were added directly to the *path operation* declaration in OpenAPI (outside the parts of OpenAPI that use JSON Schema). -So, OpenAPI 3.0.3 defined its own `example` for the modified version of **JSON Schema** it uses, for the same purpose (but it's a single `example`, not `examples`), and that's what is used by the API docs UI (using Swagger UI). +But now that FastAPI 0.99.0 and above uses OpenAPI 3.1.0, that uses JSON Schema 2020-12, and Swagger UI 5.0.0 and above, everything is more consistent and the examples are included in JSON Schema. -So, although `example` is not part of JSON Schema, it is part of OpenAPI's custom version of JSON Schema, and that's what will be used by the docs UI. +### Swagger UI and OpenAPI-specific `examples` -But when you use `example` or `examples` with any of the other utilities (`Query()`, `Body()`, etc.) those examples are not added to the JSON Schema that describes that data (not even to OpenAPI's own version of JSON Schema), they are added directly to the *path operation* declaration in OpenAPI (outside the parts of OpenAPI that use JSON Schema). +Now, as Swagger UI didn't support multiple JSON Schema examples (as of 2023-08-26), users didn't have a way to show multiple examples in the docs. -For `Path()`, `Query()`, `Header()`, and `Cookie()`, the `example` or `examples` are added to the OpenAPI definition, to the `Parameter Object` (in the specification). +To solve that, FastAPI `0.103.0` **added support** for declaring the same old **OpenAPI-specific** `examples` field with the new parameter `openapi_examples`. 🤓 -And for `Body()`, `File()`, and `Form()`, the `example` or `examples` are equivalently added to the OpenAPI definition, to the `Request Body Object`, in the field `content`, on the `Media Type Object` (in the specification). +### Summary -On the other hand, there's a newer version of OpenAPI: **3.1.0**, recently released. It is based on the latest JSON Schema and most of the modifications from OpenAPI's custom version of JSON Schema are removed, in exchange of the features from the recent versions of JSON Schema, so all these small differences are reduced. Nevertheless, Swagger UI currently doesn't support OpenAPI 3.1.0, so, for now, it's better to continue using the ideas above. +I used to say I didn't like history that much... and look at me now giving "tech history" lessons. 😅 + +In short, **upgrade to FastAPI 0.99.0 or above**, and things are much **simpler, consistent, and intuitive**, and you don't have to know all these historic details. 😎 diff --git a/docs/en/docs/tutorial/security/first-steps.md b/docs/en/docs/tutorial/security/first-steps.md index 45ffaab90c..2f39f1ec26 100644 --- a/docs/en/docs/tutorial/security/first-steps.md +++ b/docs/en/docs/tutorial/security/first-steps.md @@ -20,9 +20,27 @@ Let's first just use the code and see how it works, and then we'll come back to Copy the example in a file `main.py`: -```Python -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + {!> ../../../docs_src/security/tutorial001.py!} + ``` + ## Run it @@ -116,9 +134,26 @@ In this example we are going to use **OAuth2**, with the **Password** flow, usin When we create an instance of the `OAuth2PasswordBearer` class we pass in the `tokenUrl` parameter. This parameter contains the URL that the client (the frontend running in the user's browser) will use to send the `username` and `password` in order to get a token. -```Python hl_lines="6" -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="8" + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="7" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="6" + {!> ../../../docs_src/security/tutorial001.py!} + ``` !!! tip Here `tokenUrl="token"` refers to a relative URL `token` that we haven't created yet. As it's a relative URL, it's equivalent to `./token`. @@ -150,9 +185,26 @@ So, it can be used with `Depends`. Now you can pass that `oauth2_scheme` in a dependency with `Depends`. -```Python hl_lines="10" -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="12" + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="10" + {!> ../../../docs_src/security/tutorial001.py!} + ``` This dependency will provide a `str` that is assigned to the parameter `token` of the *path operation function*. diff --git a/docs/en/docs/tutorial/security/get-current-user.md b/docs/en/docs/tutorial/security/get-current-user.md index 13e867216b..dc6d87c9ca 100644 --- a/docs/en/docs/tutorial/security/get-current-user.md +++ b/docs/en/docs/tutorial/security/get-current-user.md @@ -2,9 +2,26 @@ In the previous chapter the security system (which is based on the dependency injection system) was giving the *path operation function* a `token` as a `str`: -```Python hl_lines="10" -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="12" + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="10" + {!> ../../../docs_src/security/tutorial001.py!} + ``` But that is still not that useful. @@ -16,18 +33,42 @@ First, let's create a Pydantic user model. The same way we use Pydantic to declare bodies, we can use it anywhere else: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="5 12-16" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="5 12-16" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="5 13-17" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="3 10-14" {!> ../../../docs_src/security/tutorial002_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="5 12-16" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + ## Create a `get_current_user` dependency Let's create a dependency `get_current_user`. @@ -38,50 +79,122 @@ Remember that dependencies can have sub-dependencies? The same as we were doing before in the *path operation* directly, our new dependency `get_current_user` will receive a `token` as a `str` from the sub-dependency `oauth2_scheme`: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="25" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="26" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="23" {!> ../../../docs_src/security/tutorial002_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + ## Get the user `get_current_user` will use a (fake) utility function we created, that takes a token as a `str` and returns our Pydantic `User` model: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="19-22 26-27" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20-23 27-28" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17-20 24-25" {!> ../../../docs_src/security/tutorial002_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + ## Inject the current user So now we can use the same `Depends` with our `get_current_user` in the *path operation*: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="31" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="32" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="29" {!> ../../../docs_src/security/tutorial002_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + Notice that we declare the type of `current_user` as the Pydantic model `User`. This will help us inside of the function with all the completion and type checks. @@ -114,7 +227,7 @@ Just use any kind of model, any kind of class, any kind of database that you nee ## Code size -This example might seem verbose. Have in mind that we are mixing security, data models, utility functions and *path operations* in the same file. +This example might seem verbose. Keep in mind that we are mixing security, data models, utility functions and *path operations* in the same file. But here's the key point. @@ -128,18 +241,42 @@ And all of them (or any portion of them that you want) can take the advantage of And all these thousands of *path operations* can be as small as 3 lines: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="30-32" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="31-33" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="28-30" {!> ../../../docs_src/security/tutorial002_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + ## Recap You can now get the current user directly in your *path operation function*. diff --git a/docs/en/docs/tutorial/security/index.md b/docs/en/docs/tutorial/security/index.md index 9aed2adb5d..659a94dc30 100644 --- a/docs/en/docs/tutorial/security/index.md +++ b/docs/en/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# Security Intro +# Security There are many ways to handle security, authentication and authorization. @@ -26,7 +26,7 @@ That's what all the systems with "login with Facebook, Google, Twitter, GitHub" ### OAuth 1 -There was an OAuth 1, which is very different from OAuth2, and more complex, as it included directly specifications on how to encrypt the communication. +There was an OAuth 1, which is very different from OAuth2, and more complex, as it included direct specifications on how to encrypt the communication. It is not very popular or used nowadays. diff --git a/docs/en/docs/tutorial/security/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md index 41f00fd41e..1c792e3d9e 100644 --- a/docs/en/docs/tutorial/security/oauth2-jwt.md +++ b/docs/en/docs/tutorial/security/oauth2-jwt.md @@ -109,18 +109,42 @@ And another utility to verify if a received password matches the hash stored. And another one to authenticate and return a user. -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="7 48 55-56 59-60 69-75" - {!> ../../../docs_src/security/tutorial004.py!} + {!> ../../../docs_src/security/tutorial004_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="7 48 55-56 59-60 69-75" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="7 49 56-57 60-61 70-76" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="6 47 54-55 58-59 68-74" {!> ../../../docs_src/security/tutorial004_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7 48 55-56 59-60 69-75" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + !!! note If you check the new (fake) database `fake_users_db`, you will see how the hashed password looks like now: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. @@ -152,18 +176,42 @@ Define a Pydantic Model that will be used in the token endpoint for the response Create a utility function to generate a new access token. -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="6 12-14 28-30 78-86" - {!> ../../../docs_src/security/tutorial004.py!} + {!> ../../../docs_src/security/tutorial004_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="6 12-14 28-30 78-86" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="6 13-15 29-31 79-87" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="5 11-13 27-29 77-85" {!> ../../../docs_src/security/tutorial004_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="6 12-14 28-30 78-86" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + ## Update the dependencies Update `get_current_user` to receive the same token as before, but this time, using JWT tokens. @@ -172,36 +220,84 @@ Decode the received token, verify it, and return the current user. If the token is invalid, return an HTTP error right away. -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="89-106" - {!> ../../../docs_src/security/tutorial004.py!} + {!> ../../../docs_src/security/tutorial004_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="89-106" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="90-107" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="88-105" {!> ../../../docs_src/security/tutorial004_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="89-106" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + ## Update the `/token` *path operation* Create a `timedelta` with the expiration time of the token. -Create a real JWT access token and return it. +Create a real JWT access token and return it -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="115-128" - {!> ../../../docs_src/security/tutorial004.py!} + ```Python hl_lines="117-132" + {!> ../../../docs_src/security/tutorial004_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" - ```Python hl_lines="114-127" + ```Python hl_lines="117-132" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="118-133" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="114-129" {!> ../../../docs_src/security/tutorial004_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="115-130" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + ### Technical details about the JWT "subject" `sub` The JWT specification says that there's a key `sub`, with the subject of the token. @@ -222,7 +318,7 @@ In those cases, several of those entities could have the same ID, let's say `foo So, to avoid ID collisions, when creating the JWT token for the user, you could prefix the value of the `sub` key, e.g. with `username:`. So, in this example, the value of `sub` could have been: `username:johndoe`. -The important thing to have in mind is that the `sub` key should have a unique identifier across the entire application, and it should be a string. +The important thing to keep in mind is that the `sub` key should have a unique identifier across the entire application, and it should be a string. ## Check it diff --git a/docs/en/docs/tutorial/security/simple-oauth2.md b/docs/en/docs/tutorial/security/simple-oauth2.md index 505b223b16..88edc9eab9 100644 --- a/docs/en/docs/tutorial/security/simple-oauth2.md +++ b/docs/en/docs/tutorial/security/simple-oauth2.md @@ -49,18 +49,42 @@ Now let's use the utilities provided by **FastAPI** to handle this. First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depends` in the *path operation* for `/token`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="4 76" - {!> ../../../docs_src/security/tutorial003.py!} + ```Python hl_lines="4 78" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="4 78" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4 79" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="2 74" {!> ../../../docs_src/security/tutorial003_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4 76" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + `OAuth2PasswordRequestForm` is a class dependency that declares a form body with: * The `username`. @@ -98,18 +122,42 @@ If there is no such user, we return an error saying "incorrect username or passw For the error, we use the exception `HTTPException`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="3 77-79" - {!> ../../../docs_src/security/tutorial003.py!} + ```Python hl_lines="3 79-81" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="3 79-81" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3 80-82" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1 75-77" {!> ../../../docs_src/security/tutorial003_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="3 77-79" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + ### Check the password At this point we have the user data from our database, but we haven't checked the password. @@ -134,18 +182,42 @@ If your database is stolen, the thief won't have your users' plaintext passwords So, the thief won't be able to try to use those same passwords in another system (as many users use the same password everywhere, this would be dangerous). -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="80-83" - {!> ../../../docs_src/security/tutorial003.py!} + ```Python hl_lines="82-85" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="82-85" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="83-86" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="78-81" {!> ../../../docs_src/security/tutorial003_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="80-83" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + #### About `**user_dict` `UserInDB(**user_dict)` means: @@ -180,18 +252,42 @@ For this simple example, we are going to just be completely insecure and return But for now, let's focus on the specific details we need. -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="85" - {!> ../../../docs_src/security/tutorial003.py!} + ```Python hl_lines="87" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="87" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="88" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="83" {!> ../../../docs_src/security/tutorial003_py310.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="85" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + !!! tip By the spec, you should return a JSON with an `access_token` and a `token_type`, the same as in this example. @@ -213,18 +309,42 @@ Both of these dependencies will just return an HTTP error if the user doesn't ex So, in our endpoint, we will only get a user if the user exists, was correctly authenticated, and is active: -=== "Python 3.6 and above" +=== "Python 3.10+" + + ```Python hl_lines="58-66 69-74 94" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="58-66 69-74 94" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="59-67 70-75 95" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="56-64 67-70 88" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="58-66 69-72 90" {!> ../../../docs_src/security/tutorial003.py!} ``` -=== "Python 3.10 and above" - - ```Python hl_lines="55-64 67-70 88" - {!> ../../../docs_src/security/tutorial003_py310.py!} - ``` - !!! info The additional header `WWW-Authenticate` with value `Bearer` we are returning here is also part of the spec. diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index 5ccaf05ece..70d9482df2 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -1,5 +1,12 @@ # SQL (Relational) Databases +!!! info + These docs are about to be updated. 🎉 + + The current version assumes Pydantic v1, and SQLAlchemy versions less than 2.0. + + The new docs will include Pydantic v2 and will use SQLModel (which is also based on SQLAlchemy) once it is updated to use Pydantic v2 as well. + **FastAPI** doesn't require you to use a SQL (relational) database. But you can use any relational database that you want. @@ -262,22 +269,22 @@ So, the user will also have a `password` when creating it. But for security, the `password` won't be in other Pydantic *models*, for example, it won't be sent from the API when reading a user. -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="3 6-8 11-12 23-24 27-28" - {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ```Python hl_lines="1 4-6 9-10 21-22 25-26" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="3 6-8 11-12 23-24 27-28" {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="1 4-6 9-10 21-22 25-26" - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` #### SQLAlchemy style and Pydantic style @@ -294,7 +301,7 @@ while Pydantic *models* declare the types using `:`, the new type annotation syn name: str ``` -Have it in mind, so you don't get confused when using `=` and `:` with them. +Keep these in mind, so you don't get confused when using `=` and `:` with them. ### Create Pydantic *models* / schemas for reading / returning @@ -306,22 +313,22 @@ The same way, when reading a user, we can now declare that `items` will contain Not only the IDs of those items, but all the data that we defined in the Pydantic *model* for reading items: `Item`. -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="15-17 31-34" - {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ```Python hl_lines="13-15 29-32" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="15-17 31-34" {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="13-15 29-32" - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` !!! tip @@ -335,22 +342,22 @@ This ../../../docs_src/sql_databases/sql_app/schemas.py!} + ```Python hl_lines="13 17-18 29 34-35" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="15 19-20 31 36-37" {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="13 17-18 29 34-35" - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ```Python hl_lines="15 19-20 31 36-37" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` !!! tip @@ -444,6 +451,11 @@ The steps are: {!../../../docs_src/sql_databases/sql_app/crud.py!} ``` +!!! info + In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. + + The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. + !!! tip The SQLAlchemy model for `User` contains a `hashed_password` that should contain a secure hashed version of the password. @@ -481,18 +493,18 @@ And now in the file `sql_app/main.py` let's integrate and use all the other part In a very simplistic way create the database tables: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="7" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + #### Alembic Note Normally you would probably initialize your database (create tables, etc) with Alembic. @@ -501,7 +513,7 @@ And you would also use Alembic for "migrations" (that's its main job). A "migration" is the set of steps needed whenever you change the structure of your SQLAlchemy models, add a new attribute, etc. to replicate those changes in the database, add a new column, a new table, etc. -You can find an example of Alembic in a FastAPI project in the templates from [Project Generation - Template](../project-generation.md){.internal-link target=_blank}. Specifically in the `alembic` directory in the source code. +You can find an example of Alembic in a FastAPI project in the templates from [Project Generation - Template](../project-generation.md){.internal-link target=_blank}. Specifically in the `alembic` directory in the source code. ### Create a dependency @@ -515,18 +527,18 @@ For that, we will create a new dependency with `yield`, as explained before in t Our dependency will create a new SQLAlchemy `SessionLocal` that will be used in a single request, and then close it once the request is finished. -=== "Python 3.6 and above" - - ```Python hl_lines="15-20" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="13-18" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="15-20" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + !!! info We put the creation of the `SessionLocal()` and handling of the requests in a `try` block. @@ -540,18 +552,18 @@ And then, when using the dependency in a *path operation function*, we declare i This will then give us better editor support inside the *path operation function*, because the editor will know that the `db` parameter is of type `Session`: -=== "Python 3.6 and above" - - ```Python hl_lines="24 32 38 47 53" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="22 30 36 45 51" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="24 32 38 47 53" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + !!! info "Technical Details" The parameter `db` is actually of type `SessionLocal`, but this class (created with `sessionmaker()`) is a "proxy" of a SQLAlchemy `Session`, so, the editor doesn't really know what methods are provided. @@ -561,18 +573,18 @@ This will then give us better editor support inside the *path operation function Now, finally, here's the standard **FastAPI** *path operations* code. -=== "Python 3.6 and above" - - ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + We are creating the database session before each request in the dependency with `yield`, and then closing it afterwards. And then we can create the required dependency in the *path operation function*, to get that session directly. @@ -617,7 +629,7 @@ def read_user(user_id: int, db: Session = Depends(get_db)): ``` !!! info - If you need to connect to your relational database asynchronously, see [Async SQL (Relational) Databases](../advanced/async-sql-databases.md){.internal-link target=_blank}. + If you need to connect to your relational database asynchronously, see [Async SQL (Relational) Databases](../how-to/async-sql-encode-databases.md){.internal-link target=_blank}. !!! note "Very Technical Details" If you are curious and have a deep technical knowledge, you can check the very technical details of how this `async def` vs `def` is handled in the [Async](../async.md#very-technical-details){.internal-link target=_blank} docs. @@ -654,22 +666,22 @@ For example, in a background task worker with ../../../docs_src/sql_databases/sql_app/schemas.py!} + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" ```Python - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` * `sql_app/crud.py`: @@ -680,18 +692,18 @@ For example, in a background task worker with ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.8+" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + ## Check it You can copy this code and use it as is. @@ -739,18 +751,18 @@ A "middleware" is basically a function that is always executed for each request, The middleware we'll add (just a function) will create a new SQLAlchemy `SessionLocal` for each request, add it to the request and then close it once the request is finished. -=== "Python 3.6 and above" - - ```Python hl_lines="14-22" - {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="12-20" {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="14-22" + {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} + ``` + !!! info We put the creation of the `SessionLocal()` and handling of the requests in a `try` block. diff --git a/docs/en/docs/tutorial/static-files.md b/docs/en/docs/tutorial/static-files.md index 7a0c36af3f..311d2b1c8d 100644 --- a/docs/en/docs/tutorial/static-files.md +++ b/docs/en/docs/tutorial/static-files.md @@ -22,7 +22,7 @@ You can serve static files automatically from a directory using `StaticFiles`. This is different from using an `APIRouter` as a mounted application is completely independent. The OpenAPI and docs from your main application won't include anything from the mounted application, etc. -You can read more about this in the **Advanced User Guide**. +You can read more about this in the [Advanced User Guide](../advanced/index.md){.internal-link target=_blank}. ## Details diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index be07aab37d..3f8dd69a1d 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -110,18 +110,42 @@ It has a `POST` operation that could return several errors. Both *path operations* require an `X-Token` header. -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python - {!> ../../../docs_src/app_testing/app_b/main.py!} + {!> ../../../docs_src/app_testing/app_b_an_py310/main.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} + ``` + +=== "Python 3.8+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an/main.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python {!> ../../../docs_src/app_testing/app_b_py310/main.py!} ``` +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + ### Extended testing file You could then update `test_main.py` with the extended tests: diff --git a/docs/en/layouts/custom.yml b/docs/en/layouts/custom.yml new file mode 100644 index 0000000000..aad81af28e --- /dev/null +++ b/docs/en/layouts/custom.yml @@ -0,0 +1,228 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +# ----------------------------------------------------------------------------- +# Configuration +# ----------------------------------------------------------------------------- + +# The same default card with a a configurable logo + +# Definitions +definitions: + + # Background image + - &background_image >- + {{ layout.background_image or "" }} + + # Background color (default: indigo) + - &background_color >- + {%- if layout.background_color -%} + {{ layout.background_color }} + {%- else -%} + {%- set palette = config.theme.palette or {} -%} + {%- if not palette is mapping -%} + {%- set palette = palette | first -%} + {%- endif -%} + {%- set primary = palette.get("primary", "indigo") -%} + {%- set primary = primary.replace(" ", "-") -%} + {{ { + "red": "#ef5552", + "pink": "#e92063", + "purple": "#ab47bd", + "deep-purple": "#7e56c2", + "indigo": "#4051b5", + "blue": "#2094f3", + "light-blue": "#02a6f2", + "cyan": "#00bdd6", + "teal": "#009485", + "green": "#4cae4f", + "light-green": "#8bc34b", + "lime": "#cbdc38", + "yellow": "#ffec3d", + "amber": "#ffc105", + "orange": "#ffa724", + "deep-orange": "#ff6e42", + "brown": "#795649", + "grey": "#757575", + "blue-grey": "#546d78", + "black": "#000000", + "white": "#ffffff" + }[primary] or "#4051b5" }} + {%- endif -%} + + # Text color (default: white) + - &color >- + {%- if layout.color -%} + {{ layout.color }} + {%- else -%} + {%- set palette = config.theme.palette or {} -%} + {%- if not palette is mapping -%} + {%- set palette = palette | first -%} + {%- endif -%} + {%- set primary = palette.get("primary", "indigo") -%} + {%- set primary = primary.replace(" ", "-") -%} + {{ { + "red": "#ffffff", + "pink": "#ffffff", + "purple": "#ffffff", + "deep-purple": "#ffffff", + "indigo": "#ffffff", + "blue": "#ffffff", + "light-blue": "#ffffff", + "cyan": "#ffffff", + "teal": "#ffffff", + "green": "#ffffff", + "light-green": "#ffffff", + "lime": "#000000", + "yellow": "#000000", + "amber": "#000000", + "orange": "#000000", + "deep-orange": "#ffffff", + "brown": "#ffffff", + "grey": "#ffffff", + "blue-grey": "#ffffff", + "black": "#ffffff", + "white": "#000000" + }[primary] or "#ffffff" }} + {%- endif -%} + + # Font family (default: Roboto) + - &font_family >- + {%- if layout.font_family -%} + {{ layout.font_family }} + {%- elif config.theme.font != false -%} + {{ config.theme.font.get("text", "Roboto") }} + {%- else -%} + Roboto + {%- endif -%} + + # Site name + - &site_name >- + {{ config.site_name }} + + # Page title + - &page_title >- + {{ page.meta.get("title", page.title) }} + + # Page title with site name + - &page_title_with_site_name >- + {%- if not page.is_homepage -%} + {{ page.meta.get("title", page.title) }} - {{ config.site_name }} + {%- else -%} + {{ page.meta.get("title", page.title) }} + {%- endif -%} + + # Page description + - &page_description >- + {{ page.meta.get("description", config.site_description) or "" }} + + + # Start of custom modified logic + # Logo + - &logo >- + {%- if layout.logo -%} + {{ layout.logo }} + {%- elif config.theme.logo -%} + {{ config.docs_dir }}/{{ config.theme.logo }} + {%- endif -%} + # End of custom modified logic + + # Logo (icon) + - &logo_icon >- + {{ config.theme.icon.logo or "" }} + +# Meta tags +tags: + + # Open Graph + og:type: website + og:title: *page_title_with_site_name + og:description: *page_description + og:image: "{{ image.url }}" + og:image:type: "{{ image.type }}" + og:image:width: "{{ image.width }}" + og:image:height: "{{ image.height }}" + og:url: "{{ page.canonical_url }}" + + # Twitter + twitter:card: summary_large_image + twitter.title: *page_title_with_site_name + twitter:description: *page_description + twitter:image: "{{ image.url }}" + +# ----------------------------------------------------------------------------- +# Specification +# ----------------------------------------------------------------------------- + +# Card size and layers +size: { width: 1200, height: 630 } +layers: + + # Background + - background: + image: *background_image + color: *background_color + + # Logo + - size: { width: 144, height: 144 } + offset: { x: 992, y: 64 } + background: + image: *logo + icon: + value: *logo_icon + color: *color + + # Site name + - size: { width: 832, height: 42 } + offset: { x: 64, y: 64 } + typography: + content: *site_name + color: *color + font: + family: *font_family + style: Bold + + # Page title + - size: { width: 832, height: 310 } + offset: { x: 62, y: 160 } + typography: + content: *page_title + align: start + color: *color + line: + amount: 3 + height: 1.25 + font: + family: *font_family + style: Bold + + # Page description + - size: { width: 832, height: 64 } + offset: { x: 64, y: 512 } + typography: + content: *page_description + align: start + color: *color + line: + amount: 2 + height: 1.5 + font: + family: *font_family + style: Regular diff --git a/docs/en/mkdocs.insiders.yml b/docs/en/mkdocs.insiders.yml new file mode 100644 index 0000000000..d204974b80 --- /dev/null +++ b/docs/en/mkdocs.insiders.yml @@ -0,0 +1,7 @@ +plugins: + social: + cards_layout_dir: ../en/layouts + cards_layout: custom + cards_layout_options: + logo: ../en/docs/img/icon-white.svg + typeset: diff --git a/docs/en/mkdocs.maybe-insiders.yml b/docs/en/mkdocs.maybe-insiders.yml new file mode 100644 index 0000000000..37fd9338ef --- /dev/null +++ b/docs/en/mkdocs.maybe-insiders.yml @@ -0,0 +1,6 @@ +# Define this here and not in the main mkdocs.yml file because that one is auto +# updated and written, and the script would remove the env var +INHERIT: !ENV [INSIDERS_FILE, '../en/mkdocs.no-insiders.yml'] +markdown_extensions: + pymdownx.highlight: + linenums: !ENV [LINENUMS, false] diff --git a/docs/az/overrides/.gitignore b/docs/en/mkdocs.no-insiders.yml similarity index 100% rename from docs/az/overrides/.gitignore rename to docs/en/mkdocs.no-insiders.yml diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 69ad29624f..92d081aa12 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -1,9 +1,10 @@ +INHERIT: ../en/mkdocs.maybe-insiders.yml site_name: FastAPI site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production site_url: https://fastapi.tiangolo.com/ theme: name: material - custom_dir: overrides + custom_dir: ../en/overrides palette: - media: '(prefers-color-scheme: light)' scheme: default @@ -11,18 +12,25 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes + - content.tooltips + - navigation.path + - content.code.annotate + - content.code.copy + - content.code.select + - navigation.tabs icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg @@ -32,162 +40,214 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search -- markdownextradata: - data: data + search: null + markdownextradata: + data: ../en/data + redirects: + redirect_maps: + deployment/deta.md: deployment/cloud.md + advanced/sql-databases-peewee.md: how-to/sql-databases-peewee.md + advanced/async-sql-databases.md: how-to/async-sql-encode-databases.md + advanced/nosql-databases.md: how-to/nosql-databases-couchbase.md + advanced/graphql.md: how-to/graphql.md + advanced/custom-request-and-route.md: how-to/custom-request-and-route.md + advanced/conditional-openapi.md: how-to/conditional-openapi.md + advanced/extending-openapi.md: how-to/extending-openapi.md + mkdocstrings: + handlers: + python: + options: + extensions: + - griffe_typingdoc + show_root_heading: true + show_if_no_docstring: true + preload_modules: + - httpx + - starlette + inherited_members: true + members_order: source + separate_signature: true + unwrap_annotated: true + filters: + - '!^_' + merge_init_into_class: true + docstring_section_style: spacy + signature_crossrefs: true + show_symbol_type_heading: true + show_symbol_type_toc: true nav: - FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ - features.md +- Learn: + - learn/index.md + - python-types.md + - async.md + - Tutorial - User Guide: + - tutorial/index.md + - tutorial/first-steps.md + - tutorial/path-params.md + - tutorial/query-params.md + - tutorial/body.md + - tutorial/query-params-str-validations.md + - tutorial/path-params-numeric-validations.md + - tutorial/body-multiple-params.md + - tutorial/body-fields.md + - tutorial/body-nested-models.md + - tutorial/schema-extra-example.md + - tutorial/extra-data-types.md + - tutorial/cookie-params.md + - tutorial/header-params.md + - tutorial/response-model.md + - tutorial/extra-models.md + - tutorial/response-status-code.md + - tutorial/request-forms.md + - tutorial/request-files.md + - tutorial/request-forms-and-files.md + - tutorial/handling-errors.md + - tutorial/path-operation-configuration.md + - tutorial/encoder.md + - tutorial/body-updates.md + - Dependencies: + - tutorial/dependencies/index.md + - tutorial/dependencies/classes-as-dependencies.md + - tutorial/dependencies/sub-dependencies.md + - tutorial/dependencies/dependencies-in-path-operation-decorators.md + - tutorial/dependencies/global-dependencies.md + - tutorial/dependencies/dependencies-with-yield.md + - Security: + - tutorial/security/index.md + - tutorial/security/first-steps.md + - tutorial/security/get-current-user.md + - tutorial/security/simple-oauth2.md + - tutorial/security/oauth2-jwt.md + - tutorial/middleware.md + - tutorial/cors.md + - tutorial/sql-databases.md + - tutorial/bigger-applications.md + - tutorial/background-tasks.md + - tutorial/metadata.md + - tutorial/static-files.md + - tutorial/testing.md + - tutorial/debugging.md + - Advanced User Guide: + - advanced/index.md + - advanced/path-operation-advanced-configuration.md + - advanced/additional-status-codes.md + - advanced/response-directly.md + - advanced/custom-response.md + - advanced/additional-responses.md + - advanced/response-cookies.md + - advanced/response-headers.md + - advanced/response-change-status-code.md + - advanced/advanced-dependencies.md + - Advanced Security: + - advanced/security/index.md + - advanced/security/oauth2-scopes.md + - advanced/security/http-basic-auth.md + - advanced/using-request-directly.md + - advanced/dataclasses.md + - advanced/middleware.md + - advanced/sub-applications.md + - advanced/behind-a-proxy.md + - advanced/templates.md + - advanced/websockets.md + - advanced/events.md + - advanced/testing-websockets.md + - advanced/testing-events.md + - advanced/testing-dependencies.md + - advanced/testing-database.md + - advanced/async-tests.md + - advanced/settings.md + - advanced/openapi-callbacks.md + - advanced/openapi-webhooks.md + - advanced/wsgi.md + - advanced/generate-clients.md + - Deployment: + - deployment/index.md + - deployment/versions.md + - deployment/https.md + - deployment/manually.md + - deployment/concepts.md + - deployment/cloud.md + - deployment/server-workers.md + - deployment/docker.md + - How To - Recipes: + - how-to/index.md + - how-to/general.md + - how-to/sql-databases-peewee.md + - how-to/async-sql-encode-databases.md + - how-to/nosql-databases-couchbase.md + - how-to/graphql.md + - how-to/custom-request-and-route.md + - how-to/conditional-openapi.md + - how-to/extending-openapi.md + - how-to/separate-openapi-schemas.md + - how-to/custom-docs-ui-assets.md + - how-to/configure-swagger-ui.md +- Reference (Code API): + - reference/index.md + - reference/fastapi.md + - reference/parameters.md + - reference/status.md + - reference/uploadfile.md + - reference/exceptions.md + - reference/dependencies.md + - reference/apirouter.md + - reference/background.md + - reference/request.md + - reference/websockets.md + - reference/httpconnection.md + - reference/response.md + - reference/responses.md + - reference/middleware.md + - OpenAPI: + - reference/openapi/index.md + - reference/openapi/docs.md + - reference/openapi/models.md + - reference/security/index.md + - reference/encoders.md + - reference/staticfiles.md + - reference/templating.md + - reference/testclient.md - fastapi-people.md -- python-types.md -- Tutorial - User Guide: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/query-params-str-validations.md - - tutorial/path-params-numeric-validations.md - - tutorial/body-multiple-params.md - - tutorial/body-fields.md - - tutorial/body-nested-models.md - - tutorial/schema-extra-example.md - - tutorial/extra-data-types.md - - tutorial/cookie-params.md - - tutorial/header-params.md - - tutorial/response-model.md - - tutorial/extra-models.md - - tutorial/response-status-code.md - - tutorial/request-forms.md - - tutorial/request-files.md - - tutorial/request-forms-and-files.md - - tutorial/handling-errors.md - - tutorial/path-operation-configuration.md - - tutorial/encoder.md - - tutorial/body-updates.md - - Dependencies: - - tutorial/dependencies/index.md - - tutorial/dependencies/classes-as-dependencies.md - - tutorial/dependencies/sub-dependencies.md - - tutorial/dependencies/dependencies-in-path-operation-decorators.md - - tutorial/dependencies/global-dependencies.md - - tutorial/dependencies/dependencies-with-yield.md - - Security: - - tutorial/security/index.md - - tutorial/security/first-steps.md - - tutorial/security/get-current-user.md - - tutorial/security/simple-oauth2.md - - tutorial/security/oauth2-jwt.md - - tutorial/middleware.md - - tutorial/cors.md - - tutorial/sql-databases.md - - tutorial/bigger-applications.md - - tutorial/background-tasks.md - - tutorial/metadata.md - - tutorial/static-files.md - - tutorial/testing.md - - tutorial/debugging.md -- Advanced User Guide: - - advanced/index.md - - advanced/path-operation-advanced-configuration.md - - advanced/additional-status-codes.md - - advanced/response-directly.md - - advanced/custom-response.md - - advanced/additional-responses.md - - advanced/response-cookies.md - - advanced/response-headers.md - - advanced/response-change-status-code.md - - advanced/advanced-dependencies.md - - Advanced Security: - - advanced/security/index.md - - advanced/security/oauth2-scopes.md - - advanced/security/http-basic-auth.md - - advanced/using-request-directly.md - - advanced/dataclasses.md - - advanced/middleware.md - - advanced/sql-databases-peewee.md - - advanced/async-sql-databases.md - - advanced/nosql-databases.md - - advanced/sub-applications.md - - advanced/behind-a-proxy.md - - advanced/templates.md - - advanced/graphql.md - - advanced/websockets.md - - advanced/events.md - - advanced/custom-request-and-route.md - - advanced/testing-websockets.md - - advanced/testing-events.md - - advanced/testing-dependencies.md - - advanced/testing-database.md - - advanced/async-tests.md - - advanced/settings.md - - advanced/conditional-openapi.md - - advanced/extending-openapi.md - - advanced/openapi-callbacks.md - - advanced/wsgi.md - - advanced/generate-clients.md -- async.md -- Deployment: - - deployment/index.md - - deployment/versions.md - - deployment/https.md - - deployment/manually.md - - deployment/concepts.md - - deployment/deta.md - - deployment/server-workers.md - - deployment/docker.md -- project-generation.md -- alternatives.md -- history-design-future.md -- external-links.md -- benchmarks.md -- help-fastapi.md -- contributing.md +- Resources: + - resources/index.md + - project-generation.md + - external-links.md + - newsletter.md +- About: + - about/index.md + - alternatives.md + - history-design-future.md + - benchmarks.md +- Help: + - help/index.md + - help-fastapi.md + - contributing.md - release-notes.md markdown_extensions: -- toc: + toc: permalink: true -- markdown.extensions.codehilite: + markdown.extensions.codehilite: guess_lang: false -- mdx_include: + mdx_include: base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: + admonition: null + codehilite: null + extra: null + pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: + pymdownx.tabbed: alternate_style: true -- attr_list -- md_in_html + attr_list: null + md_in_html: null extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -206,47 +266,49 @@ extra: alternate: - link: / name: en - English - - link: /az/ - name: az - link: /de/ - name: de + name: de - Deutsch - link: /es/ name: es - español - link: /fa/ - name: fa + name: fa - فارسی - link: /fr/ name: fr - français - link: /he/ - name: he + name: he - עברית + - link: /hu/ + name: hu - magyar - link: /id/ - name: id - - link: /it/ - name: it - italiano + name: id - Bahasa Indonesia - link: /ja/ name: ja - 日本語 - link: /ko/ name: ko - 한국어 - - link: /nl/ - name: nl - link: /pl/ - name: pl + name: pl - Polski - link: /pt/ name: pt - português - link: /ru/ name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ name: uk - українська мова + - link: /ur/ + name: ur - اردو + - link: /vi/ + name: vi - Tiếng Việt + - link: /yo/ + name: yo - Yorùbá - link: /zh/ name: zh - 汉语 + - link: /em/ + name: 😉 extra_css: - css/termynal.css - css/custom.css extra_javascript: - js/termynal.js - js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index b85f0c4cfc..476b436767 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -22,12 +22,6 @@ diff --git a/docs/es/docs/about/index.md b/docs/es/docs/about/index.md new file mode 100644 index 0000000000..e83400a8dc --- /dev/null +++ b/docs/es/docs/about/index.md @@ -0,0 +1,3 @@ +# Acerca de + +Acerca de FastAPI, su diseño, inspiración y más. 🤓 diff --git a/docs/es/docs/advanced/additional-status-codes.md b/docs/es/docs/advanced/additional-status-codes.md index 1f28ea85b7..eaa3369ebe 100644 --- a/docs/es/docs/advanced/additional-status-codes.md +++ b/docs/es/docs/advanced/additional-status-codes.md @@ -23,7 +23,7 @@ Para conseguir esto importa `JSONResponse` y devuelve ahí directamente tu conte No será serializado con el modelo, etc. - Asegurate de que la respuesta tenga los datos que quieras, y que los valores sean JSON válidos (si estás usando `JSONResponse`). + Asegúrate de que la respuesta tenga los datos que quieras, y que los valores sean JSON válidos (si estás usando `JSONResponse`). !!! note "Detalles Técnicos" También podrías utilizar `from starlette.responses import JSONResponse`. diff --git a/docs/es/docs/advanced/index.md b/docs/es/docs/advanced/index.md index 1bee540f2b..ba1d20b0d1 100644 --- a/docs/es/docs/advanced/index.md +++ b/docs/es/docs/advanced/index.md @@ -1,4 +1,4 @@ -# Guía de Usuario Avanzada - Introducción +# Guía de Usuario Avanzada ## Características Adicionales diff --git a/docs/es/docs/advanced/response-directly.md b/docs/es/docs/advanced/response-directly.md index 54dadf5763..dee44ac08a 100644 --- a/docs/es/docs/advanced/response-directly.md +++ b/docs/es/docs/advanced/response-directly.md @@ -21,7 +21,7 @@ Y cuando devuelves una `Response`, **FastAPI** la pasará directamente. No hará ninguna conversión de datos con modelos Pydantic, no convertirá el contenido a ningún tipo, etc. -Esto te da mucha flexibilidad. Puedes devolver cualquier tipo de dato, sobrescribir cualquer declaración de datos o validación, etc. +Esto te da mucha flexibilidad. Puedes devolver cualquier tipo de dato, sobrescribir cualquier declaración de datos o validación, etc. ## Usando el `jsonable_encoder` en una `Response` diff --git a/docs/es/docs/async.md b/docs/es/docs/async.md index 90fd7b3d83..83dd532ee9 100644 --- a/docs/es/docs/async.md +++ b/docs/es/docs/async.md @@ -104,24 +104,40 @@ Para entender las diferencias, imagina la siguiente historia sobre hamburguesas: Vas con la persona que te gusta 😍 a pedir comida rápida 🍔, haces cola mientras el cajero 💁 recoge los pedidos de las personas de delante tuyo. +illustration + Llega tu turno, haces tu pedido de 2 hamburguesas impresionantes para esa persona 😍 y para ti. -Pagas 💸. +illustration El cajero 💁 le dice algo al chico de la cocina 👨‍🍳 para que sepa que tiene que preparar tus hamburguesas 🍔 (a pesar de que actualmente está preparando las de los clientes anteriores). +illustration + +Pagas 💸. El cajero 💁 te da el número de tu turno. +illustration + Mientras esperas, vas con esa persona 😍 y eliges una mesa, se sientan y hablan durante un rato largo (ya que las hamburguesas son muy impresionantes y necesitan un rato para prepararse ✨🍔✨). Mientras te sientas en la mesa con esa persona 😍, esperando las hamburguesas 🍔, puedes disfrutar ese tiempo admirando lo increíble, inteligente, y bien que se ve ✨😍✨. +illustration + Mientras esperas y hablas con esa persona 😍, de vez en cuando, verificas el número del mostrador para ver si ya es tu turno. Al final, en algún momento, llega tu turno. Vas al mostrador, coges tus hamburguesas 🍔 y vuelves a la mesa. +illustration + Tú y esa persona 😍 se comen las hamburguesas 🍔 y la pasan genial ✨. +illustration + +!!! info + Las ilustraciones fueron creados por Ketrina Thompson. 🎨 + --- Imagina que eres el sistema / programa 🤖 en esa historia. @@ -150,26 +166,41 @@ Haces la cola mientras varios cajeros (digamos 8) que a la vez son cocineros Todos los que están antes de ti están esperando 🕙 que sus hamburguesas 🍔 estén listas antes de dejar el mostrador porque cada uno de los 8 cajeros prepara la hamburguesa de inmediato antes de recibir el siguiente pedido. +illustration + Entonces finalmente es tu turno, haces tu pedido de 2 hamburguesas 🍔 impresionantes para esa persona 😍 y para ti. Pagas 💸. +illustration + El cajero va a la cocina 👨‍🍳. Esperas, de pie frente al mostrador 🕙, para que nadie más recoja tus hamburguesas 🍔, ya que no hay números para los turnos. +illustration + Como tu y esa persona 😍 están ocupados en impedir que alguien se ponga delante y recoja tus hamburguesas apenas llegan 🕙, tampoco puedes prestarle atención a esa persona 😞. Este es un trabajo "síncrono", estás "sincronizado" con el cajero / cocinero 👨‍🍳. Tienes que esperar y estar allí en el momento exacto en que el cajero / cocinero 👨‍🍳 termina las hamburguesas 🍔 y te las da, o de lo contrario, alguien más podría cogerlas. +illustration + Luego, el cajero / cocinero 👨‍🍳 finalmente regresa con tus hamburguesas 🍔, después de mucho tiempo esperando 🕙 frente al mostrador. +illustration + Cojes tus hamburguesas 🍔 y vas a la mesa con esa persona 😍. Sólo las comes y listo 🍔 ⏹. +illustration + No has hablado ni coqueteado mucho, ya que has pasado la mayor parte del tiempo esperando 🕙 frente al mostrador 😞. +!!! info + Las ilustraciones fueron creados por Ketrina Thompson. 🎨 + --- En este escenario de las hamburguesas paralelas, tú eres un sistema / programa 🤖 con dos procesadores (tú y la persona que te gusta 😍), ambos esperando 🕙 y dedicando su atención ⏯ a estar "esperando en el mostrador" 🕙 durante mucho tiempo. @@ -240,7 +271,7 @@ Pero en este caso, si pudieras traer a los 8 ex cajeros / cocineros / ahora limp En este escenario, cada uno de los limpiadores (incluido tú) sería un procesador, haciendo su parte del trabajo. -Y como la mayor parte del tiempo de ejecución lo coge el trabajo real (en lugar de esperar), y el trabajo en un sistema lo realiza una CPU , a estos problemas se les llama "CPU bond". +Y como la mayor parte del tiempo de ejecución lo coge el trabajo real (en lugar de esperar), y el trabajo en un sistema lo realiza una CPU , a estos problemas se les llama "CPU bound". --- @@ -257,7 +288,7 @@ Por ejemplo: Con **FastAPI** puedes aprovechar la concurrencia que es muy común para el desarrollo web (atractivo principal de NodeJS). -Pero también puedes aprovechar los beneficios del paralelismo y el multiprocesamiento (tener múltiples procesos ejecutándose en paralelo) para cargas de trabajo **CPU bond** como las de los sistemas de Machine Learning. +Pero también puedes aprovechar los beneficios del paralelismo y el multiprocesamiento (tener múltiples procesos ejecutándose en paralelo) para cargas de trabajo **CPU bound** como las de los sistemas de Machine Learning. Eso, más el simple hecho de que Python es el lenguaje principal para **Data Science**, Machine Learning y especialmente Deep Learning, hacen de FastAPI una muy buena combinación para las API y aplicaciones web de Data Science / Machine Learning (entre muchas otras). diff --git a/docs/es/docs/features.md b/docs/es/docs/features.md index 5d6b6509a7..d68791d635 100644 --- a/docs/es/docs/features.md +++ b/docs/es/docs/features.md @@ -13,7 +13,7 @@ ### Documentación automática -Documentación interactiva de la API e interfaces web de exploración. Hay múltiples opciones, dos incluídas por defecto, porque el framework está basado en OpenAPI. +Documentación interactiva de la API e interfaces web de exploración. Hay múltiples opciones, dos incluidas por defecto, porque el framework está basado en OpenAPI. * Swagger UI, con exploración interactiva, llama y prueba tu API directamente desde tu navegador. @@ -25,7 +25,7 @@ Documentación interactiva de la API e interfaces web de exploración. Hay múlt ### Simplemente Python moderno -Todo está basado en las declaraciones de tipo de **Python 3.6** estándar (gracias a Pydantic). No necesitas aprender una sintáxis nueva, solo Python moderno. +Todo está basado en las declaraciones de tipo de **Python 3.8** estándar (gracias a Pydantic). No necesitas aprender una sintaxis nueva, solo Python moderno. Si necesitas un repaso de 2 minutos de cómo usar los tipos de Python (así no uses FastAPI) prueba el tutorial corto: [Python Types](python-types.md){.internal-link target=_blank}. @@ -72,9 +72,9 @@ my_second_user: User = User(**second_user_data) El framework fue diseñado en su totalidad para ser fácil e intuitivo de usar. Todas las decisiones fueron probadas en múltiples editores antes de comenzar el desarrollo para asegurar la mejor experiencia de desarrollo. -En la última encuesta a desarrolladores de Python fue claro que la característica más usada es el "autocompletado". +En la última encuesta a desarrolladores de Python fue claro que la característica más usada es el "auto-completado". -El framework **FastAPI** está creado para satisfacer eso. El autocompletado funciona en todas partes. +El framework **FastAPI** está creado para satisfacer eso. El auto-completado funciona en todas partes. No vas a tener que volver a la documentación seguido. @@ -140,13 +140,13 @@ FastAPI incluye un sistema de + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + + + +صفحه مستندات را در آدرس http://127.0.0.1:8000/docs باز کنید. + +اسناد API خودکار برنامه اصلی را مشاهده خواهید کرد که فقط شامل path operations خود می شود: + + + +و سپس اسناد زیر برنامه را در آدرس http://127.0.0.1:8000/subapi/docs. باز کنید. + +اسناد API خودکار برای زیر برنامه را خواهید دید، که فقط شامل path operations خود می شود، همه در زیر مسیر `/subapi` قرار دارند: + + + +اگر سعی کنید با هر یک از این دو رابط کاربری تعامل داشته باشید، آنها به درستی کار می کنند، زیرا مرورگر می تواند با هر یک از برنامه ها یا زیر برنامه های خاص صحبت کند. + +### جرئیات فنی : `root_path` + +هنگامی که یک زیر برنامه را همانطور که در بالا توضیح داده شد متصل می کنید, FastAPI با استفاده از مکانیزمی از مشخصات ASGI به نام `root_path` ارتباط مسیر mount را برای زیر برنامه انجام می دهد. + +به این ترتیب، زیر برنامه می داند که از آن پیشوند مسیر برای رابط کاربری اسناد (docs UI) استفاده کند. + +و زیر برنامه ها نیز می تواند زیر برنامه های متصل شده خود را داشته باشد و همه چیز به درستی کار کند، زیرا FastAPI تمام این مسیرهای `root_path` را به طور خودکار مدیریت می کند. + +در بخش [پشت پراکسی](./behind-a-proxy.md){.internal-link target=_blank}. درباره `root_path` و نحوه استفاده درست از آن بیشتر خواهید آموخت. diff --git a/docs/fa/docs/features.md b/docs/fa/docs/features.md new file mode 100644 index 0000000000..3040ce3dd9 --- /dev/null +++ b/docs/fa/docs/features.md @@ -0,0 +1,206 @@ +# ویژگی ها + +## ویژگی های FastAPI + +**FastAPI** موارد زیر را به شما ارائه میدهد: + +### برپایه استاندارد های باز + +* OpenAPI برای ساخت API, شامل مشخص سازی path operation ها, پارامترها, body request ها, امنیت و غیره. +* مستندسازی خودکار data model با JSON Schema (همانطور که OpenAPI خود نیز مبتنی بر JSON Schema است). +* طراحی شده بر اساس استاندارد هایی که پس از یک مطالعه دقیق بدست آمده اند بجای طرحی ناپخته و بدون فکر. +* همچنین به شما اجازه میدهد تا از تولید خودکار client code در بسیاری از زبان ها استفاده کنید. + +### مستندات خودکار + +مستندات API تعاملی و ایجاد رابط کاربری وب. از آنجایی که این فریم ورک برپایه OpenAPI میباشد، آپشن های متعددی وجود دارد که ۲ مورد بصورت پیش فرض گنجانده شده اند. + +* Swagger UI، با کاوش تعاملی، API خود را مستقیما از طریق مرورگر صدازده و تست کنید. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* مستندات API جایگزین با ReDoc. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### فقط پایتون مدرن + +همه اینها برپایه type declaration های **پایتون ۳.۶** استاندارد (به لطف Pydantic) میباشند. سینتکس جدیدی درکار نیست. تنها پایتون مدرن استاندارد. + +اگر به یک یادآوری ۲ دقیقه ای در مورد نحوه استفاده از تایپ های پایتون دارید (حتی اگر از FastAPI استفاده نمیکنید) این آموزش کوتاه را بررسی کنید: [Python Types](python-types.md){.internal-link target=\_blank}. + +شما پایتون استاندارد را با استفاده از تایپ ها مینویسید: + +```Python +from datetime import date + +from pydantic import BaseModel + +# Declare a variable as a str +# and get editor support inside the function +def main(user_id: str): + return user_id + + +# A Pydantic model +class User(BaseModel): + id: int + name: str + joined: date +``` + +که سپس میتوان به این شکل از آن استفاده کرد: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +!!! info + `**second_user_data` یعنی: + + کلید ها و مقادیر دیکشنری `second_user_data` را مستقیما به عنوان ارگومان های key-value بفرست، که معادل است با : `User(id=4, name="Mary", joined="2018-11-30")` + +### پشتیبانی ویرایشگر + +تمام فریم ورک به گونه ای طراحی شده که استفاده از آن آسان و شهودی باشد، تمام تصمیمات حتی قبل از شروع توسعه بر روی چندین ویرایشگر آزمایش شده اند، تا از بهترین تجربه توسعه اطمینان حاصل شود. + +در آخرین نظرسنجی توسعه دهندگان پایتون کاملا مشخص بود که بیشترین ویژگی مورد استفاده از "تکمیل خودکار" است. + +تمام فریم ورک **FastAPI** برپایه ای برای براورده کردن این نیاز نیز ایجاد گشته است. تکمیل خودکار در همه جا کار میکند. + +شما به ندرت نیاز به بازگشت به مستندات را خواهید داشت. + +ببینید که چگونه ویرایشگر شما ممکن است به شما کمک کند: + +* در Visual Studio Code: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* در PyCharm: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +شما پیشنهاد های تکمیل خودکاری را خواهید گرفت که حتی ممکن است قبلا آن را غیرممکن تصور میکردید. به عنوان مثال کلید `price` در داخل بدنه JSON (که میتوانست تودرتو نیز باشد) که از یک درخواست آمده است. + +دیگر خبری از تایپ کلید اشتباهی، برگشتن به مستندات یا پایین بالا رفتن برای فهمیدن اینکه شما از `username` یا `user_name` استفاده کرده اید نیست. + +### مختصر + +FastAPI **پیش فرض** های معقولی برای همه چیز دارد، با قابلیت تنظیمات اختیاری در همه جا. تمام پارامترها را میتوانید برای انجام انچه نیاز دارید و برای تعریف API مورد نیاز خود به خوبی تنظیم کنید. + +اما به طور پیش فرض، همه چیز **کار میکند**. + +### اعتبارسنجی + +* اعتبارسنجی برای بیشتر (یا همه؟) **data type** های پایتون، شامل: + + * JSON objects (`dict`) + * آرایه های (‍‍‍‍`list`) JSON با قابلیت مشخص سازی تایپ ایتم های درون لیست. + * فیلد های رشته (`str`)، به همراه مشخص سازی حداقل و حداکثر طول رشته. + * اعداد (‍‍`int`,`float`) با حداقل و حداکثر مقدار و غیره. + +* اعتبارسنجی برای تایپ های عجیب تر، مثل: + * URL. + * Email. + * UUID. + * و غیره. + +تمام اعتبارسنجی ها توسط کتابخانه اثبات شده و قدرتمند **Pydantic** انجام میشود. + +### امنیت و احراز هویت + +امنیت و احرازهویت بدون هیچگونه ارتباط و مصالحه ای با پایگاه های داده یا مدل های داده ایجاد شده اند. + +تمام طرح های امنیتی در OpenAPI تعریف شده اند، از جمله: + +* . +* **OAuth2** (همچنین با **JWT tokens**). آموزش را در [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=\_blank} مشاهده کنید. +* کلید های API: + * Headers + * Query parameters + * Cookies، و غیره. + +به علاوه تمام ویژگی های امنیتی از **Statlette** (شامل **session cookies**) + +همه اینها به عنوان ابزارها و اجزای قابل استفاده ای ساخته شده اند که به راحتی با سیستم های شما، مخازن داده، پایگاه های داده رابطه ای و NoSQL و غیره ادغام میشوند. + +### Dependency Injection + +FastAPI شامل یک سیستم Dependency Injection بسیار آسان اما بسیار قدرتمند است. + +* حتی وابستگی ها نیز میتوانند وابستگی هایی داشته باشند و یک سلسله مراتب یا **"گرافی" از وابستگی ها** ایجاد کنند. + +* همه چیز توسط فریم ورک **به طور خودکار اداره میشود** + +* همه وابستگی ها میتوانند به داده های request ها نیاز داشته باشند و مستندات خودکار و محدودیت های path operation را **افزایش** دهند. + +* با قابلیت **اعتبارسنجی خودکار** حتی برای path operation parameter های تعریف شده در وابستگی ها. + +* پشتیبانی از سیستم های پیچیده احرازهویت کاربر، **اتصالات پایگاه داده** و غیره. + +* بدون هیچ ارتباطی با دیتابیس ها، فرانت اند و غیره. اما ادغام آسان و راحت با همه آنها. + +### پلاگین های نامحدود + +یا به عبارت دیگر، هیچ نیازی به آنها نیست، کد موردنیاز خود را وارد و استفاده کنید. + +هر یکپارچه سازی به گونه ای طراحی شده است که استفاده از آن بسیار ساده باشد (با وابستگی ها) که میتوانید با استفاده از همان ساختار و روشی که برای _path operation_ های خود استفاده کرده اید تنها در ۲ خط کد "پلاگین" برنامه خودتان را ایجاد کنید. + +### تست شده + +* 100% پوشش تست. + +* 100% کد بر اساس type annotate ها. + +* استفاده شده در اپلیکیشن های تولید + +## ویژگی های Starlette + +**FastAPI** کاملا (و براساس) با Starlette سازگار است. بنابراین، هرکد اضافی Starlette که دارید، نیز کار خواهد کرد. + +‍‍`FastAPI` در واقع یک زیرکلاس از `Starlette` است. بنابراین اگر از قبل Starlette را میشناسید یا با آن کار کرده اید، بیشتر قابلیت ها به همین روش کار خواهد کرد. + +با **FastAPI** شما تمام ویژگی های **Starlette** را خواهید داشت (زیرا FastAPI یک نسخه و نمونه به تمام معنا از Starlette است): + +* عملکرد به طورجدی چشمگیر. این یکی از سریعترین فریم ورک های موجود در پایتون است که همتراز با **نود جی اس** و **گو** است. +* پشتیبانی از **WebSocket**. +* تسک های درجریان در پس زمینه. +* رویداد های راه اندازی و متوفق شدن. +* تست کلاینت ساخته شده به روی HTTPX. +* **CORS**, GZip, فایل های استاتیک, پاسخ های جریانی. +* پشتیبانی از **نشست ها و کوکی ها**. +* 100% پوشش با تست. +* 100% کد براساس type annotate ها. + +## ویژگی های Pydantic + +**FastAPI** کاملا (و براساس) با Pydantic سازگار است. بنابراین هرکد Pydantic اضافی که داشته باشید، نیز کار خواهد کرد. + +از جمله کتابخانه های خارجی نیز مبتنی بر Pydantic میتوان به ORM و ODM ها برای دیتابیس ها اشاره کرد. + +این همچنین به این معناست که در خیلی از موارد میتوانید همان ابجکتی که از request میگیرید را **مستقیما به دیتابیس** بفرستید زیرا همه چیز به طور خودکار تأیید میشود. + +همین امر برعکس نیز صدق می‌کند، در بسیاری از موارد شما می‌توانید ابجکتی را که از پایگاه داده دریافت می‌کنید را **مستقیماً به کاربر** ارسال کنید. + +با FastAPI شما تمام ویژگی های Pydantic را دراختیار دارید (زیرا FastAPI برای تمام بخش مدیریت دیتا بر اساس Pydantic عمل میکند): + +* **خبری از گیج شدن نیست**: + * هیچ زبان خردی برای یادگیری تعریف طرحواره های جدید وجود ندارد. + * اگر تایپ های پایتون را میشناسید، نحوه استفاده از Pydantic را نیز میدانید. +* به خوبی با **IDE/linter/مغز** شما عمل میکند: + * به این دلیل که ساختار داده Pydantic فقط نمونه هایی از کلاس هایی هستند که شما تعریف میکنید، تکمیل خودکار، mypy، linting و مشاهده شما باید به درستی با داده های معتبر شما کار کنند. +* اعتبار سنجی **ساختارهای پیچیده**: + * استفاده از مدل های سلسله مراتبی Pydantic, `List` و `Dict` کتابخانه `typing` پایتون و غیره. + * و اعتبارسنج ها اجازه میدهند که طرحواره های داده پیچیده به طور واضح و آسان تعریف، بررسی و بر پایه JSON مستند شوند. + * شما میتوانید ابجکت های عمیقا تودرتو JSON را که همگی تایید شده و annotated شده اند را داشته باشید. +* **قابل توسعه**: + * Pydantic اجازه میدهد تا data type های سفارشی تعریف شوند یا میتوانید اعتبارسنجی را با روش هایی به روی مدل ها با validator decorator گسترش دهید. +* 100% پوشش با تست. diff --git a/docs/fa/docs/index.md b/docs/fa/docs/index.md index dfc4d24e33..2480843891 100644 --- a/docs/fa/docs/index.md +++ b/docs/fa/docs/index.md @@ -143,7 +143,7 @@ $ pip install "uvicorn[standard]" * فایلی به نام `main.py` با محتوای زیر ایجاد کنید : ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -436,7 +436,6 @@ item: Item استفاده شده توسط Pydantic: -* ujson - برای "تجزیه (parse)" سریع‌تر JSON . * email_validator - برای اعتبارسنجی آدرس‌های ایمیل. استفاده شده توسط Starlette: diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 7c2fe5eab8..de18856f44 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -1,145 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/fa/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: fa -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs/fr/docs/advanced/index.md b/docs/fr/docs/advanced/index.md new file mode 100644 index 0000000000..f4fa5ecf69 --- /dev/null +++ b/docs/fr/docs/advanced/index.md @@ -0,0 +1,24 @@ +# Guide de l'utilisateur avancé + +## Caractéristiques supplémentaires + +Le [Tutoriel - Guide de l'utilisateur](../tutorial/){.internal-link target=_blank} devrait suffire à vous faire découvrir toutes les fonctionnalités principales de **FastAPI**. + +Dans les sections suivantes, vous verrez des options, configurations et fonctionnalités supplémentaires. + +!!! Note + Les sections de ce chapitre ne sont **pas nécessairement "avancées"**. + + Et il est possible que pour votre cas d'utilisation, la solution se trouve dans l'un d'entre eux. + +## Lisez d'abord le didacticiel + +Vous pouvez utiliser la plupart des fonctionnalités de **FastAPI** grâce aux connaissances du [Tutoriel - Guide de l'utilisateur](../tutorial/){.internal-link target=_blank}. + +Et les sections suivantes supposent que vous l'avez lu et que vous en connaissez les idées principales. + +## Cours TestDriven.io + +Si vous souhaitez suivre un cours pour débutants avancés pour compléter cette section de la documentation, vous pouvez consulter : Développement piloté par les tests avec FastAPI et Docker par **TestDriven.io**. + +10 % de tous les bénéfices de ce cours sont reversés au développement de **FastAPI**. 🎉 😄 diff --git a/docs/fr/docs/advanced/path-operation-advanced-configuration.md b/docs/fr/docs/advanced/path-operation-advanced-configuration.md new file mode 100644 index 0000000000..7ded97ce17 --- /dev/null +++ b/docs/fr/docs/advanced/path-operation-advanced-configuration.md @@ -0,0 +1,168 @@ +# Configuration avancée des paramètres de chemin + +## ID d'opération OpenAPI + +!!! Attention + Si vous n'êtes pas un "expert" en OpenAPI, vous n'en avez probablement pas besoin. + +Dans OpenAPI, les chemins sont des ressources, tels que /users/ ou /items/, exposées par votre API, et les opérations sont les méthodes HTTP utilisées pour manipuler ces chemins, telles que GET, POST ou DELETE. Les operationId sont des chaînes uniques facultatives utilisées pour identifier une opération d'un chemin. Vous pouvez définir l'OpenAPI `operationId` à utiliser dans votre *opération de chemin* avec le paramètre `operation_id`. + +Vous devez vous assurer qu'il est unique pour chaque opération. + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial001.py!} +``` + +### Utilisation du nom *path operation function* comme operationId + +Si vous souhaitez utiliser les noms de fonction de vos API comme `operationId`, vous pouvez les parcourir tous et remplacer chaque `operation_id` de l'*opération de chemin* en utilisant leur `APIRoute.name`. + +Vous devriez le faire après avoir ajouté toutes vos *paramètres de chemin*. + +```Python hl_lines="2 12-21 24" +{!../../../docs_src/path_operation_advanced_configuration/tutorial002.py!} +``` + +!!! Astuce + Si vous appelez manuellement `app.openapi()`, vous devez mettre à jour les `operationId` avant. + +!!! Attention + Pour faire cela, vous devez vous assurer que chacun de vos *chemin* ait un nom unique. + + Même s'ils se trouvent dans des modules différents (fichiers Python). + +## Exclusion d'OpenAPI + +Pour exclure un *chemin* du schéma OpenAPI généré (et donc des systèmes de documentation automatiques), utilisez le paramètre `include_in_schema` et assignez-lui la valeur `False` : + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial003.py!} +``` + +## Description avancée de docstring + +Vous pouvez limiter le texte utilisé de la docstring d'une *fonction de chemin* qui sera affiché sur OpenAPI. + +L'ajout d'un `\f` (un caractère d'échappement "form feed") va permettre à **FastAPI** de tronquer la sortie utilisée pour OpenAPI à ce stade. + +Il n'apparaîtra pas dans la documentation, mais d'autres outils (tel que Sphinx) pourront utiliser le reste. + +```Python hl_lines="19-29" +{!../../../docs_src/path_operation_advanced_configuration/tutorial004.py!} +``` + +## Réponses supplémentaires + +Vous avez probablement vu comment déclarer le `response_model` et le `status_code` pour une *opération de chemin*. + +Cela définit les métadonnées sur la réponse principale d'une *opération de chemin*. + +Vous pouvez également déclarer des réponses supplémentaires avec leurs modèles, codes de statut, etc. + +Il y a un chapitre entier ici dans la documentation à ce sujet, vous pouvez le lire sur [Réponses supplémentaires dans OpenAPI](./additional-responses.md){.internal-link target=_blank}. + +## OpenAPI supplémentaire + +Lorsque vous déclarez un *chemin* dans votre application, **FastAPI** génère automatiquement les métadonnées concernant ce *chemin* à inclure dans le schéma OpenAPI. + +!!! note "Détails techniques" + La spécification OpenAPI appelle ces métadonnées des Objets d'opération. + +Il contient toutes les informations sur le *chemin* et est utilisé pour générer automatiquement la documentation. + +Il inclut les `tags`, `parameters`, `requestBody`, `responses`, etc. + +Ce schéma OpenAPI spécifique aux *operations* est normalement généré automatiquement par **FastAPI**, mais vous pouvez également l'étendre. + +!!! Astuce + Si vous avez seulement besoin de déclarer des réponses supplémentaires, un moyen plus pratique de le faire est d'utiliser les [réponses supplémentaires dans OpenAPI](./additional-responses.md){.internal-link target=_blank}. + +Vous pouvez étendre le schéma OpenAPI pour une *opération de chemin* en utilisant le paramètre `openapi_extra`. + +### Extensions OpenAPI + +Cet `openapi_extra` peut être utile, par exemple, pour déclarer [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions) : + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial005.py!} +``` + +Si vous ouvrez la documentation automatique de l'API, votre extension apparaîtra au bas du *chemin* spécifique. + + + +Et dans le fichier openapi généré (`/openapi.json`), vous verrez également votre extension dans le cadre du *chemin* spécifique : + +```JSON hl_lines="22" +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "x-aperture-labs-portal": "blue" + } + } + } +} +``` + +### Personnalisation du Schéma OpenAPI pour un chemin + +Le dictionnaire contenu dans la variable `openapi_extra` sera fusionné avec le schéma OpenAPI généré automatiquement pour l'*opération de chemin*. + +Ainsi, vous pouvez ajouter des données supplémentaires au schéma généré automatiquement. + +Par exemple, vous pouvez décider de lire et de valider la requête avec votre propre code, sans utiliser les fonctionnalités automatiques de validation proposée par Pydantic, mais vous pouvez toujours définir la requête dans le schéma OpenAPI. + +Vous pouvez le faire avec `openapi_extra` : + +```Python hl_lines="20-37 39-40" +{!../../../docs_src/path_operation_advanced_configuration/tutorial006.py !} +``` + +Dans cet exemple, nous n'avons déclaré aucun modèle Pydantic. En fait, le corps de la requête n'est même pas parsé en tant que JSON, il est lu directement en tant que `bytes`, et la fonction `magic_data_reader()` serait chargé de l'analyser d'une manière ou d'une autre. + +Néanmoins, nous pouvons déclarer le schéma attendu pour le corps de la requête. + +### Type de contenu OpenAPI personnalisé + +En utilisant cette même astuce, vous pouvez utiliser un modèle Pydantic pour définir le schéma JSON qui est ensuite inclus dans la section de schéma OpenAPI personnalisée pour le *chemin* concerné. + +Et vous pouvez le faire même si le type de données dans la requête n'est pas au format JSON. + +Dans cet exemple, nous n'utilisons pas les fonctionnalités de FastAPI pour extraire le schéma JSON des modèles Pydantic ni la validation automatique pour JSON. En fait, nous déclarons le type de contenu de la requête en tant que YAML, et non JSON : + +```Python hl_lines="17-22 24" +{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} +``` + +Néanmoins, bien que nous n'utilisions pas la fonctionnalité par défaut, nous utilisons toujours un modèle Pydantic pour générer manuellement le schéma JSON pour les données que nous souhaitons recevoir en YAML. + +Ensuite, nous utilisons directement la requête et extrayons son contenu en tant qu'octets. Cela signifie que FastAPI n'essaiera même pas d'analyser le payload de la requête en tant que JSON. + +Et nous analysons directement ce contenu YAML, puis nous utilisons à nouveau le même modèle Pydantic pour valider le contenu YAML : + +```Python hl_lines="26-33" +{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} +``` + +!!! Astuce + Ici, nous réutilisons le même modèle Pydantic. + + Mais nous aurions pu tout aussi bien pu le valider d'une autre manière. diff --git a/docs/fr/docs/advanced/response-directly.md b/docs/fr/docs/advanced/response-directly.md new file mode 100644 index 0000000000..1c923fb82c --- /dev/null +++ b/docs/fr/docs/advanced/response-directly.md @@ -0,0 +1,63 @@ +# Renvoyer directement une réponse + +Lorsque vous créez une *opération de chemins* **FastAPI**, vous pouvez normalement retourner n'importe quelle donnée : un `dict`, une `list`, un modèle Pydantic, un modèle de base de données, etc. + +Par défaut, **FastAPI** convertirait automatiquement cette valeur de retour en JSON en utilisant le `jsonable_encoder` expliqué dans [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}. + +Ensuite, en arrière-plan, il mettra ces données JSON-compatible (par exemple un `dict`) à l'intérieur d'un `JSONResponse` qui sera utilisé pour envoyer la réponse au client. + +Mais vous pouvez retourner une `JSONResponse` directement à partir de vos *opérations de chemin*. + +Cela peut être utile, par exemple, pour retourner des en-têtes personnalisés ou des cookies. + +## Renvoyer une `Response` + +En fait, vous pouvez retourner n'importe quelle `Response` ou n'importe quelle sous-classe de celle-ci. + +!!! Note + `JSONResponse` est elle-même une sous-classe de `Response`. + +Et quand vous retournez une `Response`, **FastAPI** la transmet directement. + +Elle ne fera aucune conversion de données avec les modèles Pydantic, elle ne convertira pas le contenu en un type quelconque. + +Cela vous donne beaucoup de flexibilité. Vous pouvez retourner n'importe quel type de données, surcharger n'importe quelle déclaration ou validation de données. + +## Utiliser le `jsonable_encoder` dans une `Response` + +Parce que **FastAPI** n'apporte aucune modification à une `Response` que vous retournez, vous devez vous assurer que son contenu est prêt à être utilisé (sérialisable). + +Par exemple, vous ne pouvez pas mettre un modèle Pydantic dans une `JSONResponse` sans d'abord le convertir en un `dict` avec tous les types de données (comme `datetime`, `UUID`, etc.) convertis en types compatibles avec JSON. + +Pour ces cas, vous pouvez spécifier un appel à `jsonable_encoder` pour convertir vos données avant de les passer à une réponse : + +```Python hl_lines="6-7 21-22" +{!../../../docs_src/response_directly/tutorial001.py!} +``` + +!!! note "Détails techniques" + Vous pouvez aussi utiliser `from starlette.responses import JSONResponse`. + + **FastAPI** fournit le même objet `starlette.responses` que `fastapi.responses` juste par commodité pour le développeur. Mais la plupart des réponses disponibles proviennent directement de Starlette. + +## Renvoyer une `Response` personnalisée + +L'exemple ci-dessus montre toutes les parties dont vous avez besoin, mais il n'est pas encore très utile, car vous auriez pu retourner l'`item` directement, et **FastAPI** l'aurait mis dans une `JSONResponse` pour vous, en le convertissant en `dict`, etc. Tout cela par défaut. + +Maintenant, voyons comment vous pourriez utiliser cela pour retourner une réponse personnalisée. + +Disons que vous voulez retourner une réponse XML. + +Vous pouvez mettre votre contenu XML dans une chaîne de caractères, la placer dans une `Response`, et la retourner : + +```Python hl_lines="1 18" +{!../../../docs_src/response_directly/tutorial002.py!} +``` + +## Notes + +Lorsque vous renvoyez une `Response` directement, ses données ne sont pas validées, converties (sérialisées), ni documentées automatiquement. + +Mais vous pouvez toujours les documenter comme décrit dans [Additional Responses in OpenAPI](additional-responses.md){.internal-link target=_blank}. + +Vous pouvez voir dans les sections suivantes comment utiliser/déclarer ces `Response`s personnalisées tout en conservant la conversion automatique des données, la documentation, etc. diff --git a/docs/fr/docs/alternatives.md b/docs/fr/docs/alternatives.md index ee20438c3d..8e58a3dfa9 100644 --- a/docs/fr/docs/alternatives.md +++ b/docs/fr/docs/alternatives.md @@ -387,7 +387,7 @@ Gérer toute la validation des données, leur sérialisation et la documentation ### Starlette -Starlette est un framework/toolkit léger ASGI, qui est idéal pour construire des services asyncio performants. +Starlette est un framework/toolkit léger ASGI, qui est idéal pour construire des services asyncio performants. Il est très simple et intuitif. Il est conçu pour être facilement extensible et avoir des composants modulaires. diff --git a/docs/fr/docs/async.md b/docs/fr/docs/async.md index db88c4663c..af4d6ca060 100644 --- a/docs/fr/docs/async.md +++ b/docs/fr/docs/async.md @@ -250,7 +250,7 @@ Par exemple : ### Concurrence + Parallélisme : Web + Machine Learning -Avec **FastAPI** vous pouvez bénéficier de la concurrence qui est très courante en developement web (c'est l'attrait principal de NodeJS). +Avec **FastAPI** vous pouvez bénéficier de la concurrence qui est très courante en développement web (c'est l'attrait principal de NodeJS). Mais vous pouvez aussi profiter du parallélisme et multiprocessing afin de gérer des charges **CPU bound** qui sont récurrentes dans les systèmes de *Machine Learning*. diff --git a/docs/fr/docs/benchmarks.md b/docs/fr/docs/benchmarks.md new file mode 100644 index 0000000000..d33c263a21 --- /dev/null +++ b/docs/fr/docs/benchmarks.md @@ -0,0 +1,34 @@ +# Test de performance + +Les tests de performance de TechEmpower montrent que les applications **FastAPI** tournant sous Uvicorn comme étant l'un des frameworks Python les plus rapides disponibles, seulement inférieur à Starlette et Uvicorn (tous deux utilisés au cœur de FastAPI). (*) + +Mais en prêtant attention aux tests de performance et aux comparaisons, il faut tenir compte de ce qu'il suit. + +## Tests de performance et rapidité + +Lorsque vous vérifiez les tests de performance, il est commun de voir plusieurs outils de différents types comparés comme équivalents. + +En particulier, on voit Uvicorn, Starlette et FastAPI comparés (parmi de nombreux autres outils). + +Plus le problème résolu par un outil est simple, mieux seront les performances obtenues. Et la plupart des tests de performance ne prennent pas en compte les fonctionnalités additionnelles fournies par les outils. + +La hiérarchie est la suivante : + +* **Uvicorn** : un serveur ASGI + * **Starlette** : (utilise Uvicorn) un micro-framework web + * **FastAPI**: (utilise Starlette) un micro-framework pour API disposant de fonctionnalités additionnelles pour la création d'API, avec la validation des données, etc. + +* **Uvicorn** : + * A les meilleures performances, étant donné qu'il n'a pas beaucoup de code mis-à-part le serveur en lui-même. + * On n'écrit pas une application avec uniquement Uvicorn. Cela signifie que le code devrait inclure plus ou moins, au minimum, tout le code offert par Starlette (ou **FastAPI**). Et si on fait cela, l'application finale apportera les mêmes complications que si on avait utilisé un framework et que l'on avait minimisé la quantité de code et de bugs. + * Si on compare Uvicorn, il faut le comparer à d'autre applications de serveurs comme Daphne, Hypercorn, uWSGI, etc. +* **Starlette** : + * A les seconde meilleures performances après Uvicorn. Starlette utilise en réalité Uvicorn. De ce fait, il ne peut qu’être plus "lent" qu'Uvicorn car il requiert l'exécution de plus de code. + * Cependant il nous apporte les outils pour construire une application web simple, avec un routage basé sur des chemins, etc. + * Si on compare Starlette, il faut le comparer à d'autres frameworks web (ou micorframework) comme Sanic, Flask, Django, etc. +* **FastAPI** : + * Comme Starlette, FastAPI utilise Uvicorn et ne peut donc pas être plus rapide que ce dernier. + * FastAPI apporte des fonctionnalités supplémentaires à Starlette. Des fonctionnalités qui sont nécessaires presque systématiquement lors de la création d'une API, comme la validation des données, la sérialisation. En utilisant FastAPI, on obtient une documentation automatiquement (qui ne requiert aucune manipulation pour être mise en place). + * Si on n'utilisait pas FastAPI mais directement Starlette (ou un outil équivalent comme Sanic, Flask, Responder, etc) il faudrait implémenter la validation des données et la sérialisation par nous-même. Le résultat serait donc le même dans les deux cas mais du travail supplémentaire serait à réaliser avec Starlette, surtout en considérant que la validation des données et la sérialisation représentent la plus grande quantité de code à écrire dans une application. + * De ce fait, en utilisant FastAPI on minimise le temps de développement, les bugs, le nombre de lignes de code, et on obtient les mêmes performances (si ce n'est de meilleurs performances) que l'on aurait pu avoir sans ce framework (en ayant à implémenter de nombreuses fonctionnalités importantes par nous-mêmes). + * Si on compare FastAPI, il faut le comparer à d'autres frameworks web (ou ensemble d'outils) qui fournissent la validation des données, la sérialisation et la documentation, comme Flask-apispec, NestJS, Molten, etc. diff --git a/docs/fr/docs/contributing.md b/docs/fr/docs/contributing.md new file mode 100644 index 0000000000..8292f14bb9 --- /dev/null +++ b/docs/fr/docs/contributing.md @@ -0,0 +1,501 @@ +# Développement - Contribuer + +Tout d'abord, vous voudrez peut-être voir les moyens de base pour [aider FastAPI et obtenir de l'aide](help-fastapi.md){.internal-link target=_blank}. + +## Développement + +Si vous avez déjà cloné le dépôt et que vous savez que vous devez vous plonger dans le code, voici quelques directives pour mettre en place votre environnement. + +### Environnement virtuel avec `venv` + +Vous pouvez créer un environnement virtuel dans un répertoire en utilisant le module `venv` de Python : + +
+ +```console +$ python -m venv env +``` + +
+ +Cela va créer un répertoire `./env/` avec les binaires Python et vous pourrez alors installer des paquets pour cet environnement isolé. + +### Activer l'environnement + +Activez le nouvel environnement avec : + +=== "Linux, macOS" + +
+ + ```console + $ source ./env/bin/activate + ``` + +
+ +=== "Windows PowerShell" + +
+ + ```console + $ .\env\Scripts\Activate.ps1 + ``` + +
+ +=== "Windows Bash" + + Ou si vous utilisez Bash pour Windows (par exemple Git Bash): + +
+ + ```console + $ source ./env/Scripts/activate + ``` + +
+ +Pour vérifier que cela a fonctionné, utilisez : + +=== "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 + ``` + +
+ +Si celui-ci montre le binaire `pip` à `env/bin/pip`, alors ça a fonctionné. 🎉 + + + +!!! tip + Chaque fois que vous installez un nouveau paquet avec `pip` sous cet environnement, activez à nouveau l'environnement. + + Cela permet de s'assurer que si vous utilisez un programme terminal installé par ce paquet (comme `flit`), vous utilisez celui de votre environnement local et pas un autre qui pourrait être installé globalement. + +### Flit + +**FastAPI** utilise Flit pour build, packager et publier le projet. + +Après avoir activé l'environnement comme décrit ci-dessus, installez `flit` : + +
+ +```console +$ pip install flit + +---> 100% +``` + +
+ +Réactivez maintenant l'environnement pour vous assurer que vous utilisez le "flit" que vous venez d'installer (et non un environnement global). + +Et maintenant, utilisez `flit` pour installer les dépendances de développement : + +=== "Linux, macOS" + +
+ + ```console + $ flit install --deps develop --symlink + + ---> 100% + ``` + +
+ +=== "Windows" + + Si vous êtes sous Windows, utilisez `--pth-file` au lieu de `--symlink` : + +
+ + ```console + $ flit install --deps develop --pth-file + + ---> 100% + ``` + +
+ +Il installera toutes les dépendances et votre FastAPI local dans votre environnement local. + +#### Utiliser votre FastAPI local + +Si vous créez un fichier Python qui importe et utilise FastAPI, et que vous l'exécutez avec le Python de votre environnement local, il utilisera votre code source FastAPI local. + +Et si vous mettez à jour le code source local de FastAPI, tel qu'il est installé avec `--symlink` (ou `--pth-file` sous Windows), lorsque vous exécutez à nouveau ce fichier Python, il utilisera la nouvelle version de FastAPI que vous venez d'éditer. + +De cette façon, vous n'avez pas à "installer" votre version locale pour pouvoir tester chaque changement. + +### Formatage + +Il existe un script que vous pouvez exécuter qui formatera et nettoiera tout votre code : + +
+ +```console +$ bash scripts/format.sh +``` + +
+ +Il effectuera également un tri automatique de touts vos imports. + +Pour qu'il puisse les trier correctement, vous devez avoir FastAPI installé localement dans votre environnement, avec la commande dans la section ci-dessus en utilisant `--symlink` (ou `--pth-file` sous Windows). + +### Formatage des imports + +Il existe un autre script qui permet de formater touts les imports et de s'assurer que vous n'avez pas d'imports inutilisés : + +
+ +```console +$ bash scripts/format-imports.sh +``` + +
+ +Comme il exécute une commande après l'autre et modifie et inverse de nombreux fichiers, il prend un peu plus de temps à s'exécuter, il pourrait donc être plus facile d'utiliser fréquemment `scripts/format.sh` et `scripts/format-imports.sh` seulement avant de commit. + +## Documentation + +Tout d'abord, assurez-vous que vous configurez votre environnement comme décrit ci-dessus, qui installera toutes les exigences. + +La documentation utilise MkDocs. + +Et il y a des outils/scripts supplémentaires en place pour gérer les traductions dans `./scripts/docs.py`. + +!!! tip + Vous n'avez pas besoin de voir le code dans `./scripts/docs.py`, vous l'utilisez simplement dans la ligne de commande. + +Toute la documentation est au format Markdown dans le répertoire `./docs/fr/`. + +De nombreux tutoriels comportent des blocs de code. + +Dans la plupart des cas, ces blocs de code sont de véritables applications complètes qui peuvent être exécutées telles quelles. + +En fait, ces blocs de code ne sont pas écrits à l'intérieur du Markdown, ce sont des fichiers Python dans le répertoire `./docs_src/`. + +Et ces fichiers Python sont inclus/injectés dans la documentation lors de la génération du site. + +### Documentation pour les tests + +La plupart des tests sont en fait effectués par rapport aux exemples de fichiers sources dans la documentation. + +Cela permet de s'assurer que : + +* La documentation est à jour. +* Les exemples de documentation peuvent être exécutés tels quels. +* La plupart des fonctionnalités sont couvertes par la documentation, assurées par la couverture des tests. + +Au cours du développement local, un script build le site et vérifie les changements éventuels, puis il est rechargé en direct : + +
+ +```console +$ python ./scripts/docs.py live + +[INFO] Serving on http://127.0.0.1:8008 +[INFO] Start watching changes +[INFO] Start detecting changes +``` + +
+ +Il servira la documentation sur `http://127.0.0.1:8008`. + +De cette façon, vous pouvez modifier la documentation/les fichiers sources et voir les changements en direct. + +#### Typer CLI (facultatif) + +Les instructions ici vous montrent comment utiliser le script à `./scripts/docs.py` avec le programme `python` directement. + +Mais vous pouvez également utiliser Typer CLI, et vous obtiendrez l'auto-complétion dans votre terminal pour les commandes après l'achèvement de l'installation. + +Si vous installez Typer CLI, vous pouvez installer la complétion avec : + +
+ +```console +$ typer --install-completion + +zsh completion installed in /home/user/.bashrc. +Completion will take effect once you restart the terminal. +``` + +
+ +### Apps et documentation en même temps + +Si vous exécutez les exemples avec, par exemple : + +
+ +```console +$ uvicorn tutorial001:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Comme Uvicorn utilisera par défaut le port `8000`, la documentation sur le port `8008` n'entrera pas en conflit. + +### Traductions + +L'aide aux traductions est TRÈS appréciée ! Et cela ne peut se faire sans l'aide de la communauté. 🌎 🚀 + +Voici les étapes à suivre pour aider à la traduction. + +#### Conseils et lignes directrices + +* Vérifiez les pull requests existantes pour votre langue et ajouter des reviews demandant des changements ou les approuvant. + +!!! tip + Vous pouvez ajouter des commentaires avec des suggestions de changement aux pull requests existantes. + + Consultez les documents concernant l'ajout d'un review de pull request pour l'approuver ou demander des modifications. + +* Vérifiez dans issues pour voir s'il y a une personne qui coordonne les traductions pour votre langue. + +* Ajoutez une seule pull request par page traduite. Il sera ainsi beaucoup plus facile pour les autres de l'examiner. + +Pour les langues que je ne parle pas, je vais attendre plusieurs autres reviews de la traduction avant de merge. + +* Vous pouvez également vérifier s'il existe des traductions pour votre langue et y ajouter une review, ce qui m'aidera à savoir si la traduction est correcte et je pourrai la fusionner. + +* Utilisez les mêmes exemples en Python et ne traduisez que le texte des documents. Vous n'avez pas besoin de changer quoi que ce soit pour que cela fonctionne. + +* Utilisez les mêmes images, noms de fichiers et liens. Vous n'avez pas besoin de changer quoi que ce soit pour que cela fonctionne. + +* Pour vérifier le code à 2 lettres de la langue que vous souhaitez traduire, vous pouvez utiliser le tableau Liste des codes ISO 639-1. + +#### Langue existante + +Disons que vous voulez traduire une page pour une langue qui a déjà des traductions pour certaines pages, comme l'espagnol. + +Dans le cas de l'espagnol, le code à deux lettres est `es`. Ainsi, le répertoire des traductions espagnoles se trouve à l'adresse `docs/es/`. + +!!! tip + La langue principale ("officielle") est l'anglais, qui se trouve à l'adresse "docs/en/". + +Maintenant, lancez le serveur en live pour les documents en espagnol : + +
+ +```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 +``` + +
+ +Vous pouvez maintenant aller sur http://127.0.0.1:8008 et voir vos changements en direct. + +Si vous regardez le site web FastAPI docs, vous verrez que chaque langue a toutes les pages. Mais certaines pages ne sont pas traduites et sont accompagnées d'une notification concernant la traduction manquante. + +Mais si vous le gérez localement de cette manière, vous ne verrez que les pages déjà traduites. + +Disons maintenant que vous voulez ajouter une traduction pour la section [Features](features.md){.internal-link target=_blank}. + +* Copiez le fichier à : + +``` +docs/en/docs/features.md +``` + +* Collez-le exactement au même endroit mais pour la langue que vous voulez traduire, par exemple : + +``` +docs/es/docs/features.md +``` + +!!! tip + Notez que le seul changement dans le chemin et le nom du fichier est le code de langue, qui passe de `en` à `es`. + +* Ouvrez maintenant le fichier de configuration de MkDocs pour l'anglais à + +``` +docs/en/docs/mkdocs.yml +``` + +* Trouvez l'endroit où cette `docs/features.md` se trouve dans le fichier de configuration. Quelque part comme : + +```YAML hl_lines="8" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +``` + +* Ouvrez le fichier de configuration MkDocs pour la langue que vous éditez, par exemple : + +``` +docs/es/docs/mkdocs.yml +``` + +* Ajoutez-le à l'endroit exact où il se trouvait pour l'anglais, par exemple : + +```YAML hl_lines="8" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +``` + +Assurez-vous que s'il y a d'autres entrées, la nouvelle entrée avec votre traduction est exactement dans le même ordre que dans la version anglaise. + +Si vous allez sur votre navigateur, vous verrez que maintenant les documents montrent votre nouvelle section. 🎉 + +Vous pouvez maintenant tout traduire et voir à quoi cela ressemble au fur et à mesure que vous enregistrez le fichier. + +#### Nouvelle langue + +Disons que vous voulez ajouter des traductions pour une langue qui n'est pas encore traduite, pas même quelques pages. + +Disons que vous voulez ajouter des traductions pour le Créole, et que ce n'est pas encore dans les documents. + +En vérifiant le lien ci-dessus, le code pour "Créole" est `ht`. + +L'étape suivante consiste à exécuter le script pour générer un nouveau répertoire de traduction : + +
+ +```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 +``` + +
+ +Vous pouvez maintenant vérifier dans votre éditeur de code le répertoire nouvellement créé `docs/ht/`. + +!!! tip + Créez une première demande d'extraction à l'aide de cette fonction, afin de configurer la nouvelle langue avant d'ajouter des traductions. + + Ainsi, d'autres personnes peuvent vous aider à rédiger d'autres pages pendant que vous travaillez sur la première. 🚀 + +Commencez par traduire la page principale, `docs/ht/index.md`. + +Vous pouvez ensuite continuer avec les instructions précédentes, pour une "langue existante". + +##### Nouvelle langue non prise en charge + +Si, lors de l'exécution du script du serveur en direct, vous obtenez une erreur indiquant que la langue n'est pas prise en charge, quelque chose comme : + +``` + raise TemplateNotFound(template) +jinja2.exceptions.TemplateNotFound: partials/language/xx.html +``` + +Cela signifie que le thème ne supporte pas cette langue (dans ce cas, avec un faux code de 2 lettres de `xx`). + +Mais ne vous inquiétez pas, vous pouvez définir la langue du thème en anglais et ensuite traduire le contenu des documents. + +Si vous avez besoin de faire cela, modifiez le fichier `mkdocs.yml` pour votre nouvelle langue, il aura quelque chose comme : + +```YAML hl_lines="5" +site_name: FastAPI +# More stuff +theme: + # More stuff + language: xx +``` + +Changez cette langue de `xx` (de votre code de langue) à `fr`. + +Vous pouvez ensuite relancer le serveur live. + +#### Prévisualisez le résultat + +Lorsque vous utilisez le script à `./scripts/docs.py` avec la commande `live`, il n'affiche que les fichiers et les traductions disponibles pour la langue courante. + +Mais une fois que vous avez terminé, vous pouvez tester le tout comme il le ferait en ligne. + +Pour ce faire, il faut d'abord construire tous les documents : + +
+ +```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 +``` + +
+ +Cela génère tous les documents à `./docs_build/` pour chaque langue. Cela inclut l'ajout de tout fichier dont la traduction est manquante, avec une note disant que "ce fichier n'a pas encore de traduction". Mais vous n'avez rien à faire avec ce répertoire. + +Ensuite, il construit tous ces sites MkDocs indépendants pour chaque langue, les combine, et génère le résultat final à `./site/`. + +Ensuite, vous pouvez servir cela avec le commandement `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 +``` + +
+ +## Tests + +Il existe un script que vous pouvez exécuter localement pour tester tout le code et générer des rapports de couverture en HTML : + +
+ +```console +$ bash scripts/test-cov-html.sh +``` + +
+ +Cette commande génère un répertoire `./htmlcov/`, si vous ouvrez le fichier `./htmlcov/index.html` dans votre navigateur, vous pouvez explorer interactivement les régions de code qui sont couvertes par les tests, et remarquer s'il y a une région manquante. diff --git a/docs/fr/docs/deployment/deta.md b/docs/fr/docs/deployment/deta.md deleted file mode 100644 index cceb7b058c..0000000000 --- a/docs/fr/docs/deployment/deta.md +++ /dev/null @@ -1,245 +0,0 @@ -# Déployer FastAPI sur Deta - -Dans cette section, vous apprendrez à déployer facilement une application **FastAPI** sur Deta en utilisant le plan tarifaire gratuit. 🎁 - -Cela vous prendra environ **10 minutes**. - -!!! info - Deta sponsorise **FastAPI**. 🎉 - -## Une application **FastAPI** de base - -* Créez un répertoire pour votre application, par exemple `./fastapideta/` et déplacez-vous dedans. - -### Le code FastAPI - -* Créer un fichier `main.py` avec : - -```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): - return {"item_id": item_id} -``` - -### Dépendances - -Maintenant, dans le même répertoire, créez un fichier `requirements.txt` avec : - -```text -fastapi -``` - -!!! tip "Astuce" - Il n'est pas nécessaire d'installer Uvicorn pour déployer sur Deta, bien qu'il soit probablement souhaitable de l'installer localement pour tester votre application. - -### Structure du répertoire - -Vous aurez maintenant un répertoire `./fastapideta/` avec deux fichiers : - -``` -. -└── main.py -└── requirements.txt -``` - -## Créer un compte gratuit sur Deta - -Créez maintenant un compte gratuit -sur Deta, vous avez juste besoin d'une adresse email et d'un mot de passe. - -Vous n'avez même pas besoin d'une carte de crédit. - -## Installer le CLI (Interface en Ligne de Commande) - -Une fois que vous avez votre compte, installez le CLI de Deta : - -=== "Linux, macOS" - -
- - ```console - $ curl -fsSL https://get.deta.dev/cli.sh | sh - ``` - -
- -=== "Windows PowerShell" - -
- - ```console - $ iwr https://get.deta.dev/cli.ps1 -useb | iex - ``` - -
- -Après l'avoir installé, ouvrez un nouveau terminal afin que la nouvelle installation soit détectée. - -Dans un nouveau terminal, confirmez qu'il a été correctement installé avec : - -
- -```console -$ deta --help - -Deta command line interface for managing deta micros. -Complete documentation available at https://docs.deta.sh - -Usage: - deta [flags] - deta [command] - -Available Commands: - auth Change auth settings for a deta micro - -... -``` - -
- -!!! tip "Astuce" - Si vous rencontrez des problèmes pour installer le CLI, consultez la documentation officielle de Deta (en anglais). - -## Connexion avec le CLI - -Maintenant, connectez-vous à Deta depuis le CLI avec : - -
- -```console -$ deta login - -Please, log in from the web page. Waiting.. -Logged in successfully. -``` - -
- -Cela ouvrira un navigateur web et permettra une authentification automatique. - -## Déployer avec Deta - -Ensuite, déployez votre application avec le CLI de Deta : - -
- -```console -$ deta new - -Successfully created a new micro - -// Notice the "endpoint" 🔍 - -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} - -Adding dependencies... - - ----> 100% - - -Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6 -``` - -
- -Vous verrez un message JSON similaire à : - -```JSON hl_lines="4" -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} -``` - -!!! tip "Astuce" - Votre déploiement aura une URL `"endpoint"` différente. - -## Vérifiez - -Maintenant, dans votre navigateur ouvrez votre URL `endpoint`. Dans l'exemple ci-dessus, c'était -`https://qltnci.deta.dev`, mais la vôtre sera différente. - -Vous verrez la réponse JSON de votre application FastAPI : - -```JSON -{ - "Hello": "World" -} -``` - -Et maintenant naviguez vers `/docs` dans votre API, dans l'exemple ci-dessus ce serait `https://qltnci.deta.dev/docs`. - -Vous verrez votre documentation comme suit : - - - -## Activer l'accès public - -Par défaut, Deta va gérer l'authentification en utilisant des cookies pour votre compte. - -Mais une fois que vous êtes prêt, vous pouvez le rendre public avec : - -
- -```console -$ deta auth disable - -Successfully disabled http auth -``` - -
- -Maintenant, vous pouvez partager cette URL avec n'importe qui et ils seront en mesure d'accéder à votre API. 🚀 - -## HTTPS - -Félicitations ! Vous avez déployé votre application FastAPI sur Deta ! 🎉 🍰 - -Remarquez également que Deta gère correctement HTTPS pour vous, vous n'avez donc pas à vous en occuper et pouvez être sûr que vos clients auront une connexion cryptée sécurisée. ✅ 🔒 - -## Vérifiez le Visor - -À partir de l'interface graphique de votre documentation (dans une URL telle que `https://qltnci.deta.dev/docs`) -envoyez une requête à votre *opération de chemin* `/items/{item_id}`. - -Par exemple avec l'ID `5`. - -Allez maintenant sur https://web.deta.sh. - -Vous verrez qu'il y a une section à gauche appelée "Micros" avec chacune de vos applications. - -Vous verrez un onglet avec "Details", et aussi un onglet "Visor", allez à l'onglet "Visor". - -Vous pouvez y consulter les requêtes récentes envoyées à votre application. - -Vous pouvez également les modifier et les relancer. - - - -## En savoir plus - -À un moment donné, vous voudrez probablement stocker certaines données pour votre application d'une manière qui -persiste dans le temps. Pour cela, vous pouvez utiliser Deta Base, il dispose également d'un généreux **plan gratuit**. - -Vous pouvez également en lire plus dans la documentation Deta. diff --git a/docs/fr/docs/deployment/index.md b/docs/fr/docs/deployment/index.md index e855adfa3c..e2014afe95 100644 --- a/docs/fr/docs/deployment/index.md +++ b/docs/fr/docs/deployment/index.md @@ -1,4 +1,4 @@ -# Déploiement - Intro +# Déploiement Le déploiement d'une application **FastAPI** est relativement simple. diff --git a/docs/fr/docs/deployment/manually.md b/docs/fr/docs/deployment/manually.md new file mode 100644 index 0000000000..c53e2db677 --- /dev/null +++ b/docs/fr/docs/deployment/manually.md @@ -0,0 +1,153 @@ +# Exécuter un serveur manuellement - Uvicorn + +La principale chose dont vous avez besoin pour exécuter une application **FastAPI** sur une machine serveur distante est un programme serveur ASGI tel que **Uvicorn**. + +Il existe 3 principales alternatives : + +* Uvicorn : un serveur ASGI haute performance. +* Hypercorn : un serveur + ASGI compatible avec HTTP/2 et Trio entre autres fonctionnalités. +* Daphne : le serveur ASGI + conçu pour Django Channels. + +## Machine serveur et programme serveur + +Il y a un petit détail sur les noms à garder à l'esprit. 💡 + +Le mot "**serveur**" est couramment utilisé pour désigner à la fois l'ordinateur distant/cloud (la machine physique ou virtuelle) et également le programme qui s'exécute sur cette machine (par exemple, Uvicorn). + +Gardez cela à l'esprit lorsque vous lisez "serveur" en général, cela pourrait faire référence à l'une de ces deux choses. + +Lorsqu'on se réfère à la machine distante, il est courant de l'appeler **serveur**, mais aussi **machine**, **VM** (machine virtuelle), **nœud**. Tout cela fait référence à un type de machine distante, exécutant Linux, en règle générale, sur laquelle vous exécutez des programmes. + + +## Installer le programme serveur + +Vous pouvez installer un serveur compatible ASGI avec : + +=== "Uvicorn" + + * Uvicorn, un serveur ASGI rapide comme l'éclair, basé sur uvloop et httptools. + +
+ + ```console + $ pip install "uvicorn[standard]" + + ---> 100% + ``` + +
+ + !!! tip "Astuce" + En ajoutant `standard`, Uvicorn va installer et utiliser quelques dépendances supplémentaires recommandées. + + Cela inclut `uvloop`, le remplaçant performant de `asyncio`, qui fournit le gros gain de performance en matière de concurrence. + +=== "Hypercorn" + + * Hypercorn, un serveur ASGI également compatible avec HTTP/2. + +
+ + ```console + $ pip install hypercorn + + ---> 100% + ``` + +
+ + ...ou tout autre serveur ASGI. + +## Exécutez le programme serveur + +Vous pouvez ensuite exécuter votre application de la même manière que vous l'avez fait dans les tutoriels, mais sans l'option `--reload`, par exemple : + +=== "Uvicorn" + +
+ + ```console + $ uvicorn main:app --host 0.0.0.0 --port 80 + + INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) + ``` + +
+ +=== "Hypercorn" + +
+ + ```console + $ hypercorn main:app --bind 0.0.0.0:80 + + Running on 0.0.0.0:8080 over http (CTRL + C to quit) + ``` + +
+ +!!! warning + N'oubliez pas de supprimer l'option `--reload` si vous l'utilisiez. + + L'option `--reload` consomme beaucoup plus de ressources, est plus instable, etc. + + Cela aide beaucoup pendant le **développement**, mais vous **ne devriez pas** l'utiliser en **production**. + +## Hypercorn avec Trio + +Starlette et **FastAPI** sont basés sur +AnyIO, qui les rend +compatibles avec asyncio, de la bibliothèque standard Python et +Trio. + +Néanmoins, Uvicorn n'est actuellement compatible qu'avec asyncio, et il utilise normalement `uvloop`, le remplaçant hautes performances de `asyncio`. + +Mais si vous souhaitez utiliser directement **Trio**, vous pouvez utiliser **Hypercorn** car il le prend en charge. ✨ + +### Installer Hypercorn avec Trio + +Vous devez d'abord installer Hypercorn avec le support Trio : + +
+ +```console +$ pip install "hypercorn[trio]" +---> 100% +``` + +
+ +### Exécuter avec Trio + +Ensuite, vous pouvez passer l'option de ligne de commande `--worker-class` avec la valeur `trio` : + +
+ +```console +$ hypercorn main:app --worker-class trio +``` + +
+ +Et cela démarrera Hypercorn avec votre application en utilisant Trio comme backend. + +Vous pouvez désormais utiliser Trio en interne dans votre application. Ou mieux encore, vous pouvez utiliser AnyIO pour que votre code reste compatible avec Trio et asyncio. 🎉 + +## Concepts de déploiement + +Ces exemples lancent le programme serveur (e.g. Uvicorn), démarrant **un seul processus**, sur toutes les IPs (`0.0. +0.0`) sur un port prédéfini (par example, `80`). + +C'est l'idée de base. Mais vous vous préoccuperez probablement de certains concepts supplémentaires, tels que ... : + +* la sécurité - HTTPS +* l'exécution au démarrage +* les redémarrages +* la réplication (le nombre de processus en cours d'exécution) +* la mémoire +* les étapes précédant le démarrage + +Je vous en dirai plus sur chacun de ces concepts, sur la façon de les aborder, et donnerai quelques exemples concrets avec des stratégies pour les traiter dans les prochains chapitres. 🚀 diff --git a/docs/fr/docs/external-links.md b/docs/fr/docs/external-links.md index 002e6d2b21..37b8c5b139 100644 --- a/docs/fr/docs/external-links.md +++ b/docs/fr/docs/external-links.md @@ -9,70 +9,21 @@ Voici une liste incomplète de certains d'entre eux. !!! tip "Astuce" Si vous avez un article, projet, outil, ou quoi que ce soit lié à **FastAPI** qui n'est actuellement pas listé ici, créez une Pull Request l'ajoutant. -## Articles +{% for section_name, section_content in external_links.items() %} -### Anglais +## {{ section_name }} -{% if external_links %} -{% for article in external_links.articles.english %} +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* {{ item.title }} by {{ item.author }}. -* {{ article.title }} par {{ article.author }}. {% endfor %} -{% endif %} - -### Japonais - -{% if external_links %} -{% for article in external_links.articles.japanese %} - -* {{ article.title }} par {{ article.author }}. {% endfor %} -{% endif %} - -### Vietnamien - -{% if external_links %} -{% for article in external_links.articles.vietnamese %} - -* {{ article.title }} par {{ article.author }}. {% endfor %} -{% endif %} - -### Russe - -{% if external_links %} -{% for article in external_links.articles.russian %} - -* {{ article.title }} par {{ article.author }}. -{% endfor %} -{% endif %} - -### Allemand - -{% if external_links %} -{% for article in external_links.articles.german %} - -* {{ article.title }} par {{ article.author }}. -{% endfor %} -{% endif %} - -## Podcasts - -{% if external_links %} -{% for article in external_links.podcasts.english %} - -* {{ article.title }} par {{ article.author }}. -{% endfor %} -{% endif %} - -## Conférences - -{% if external_links %} -{% for article in external_links.talks.english %} - -* {{ article.title }} par {{ article.author }}. -{% endfor %} -{% endif %} ## Projets diff --git a/docs/fr/docs/features.md b/docs/fr/docs/features.md index dcc0e39ed7..0c1f6269a0 100644 --- a/docs/fr/docs/features.md +++ b/docs/fr/docs/features.md @@ -25,7 +25,7 @@ Documentation d'API interactive et interface web d'exploration. Comme le framewo ### Faite en python moderne -Tout est basé sur la déclaration de type standard de **Python 3.6** (grâce à Pydantic). Pas de nouvelles syntaxes à apprendre. Juste du Python standard et moderne. +Tout est basé sur la déclaration de type standard de **Python 3.8** (grâce à Pydantic). Pas de nouvelles syntaxes à apprendre. Juste du Python standard et moderne. Si vous souhaitez un rappel de 2 minutes sur l'utilisation des types en Python (même si vous ne comptez pas utiliser FastAPI), jetez un oeil au tutoriel suivant: [Python Types](python-types.md){.internal-link target=_blank}. @@ -71,9 +71,9 @@ my_second_user: User = User(**second_user_data) Tout le framework a été conçu pour être facile et intuitif d'utilisation, toutes les décisions de design ont été testées sur de nombreux éditeurs avant même de commencer le développement final afin d'assurer la meilleure expérience de développement possible. -Dans le dernier sondage effectué auprès de développeurs python il était clair que la fonctionnalité la plus utilisée est "l'autocomplètion". +Dans le dernier sondage effectué auprès de développeurs python il était clair que la fonctionnalité la plus utilisée est "l’autocomplétion". -Tout le framwork **FastAPI** a été conçu avec cela en tête. L'autocomplétion fonctionne partout. +Tout le framework **FastAPI** a été conçu avec cela en tête. L'autocomplétion fonctionne partout. Vous devrez rarement revenir à la documentation. @@ -136,7 +136,7 @@ FastAPI contient un système simple mais extrêmement puissant d'Starlette. Le code utilisant Starlette que vous ajouterez fonctionnera donc aussi. -En fait, `FastAPI` est un sous compposant de `Starlette`. Donc, si vous savez déjà comment utiliser Starlette, la plupart des fonctionnalités fonctionneront de la même manière. +En fait, `FastAPI` est un sous composant de `Starlette`. Donc, si vous savez déjà comment utiliser Starlette, la plupart des fonctionnalités fonctionneront de la même manière. Avec **FastAPI** vous aurez toutes les fonctionnalités de **Starlette** (FastAPI est juste Starlette sous stéroïdes): -* Des performances vraiments impressionnantes. C'est l'un des framework Python les plus rapide, à égalité avec **NodeJS** et **GO**. +* Des performances vraiment impressionnantes. C'est l'un des framework Python les plus rapide, à égalité avec **NodeJS** et **GO**. * Le support des **WebSockets**. * Le support de **GraphQL**. * Les tâches d'arrière-plan. @@ -180,7 +180,7 @@ Inclus des librairies externes basées, aussi, sur Pydantic, servent d' décorateur de validation * 100% de couverture de test. diff --git a/docs/fr/docs/help-fastapi.md b/docs/fr/docs/help-fastapi.md index 0995721e1e..525c699f5f 100644 --- a/docs/fr/docs/help-fastapi.md +++ b/docs/fr/docs/help-fastapi.md @@ -36,8 +36,8 @@ Vous pouvez : * Me suivre sur **Twitter**. * Dites-moi comment vous utilisez FastAPI (j'adore entendre ça). * Entendre quand je fais des annonces ou que je lance de nouveaux outils. -* Vous connectez à moi sur **Linkedin**. - * Etre notifié quand je fais des annonces ou que je lance de nouveaux outils (bien que j'utilise plus souvent Twitte 🤷‍♂). +* Vous connectez à moi sur **LinkedIn**. + * Etre notifié quand je fais des annonces ou que je lance de nouveaux outils (bien que j'utilise plus souvent Twitter 🤷‍♂). * Lire ce que j’écris (ou me suivre) sur **Dev.to** ou **Medium**. * Lire d'autres idées, articles, et sur les outils que j'ai créés. * Suivez-moi pour lire quand je publie quelque chose de nouveau. @@ -84,24 +84,6 @@ Vous pouvez - Rejoindre le chat à https://gitter.im/tiangolo/fastapi - - -Rejoignez le chat sur Gitter: https://gitter.im/tiangolo/fastapi. - -Vous pouvez y avoir des conversations rapides avec d'autres personnes, aider les autres, partager des idées, etc. - -Mais gardez à l'esprit que, comme il permet une "conversation plus libre", il est facile de poser des questions trop générales et plus difficiles à répondre, de sorte que vous risquez de ne pas recevoir de réponses. - -Dans les Issues de GitHub, le modèle vous guidera pour écrire la bonne question afin que vous puissiez plus facilement obtenir une bonne réponse, ou même résoudre le problème vous-même avant même de le poser. Et dans GitHub, je peux m'assurer que je réponds toujours à tout, même si cela prend du temps. Je ne peux pas faire cela personnellement avec le chat Gitter. 😅 - -Les conversations dans Gitter ne sont pas non plus aussi facilement consultables que dans GitHub, de sorte que les questions et les réponses peuvent se perdre dans la conversation. - -De l'autre côté, il y a plus de 1000 personnes dans le chat, il y a donc de fortes chances que vous y trouviez quelqu'un à qui parler, presque tout le temps. 😄 - ## Parrainer l'auteur Vous pouvez également soutenir financièrement l'auteur (moi) via GitHub sponsors. diff --git a/docs/fr/docs/index.md b/docs/fr/docs/index.md index e7fb9947d5..f732fc74c3 100644 --- a/docs/fr/docs/index.md +++ b/docs/fr/docs/index.md @@ -1,48 +1,46 @@ - -{!../../../docs/missing-translation.md!} - -

FastAPI

- FastAPI framework, high performance, easy to learn, fast to code, ready for production + Framework FastAPI, haute performance, facile à apprendre, rapide à coder, prêt pour la production

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

--- -**Documentation**: https://fastapi.tiangolo.com +**Documentation** : https://fastapi.tiangolo.com -**Source Code**: https://github.com/tiangolo/fastapi +**Code Source** : 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. +FastAPI est un framework web moderne et rapide (haute performance) pour la création d'API avec Python 3.8+, basé sur les annotations de type standard de Python. -The key features are: +Les principales fonctionnalités sont : -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). +* **Rapidité** : De très hautes performances, au niveau de **NodeJS** et **Go** (grâce à Starlette et Pydantic). [L'un des frameworks Python les plus rapides](#performance). +* **Rapide à coder** : Augmente la vitesse de développement des fonctionnalités d'environ 200 % à 300 %. * +* **Moins de bugs** : Réduit d'environ 40 % les erreurs induites par le développeur. * +* **Intuitif** : Excellente compatibilité avec les IDE. Complétion complète. Moins de temps passé à déboguer. +* **Facile** : Conçu pour être facile à utiliser et à apprendre. Moins de temps passé à lire la documentation. +* **Concis** : Diminue la duplication de code. De nombreuses fonctionnalités liées à la déclaration de chaque paramètre. Moins de bugs. +* **Robuste** : Obtenez un code prêt pour la production. Avec une documentation interactive automatique. +* **Basé sur des normes** : Basé sur (et entièrement compatible avec) les standards ouverts pour les APIs : OpenAPI (précédemment connu sous le nom de Swagger) et JSON Schema. -* **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. +* estimation basée sur des tests d'une équipe de développement interne, construisant des applications de production. ## Sponsors @@ -63,60 +61,66 @@ The key features are: ## 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._" +"_[...] J'utilise beaucoup **FastAPI** ces derniers temps. [...] Je prévois de l'utiliser dans mon équipe pour tous les **services de ML chez Microsoft**. Certains d'entre eux seront intégrés dans le coeur de **Windows** et dans certains produits **Office**._"
Kabir Khan - Microsoft (ref)
--- -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" +"_Nous avons adopté la bibliothèque **FastAPI** pour créer un serveur **REST** qui peut être interrogé pour obtenir des **prédictions**. [pour Ludwig]_" -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+
Piero Molino, Yaroslav Dudin et Sai Sumanth Miryala - Uber (ref)
--- -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" +"_**Netflix** a le plaisir d'annoncer la sortie en open-source de notre framework d'orchestration de **gestion de crise** : **Dispatch** ! [construit avec **FastAPI**]_"
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
--- -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" +"_Je suis très enthousiaste à propos de **FastAPI**. C'est un bonheur !_" -
Brian Okken - Python Bytes podcast host (ref)
+
Brian Okken - Auteur du podcast Python Bytes (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._" +"_Honnêtement, ce que vous avez construit a l'air super solide et élégant. A bien des égards, c'est comme ça que je voulais que **Hug** soit - c'est vraiment inspirant de voir quelqu'un construire ça._" -
Timothy Crosley - Hug creator (ref)
+
Timothy Crosley - Créateur de Hug (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 [...]_" +"_Si vous cherchez à apprendre un **framework moderne** pour créer des APIs REST, regardez **FastAPI** [...] C'est rapide, facile à utiliser et à apprendre [...]_" -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" +"_Nous sommes passés à **FastAPI** pour nos **APIs** [...] Je pense que vous l'aimerez [...]_" -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+
Ines Montani - Matthew Honnibal - Fondateurs de Explosion AI - Créateurs de spaCy (ref) - (ref)
--- -## **Typer**, the FastAPI of CLIs +"_Si quelqu'un cherche à construire une API Python de production, je recommande vivement **FastAPI**. Il est **bien conçu**, **simple à utiliser** et **très évolutif**. Il est devenu un **composant clé** dans notre stratégie de développement API first et il est à l'origine de nombreux automatismes et services tels que notre ingénieur virtuel TAC._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## **Typer**, le FastAPI des CLI -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. +Si vous souhaitez construire une application CLI utilisable dans un terminal au lieu d'une API web, regardez **Typer**. -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 +**Typer** est le petit frère de FastAPI. Et il est destiné à être le **FastAPI des CLI**. ⌨️ 🚀 -## Requirements +## Prérequis -Python 3.7+ +Python 3.8+ -FastAPI stands on the shoulders of giants: +FastAPI repose sur les épaules de géants : -* Starlette for the web parts. -* Pydantic for the data parts. +* Starlette pour les parties web. +* Pydantic pour les parties données. ## Installation @@ -130,7 +134,7 @@ $ pip install fastapi -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. +Vous aurez également besoin d'un serveur ASGI pour la production tel que Uvicorn ou Hypercorn.
@@ -142,11 +146,11 @@ $ pip install "uvicorn[standard]"
-## Example +## Exemple -### Create it +### Créez -* Create a file `main.py` with: +* Créez un fichier `main.py` avec : ```Python from typing import Union @@ -167,11 +171,11 @@ def read_item(item_id: int, q: Union[str, None] = None): ```
-Or use async def... +Ou utilisez async def ... -If your code uses `async` / `await`, use `async def`: +Si votre code utilise `async` / `await`, utilisez `async def` : -```Python hl_lines="9 14" +```Python hl_lines="9 14" from typing import Union from fastapi import FastAPI @@ -189,15 +193,15 @@ async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` -**Note**: +**Note** -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. +Si vous n'êtes pas familier avec cette notion, consultez la section _"Vous êtes pressés ?"_ à propos de `async` et `await` dans la documentation.
-### Run it +### Lancez -Run the server with: +Lancez le serveur avec :
@@ -214,56 +218,56 @@ INFO: Application startup complete.
-About the command uvicorn main:app --reload... +À propos de la commande uvicorn main:app --reload ... -The command `uvicorn main:app` refers to: +La commande `uvicorn main:app` fait référence à : -* `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. +* `main` : le fichier `main.py` (le "module" Python). +* `app` : l'objet créé à l'intérieur de `main.py` avec la ligne `app = FastAPI()`. +* `--reload` : fait redémarrer le serveur après des changements de code. À n'utiliser que pour le développement.
-### Check it +### Vérifiez -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. +Ouvrez votre navigateur à l'adresse http://127.0.0.1:8000/items/5?q=somequery. -You will see the JSON response as: +Vous obtenez alors cette réponse JSON : ```JSON {"item_id": 5, "q": "somequery"} ``` -You already created an API that: +Vous venez de créer une API qui : -* 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`. +* Reçoit les requêtes HTTP pour les _chemins_ `/` et `/items/{item_id}`. +* Les deux _chemins_ acceptent des opérations `GET` (également connu sous le nom de _méthodes_ HTTP). +* Le _chemin_ `/items/{item_id}` a un _paramètre_ `item_id` qui doit être un `int`. +* Le _chemin_ `/items/{item_id}` a un _paramètre de requête_ optionnel `q` de type `str`. -### Interactive API docs +### Documentation API interactive -Now go to http://127.0.0.1:8000/docs. +Maintenant, rendez-vous sur http://127.0.0.1:8000/docs. -You will see the automatic interactive API documentation (provided by Swagger UI): +Vous verrez la documentation interactive automatique de l'API (fournie par Swagger UI) : ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternative API docs +### Documentation API alternative -And now, go to http://127.0.0.1:8000/redoc. +Et maintenant, rendez-vous sur http://127.0.0.1:8000/redoc. -You will see the alternative automatic documentation (provided by ReDoc): +Vous verrez la documentation interactive automatique de l'API (fournie par ReDoc) : ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Example upgrade +## Exemple plus poussé -Now modify the file `main.py` to receive a body from a `PUT` request. +Maintenant, modifiez le fichier `main.py` pour recevoir le corps d'une requête `PUT`. -Declare the body using standard Python types, thanks to Pydantic. +Déclarez ce corps en utilisant les types Python standards, grâce à Pydantic. -```Python hl_lines="4 9 10 11 12 25 26 27" +```Python hl_lines="4 9-12 25-27" from typing import Union from fastapi import FastAPI @@ -293,174 +297,172 @@ 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). +Le serveur se recharge normalement automatiquement (car vous avez pensé à `--reload` dans la commande `uvicorn` ci-dessus). -### Interactive API docs upgrade +### Plus loin avec la documentation API interactive -Now go to http://127.0.0.1:8000/docs. +Maintenant, rendez-vous sur http://127.0.0.1:8000/docs. -* The interactive API documentation will be automatically updated, including the new body: +* La documentation interactive de l'API sera automatiquement mise à jour, y compris le nouveau corps de la requête : ![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: +* Cliquez sur le bouton "Try it out", il vous permet de renseigner les paramètres et d'interagir directement avec l'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: +* Cliquez ensuite sur le bouton "Execute", l'interface utilisateur communiquera avec votre API, enverra les paramètres, obtiendra les résultats et les affichera à l'écran : ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Alternative API docs upgrade +### Plus loin avec la documentation API alternative -And now, go to http://127.0.0.1:8000/redoc. +Et maintenant, rendez-vous sur http://127.0.0.1:8000/redoc. -* The alternative documentation will also reflect the new query parameter and body: +* La documentation alternative reflétera également le nouveau paramètre de requête et le nouveau corps : ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Recap +### En résumé -In summary, you declare **once** the types of parameters, body, etc. as function parameters. +En résumé, vous déclarez **une fois** les types de paramètres, le corps de la requête, etc. en tant que paramètres de fonction. -You do that with standard modern Python types. +Vous faites cela avec les types Python standard modernes. -You don't have to learn a new syntax, the methods or classes of a specific library, etc. +Vous n'avez pas à apprendre une nouvelle syntaxe, les méthodes ou les classes d'une bibliothèque spécifique, etc. -Just standard **Python 3.6+**. +Juste du **Python 3.8+** standard. -For example, for an `int`: +Par exemple, pour un `int`: ```Python item_id: int ``` -or for a more complex `Item` model: +ou pour un modèle `Item` plus complexe : ```Python item: Item ``` -...and with that single declaration you get: +... et avec cette déclaration unique, vous obtenez : -* 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: +* Une assistance dans votre IDE, notamment : + * la complétion. + * la vérification des types. +* La validation des données : + * des erreurs automatiques et claires lorsque les données ne sont pas valides. + * une validation même pour les objets JSON profondément imbriqués. +* Une conversion des données d'entrée : venant du réseau et allant vers les données et types de Python, permettant de lire : + * le JSON. + * les paramètres du chemin. + * les paramètres de la requête. + * les cookies. + * les en-têtes. + * les formulaires. + * les fichiers. +* La conversion des données de sortie : conversion des données et types Python en données réseau (au format JSON), permettant de convertir : + * les types Python (`str`, `int`, `float`, `bool`, `list`, etc). + * les objets `datetime`. + * les objets `UUID`. + * les modèles de base de données. + * ... et beaucoup plus. +* La documentation API interactive automatique, avec 2 interfaces utilisateur au choix : * Swagger UI. * ReDoc. --- -Coming back to the previous code example, **FastAPI** will: +Pour revenir à l'exemple de code précédent, **FastAPI** permet de : -* 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. +* Valider que `item_id` existe dans le chemin des requêtes `GET` et `PUT`. +* Valider que `item_id` est de type `int` pour les requêtes `GET` et `PUT`. + * Si ce n'est pas le cas, le client voit une erreur utile et claire. +* Vérifier qu'il existe un paramètre de requête facultatif nommé `q` (comme dans `http://127.0.0.1:8000/items/foo?q=somequery`) pour les requêtes `GET`. + * Puisque le paramètre `q` est déclaré avec `= None`, il est facultatif. + * Sans le `None`, il serait nécessaire (comme l'est le corps de la requête dans le cas du `PUT`). +* Pour les requêtes `PUT` vers `/items/{item_id}`, de lire le corps en JSON : + * Vérifier qu'il a un attribut obligatoire `name` qui devrait être un `str`. + * Vérifier qu'il a un attribut obligatoire `prix` qui doit être un `float`. + * Vérifier qu'il a un attribut facultatif `is_offer`, qui devrait être un `bool`, s'il est présent. + * Tout cela fonctionnerait également pour les objets JSON profondément imbriqués. +* Convertir de et vers JSON automatiquement. +* Documenter tout avec OpenAPI, qui peut être utilisé par : + * Les systèmes de documentation interactifs. + * Les systèmes de génération automatique de code client, pour de nombreuses langues. +* Fournir directement 2 interfaces web de documentation interactive. --- -We just scratched the surface, but you already get the idea of how it all works. +Nous n'avons fait qu'effleurer la surface, mais vous avez déjà une idée de la façon dont tout cela fonctionne. -Try changing the line with: +Essayez de changer la ligne contenant : ```Python return {"item_name": item.name, "item_id": item_id} ``` -...from: +... de : ```Python ... "item_name": item.name ... ``` -...to: +... vers : ```Python ... "item_price": item.price ... ``` -...and see how your editor will auto-complete the attributes and know their types: +... et voyez comment votre éditeur complétera automatiquement les attributs et connaîtra leurs types : -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) +![compatibilité IDE](https://fastapi.tiangolo.com/img/vscode-completion.png) -For a more complete example including more features, see the Tutorial - User Guide. +Pour un exemple plus complet comprenant plus de fonctionnalités, voir le Tutoriel - Guide utilisateur. -**Spoiler alert**: the tutorial - user guide includes: +**Spoiler alert** : le tutoriel - guide utilisateur inclut : -* 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: +* Déclaration de **paramètres** provenant d'autres endroits différents comme : **en-têtes.**, **cookies**, **champs de formulaire** et **fichiers**. +* L'utilisation de **contraintes de validation** comme `maximum_length` ou `regex`. +* Un **système d'injection de dépendance ** très puissant et facile à utiliser . +* Sécurité et authentification, y compris la prise en charge de **OAuth2** avec les **jetons JWT** et l'authentification **HTTP Basic**. +* Des techniques plus avancées (mais tout aussi faciles) pour déclarer les **modèles JSON profondément imbriqués** (grâce à Pydantic). +* Intégration de **GraphQL** avec Strawberry et d'autres bibliothèques. +* D'obtenir de nombreuses fonctionnalités supplémentaires (grâce à Starlette) comme : * **WebSockets** - * **GraphQL** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** + * de tester le code très facilement avec `requests` et `pytest` + * **CORS** * **Cookie Sessions** - * ...and more. + * ... et plus encore. ## 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). (*) +Les benchmarks TechEmpower indépendants montrent que les applications **FastAPI** s'exécutant sous Uvicorn sont parmi les frameworks existants en Python les plus rapides , juste derrière Starlette et Uvicorn (utilisés en interne par FastAPI). (*) -To understand more about it, see the section Benchmarks. +Pour en savoir plus, consultez la section Benchmarks. -## Optional Dependencies +## Dépendances facultatives -Used by Pydantic: +Utilisées par Pydantic: -* ujson - for faster JSON "parsing". -* email_validator - for email validation. +* email_validator - pour la validation des adresses email. -Used by Starlette: +Utilisées par Starlette : -* HTTPX - Required if you want to use the `TestClient`. -* 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`. +* requests - Obligatoire si vous souhaitez utiliser `TestClient`. +* jinja2 - Obligatoire si vous souhaitez utiliser la configuration de template par défaut. +* python-multipart - Obligatoire si vous souhaitez supporter le "décodage" de formulaire avec `request.form()`. +* itsdangerous - Obligatoire pour la prise en charge de `SessionMiddleware`. +* pyyaml - Obligatoire pour le support `SchemaGenerator` de Starlette (vous n'en avez probablement pas besoin avec FastAPI). +* ujson - Obligatoire si vous souhaitez utiliser `UJSONResponse`. -Used by FastAPI / Starlette: +Utilisées par FastAPI / Starlette : -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. +* uvicorn - Pour le serveur qui charge et sert votre application. +* orjson - Obligatoire si vous voulez utiliser `ORJSONResponse`. -You can install all of these with `pip install fastapi[all]`. +Vous pouvez tout installer avec `pip install fastapi[all]`. -## License +## Licence -This project is licensed under the terms of the MIT license. +Ce projet est soumis aux termes de la licence MIT. diff --git a/docs/fr/docs/python-types.md b/docs/fr/docs/python-types.md index 4008ed96fc..f49fbafd36 100644 --- a/docs/fr/docs/python-types.md +++ b/docs/fr/docs/python-types.md @@ -119,7 +119,7 @@ Comme l'éditeur connaît le type des variables, vous n'avez pas seulement l'aut -Maintenant que vous avez connaissance du problème, convertissez `age` en chaine de caractères grâce à `str(age)` : +Maintenant que vous avez connaissance du problème, convertissez `age` en chaîne de caractères grâce à `str(age)` : ```Python hl_lines="2" {!../../../docs_src/python_types/tutorial004.py!} diff --git a/docs/fr/docs/tutorial/body.md b/docs/fr/docs/tutorial/body.md index 1e732d3364..89720c973a 100644 --- a/docs/fr/docs/tutorial/body.md +++ b/docs/fr/docs/tutorial/body.md @@ -98,7 +98,7 @@ Et vous obtenez aussi de la vérification d'erreur pour les opérations incorrec -Ce n'est pas un hasard, ce framework entier a été bati avec ce design comme objectif. +Ce n'est pas un hasard, ce framework entier a été bâti avec ce design comme objectif. Et cela a été rigoureusement testé durant la phase de design, avant toute implémentation, pour s'assurer que cela fonctionnerait avec tous les éditeurs. diff --git a/docs/fr/docs/tutorial/debugging.md b/docs/fr/docs/tutorial/debugging.md new file mode 100644 index 0000000000..e58872d30a --- /dev/null +++ b/docs/fr/docs/tutorial/debugging.md @@ -0,0 +1,112 @@ +# Débogage + +Vous pouvez connecter le débogueur dans votre éditeur, par exemple avec Visual Studio Code ou PyCharm. + +## Faites appel à `uvicorn` + +Dans votre application FastAPI, importez et exécutez directement `uvicorn` : + +```Python hl_lines="1 15" +{!../../../docs_src/debugging/tutorial001.py!} +``` + +### À propos de `__name__ == "__main__"` + +Le but principal de `__name__ == "__main__"` est d'avoir du code qui est exécuté lorsque votre fichier est appelé avec : + +
+ +```console +$ python myapp.py +``` + +
+ +mais qui n'est pas appelé lorsqu'un autre fichier l'importe, comme dans : + +```Python +from myapp import app +``` + +#### Pour davantage de détails + +Imaginons que votre fichier s'appelle `myapp.py`. + +Si vous l'exécutez avec : + +
+ +```console +$ python myapp.py +``` + +
+ +alors la variable interne `__name__` de votre fichier, créée automatiquement par Python, aura pour valeur la chaîne de caractères `"__main__"`. + +Ainsi, la section : + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +va s'exécuter. + +--- + +Cela ne se produira pas si vous importez ce module (fichier). + +Par exemple, si vous avez un autre fichier `importer.py` qui contient : + +```Python +from myapp import app + +# Code supplémentaire +``` + +dans ce cas, la variable automatique `__name__` à l'intérieur de `myapp.py` n'aura pas la valeur `"__main__"`. + +Ainsi, la ligne : + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +ne sera pas exécutée. + +!!! info +Pour plus d'informations, consultez la documentation officielle de Python. + +## Exécutez votre code avec votre débogueur + +Parce que vous exécutez le serveur Uvicorn directement depuis votre code, vous pouvez appeler votre programme Python (votre application FastAPI) directement depuis le débogueur. + +--- + +Par exemple, dans Visual Studio Code, vous pouvez : + +- Cliquer sur l'onglet "Debug" de la barre d'activités de Visual Studio Code. +- "Add configuration...". +- Sélectionnez "Python". +- Lancez le débogueur avec l'option "`Python: Current File (Integrated Terminal)`". + +Il démarrera alors le serveur avec votre code **FastAPI**, s'arrêtera à vos points d'arrêt, etc. + +Voici à quoi cela pourrait ressembler : + + + +--- + +Si vous utilisez Pycharm, vous pouvez : + +- Ouvrir le menu "Run". +- Sélectionnez l'option "Debug...". +- Un menu contextuel s'affiche alors. +- Sélectionnez le fichier à déboguer (dans ce cas, `main.py`). + +Il démarrera alors le serveur avec votre code **FastAPI**, s'arrêtera à vos points d'arrêt, etc. + +Voici à quoi cela pourrait ressembler : + + diff --git a/docs/fr/docs/tutorial/first-steps.md b/docs/fr/docs/tutorial/first-steps.md index 224c340c66..e98283f1e2 100644 --- a/docs/fr/docs/tutorial/first-steps.md +++ b/docs/fr/docs/tutorial/first-steps.md @@ -170,7 +170,7 @@ Si vous créez votre app avec : {!../../../docs_src/first_steps/tutorial002.py!} ``` -Et la mettez dans un fichier `main.py`, alors vous appeleriez `uvicorn` avec : +Et la mettez dans un fichier `main.py`, alors vous appelleriez `uvicorn` avec :
diff --git a/docs/fr/docs/tutorial/index.md b/docs/fr/docs/tutorial/index.md new file mode 100644 index 0000000000..4dc202b334 --- /dev/null +++ b/docs/fr/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Tutoriel - Guide utilisateur - Introduction + +Ce tutoriel vous montre comment utiliser **FastAPI** avec la plupart de ses fonctionnalités, étape par étape. + +Chaque section s'appuie progressivement sur les précédentes, mais elle est structurée de manière à séparer les sujets, afin que vous puissiez aller directement à l'un d'entre eux pour résoudre vos besoins spécifiques en matière d'API. + +Il est également conçu pour fonctionner comme une référence future. + +Vous pouvez donc revenir et voir exactement ce dont vous avez besoin. + +## Exécuter le code + +Tous les blocs de code peuvent être copiés et utilisés directement (il s'agit en fait de fichiers Python testés). + +Pour exécuter l'un de ces exemples, copiez le code dans un fichier `main.py`, et commencez `uvicorn` avec : + +
+ +```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. +``` + +
+ +Il est **FORTEMENT encouragé** que vous écriviez ou copiez le code, l'éditiez et l'exécutiez localement. + +L'utiliser dans votre éditeur est ce qui vous montre vraiment les avantages de FastAPI, en voyant le peu de code que vous avez à écrire, toutes les vérifications de type, l'autocomplétion, etc. + +--- + +## Installer FastAPI + +La première étape consiste à installer FastAPI. + +Pour le tutoriel, vous voudrez peut-être l'installer avec toutes les dépendances et fonctionnalités optionnelles : + +
+ +```console +$ pip install fastapi[all] + +---> 100% +``` + +
+ +... qui comprend également `uvicorn`, que vous pouvez utiliser comme serveur pour exécuter votre code. + +!!! note + Vous pouvez également l'installer pièce par pièce. + + C'est ce que vous feriez probablement une fois que vous voudrez déployer votre application en production : + + ``` + pip install fastapi + ``` + + Installez également `uvicorn` pour qu'il fonctionne comme serveur : + + ``` + pip install uvicorn + ``` + + Et la même chose pour chacune des dépendances facultatives que vous voulez utiliser. + +## Guide utilisateur avancé + +Il existe également un **Guide d'utilisation avancé** que vous pouvez lire plus tard après ce **Tutoriel - Guide d'utilisation**. + +Le **Guide d'utilisation avancé**, qui s'appuie sur cette base, utilise les mêmes concepts et vous apprend quelques fonctionnalités supplémentaires. + +Mais vous devez d'abord lire le **Tutoriel - Guide d'utilisation** (ce que vous êtes en train de lire en ce moment). + +Il est conçu pour que vous puissiez construire une application complète avec seulement le **Tutoriel - Guide d'utilisation**, puis l'étendre de différentes manières, en fonction de vos besoins, en utilisant certaines des idées supplémentaires du **Guide d'utilisation avancé**. diff --git a/docs/fr/docs/tutorial/query-params-str-validations.md b/docs/fr/docs/tutorial/query-params-str-validations.md new file mode 100644 index 0000000000..f5248fe8b3 --- /dev/null +++ b/docs/fr/docs/tutorial/query-params-str-validations.md @@ -0,0 +1,305 @@ +# Paramètres de requête et validations de chaînes de caractères + +**FastAPI** vous permet de déclarer des informations et des validateurs additionnels pour vos paramètres de requêtes. + +Commençons avec cette application pour exemple : + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial001.py!} +``` + +Le paramètre de requête `q` a pour type `Union[str, None]` (ou `str | None` en Python 3.10), signifiant qu'il est de type `str` mais pourrait aussi être égal à `None`, et bien sûr, la valeur par défaut est `None`, donc **FastAPI** saura qu'il n'est pas requis. + +!!! note + **FastAPI** saura que la valeur de `q` n'est pas requise grâce à la valeur par défaut `= None`. + + Le `Union` dans `Union[str, None]` permettra à votre éditeur de vous offrir un meilleur support et de détecter les erreurs. + +## Validation additionnelle + +Nous allons imposer que bien que `q` soit un paramètre optionnel, dès qu'il est fourni, **sa longueur n'excède pas 50 caractères**. + +## Importer `Query` + +Pour cela, importez d'abord `Query` depuis `fastapi` : + +```Python hl_lines="3" +{!../../../docs_src/query_params_str_validations/tutorial002.py!} +``` + +## Utiliser `Query` comme valeur par défaut + +Construisez ensuite la valeur par défaut de votre paramètre avec `Query`, en choisissant 50 comme `max_length` : + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial002.py!} +``` + +Comme nous devons remplacer la valeur par défaut `None` dans la fonction par `Query()`, nous pouvons maintenant définir la valeur par défaut avec le paramètre `Query(default=None)`, il sert le même objectif qui est de définir cette valeur par défaut. + +Donc : + +```Python +q: Union[str, None] = Query(default=None) +``` + +... rend le paramètre optionnel, et est donc équivalent à : + +```Python +q: Union[str, None] = None +``` + +Mais déclare explicitement `q` comme étant un paramètre de requête. + +!!! info + Gardez à l'esprit que la partie la plus importante pour rendre un paramètre optionnel est : + + ```Python + = None + ``` + + ou : + + ```Python + = Query(None) + ``` + + et utilisera ce `None` pour détecter que ce paramètre de requête **n'est pas requis**. + + Le `Union[str, None]` est uniquement là pour permettre à votre éditeur un meilleur support. + +Ensuite, nous pouvons passer d'autres paramètres à `Query`. Dans cet exemple, le paramètre `max_length` qui s'applique aux chaînes de caractères : + +```Python +q: Union[str, None] = Query(default=None, max_length=50) +``` + +Cela va valider les données, montrer une erreur claire si ces dernières ne sont pas valides, et documenter le paramètre dans le schéma `OpenAPI` de cette *path operation*. + +## Rajouter plus de validation + +Vous pouvez aussi rajouter un second paramètre `min_length` : + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial003.py!} +``` + +## Ajouter des validations par expressions régulières + +On peut définir une expression régulière à laquelle le paramètre doit correspondre : + +```Python hl_lines="10" +{!../../../docs_src/query_params_str_validations/tutorial004.py!} +``` + +Cette expression régulière vérifie que la valeur passée comme paramètre : + +* `^` : commence avec les caractères qui suivent, avec aucun caractère avant ceux-là. +* `fixedquery` : a pour valeur exacte `fixedquery`. +* `$` : se termine directement ensuite, n'a pas d'autres caractères après `fixedquery`. + +Si vous vous sentez perdu avec le concept d'**expression régulière**, pas d'inquiétudes. Il s'agit d'une notion difficile pour beaucoup, et l'on peut déjà réussir à faire beaucoup sans jamais avoir à les manipuler. + +Mais si vous décidez d'apprendre à les utiliser, sachez qu'ensuite vous pouvez les utiliser directement dans **FastAPI**. + +## Valeurs par défaut + +De la même façon que vous pouvez passer `None` comme premier argument pour l'utiliser comme valeur par défaut, vous pouvez passer d'autres valeurs. + +Disons que vous déclarez le paramètre `q` comme ayant une longueur minimale de `3`, et une valeur par défaut étant `"fixedquery"` : + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial005.py!} +``` + +!!! note "Rappel" + Avoir une valeur par défaut rend le paramètre optionnel. + +## Rendre ce paramètre requis + +Quand on ne déclare ni validation, ni métadonnée, on peut rendre le paramètre `q` requis en ne lui déclarant juste aucune valeur par défaut : + +```Python +q: str +``` + +à la place de : + +```Python +q: Union[str, None] = None +``` + +Mais maintenant, on déclare `q` avec `Query`, comme ceci : + +```Python +q: Union[str, None] = Query(default=None, min_length=3) +``` + +Donc pour déclarer une valeur comme requise tout en utilisant `Query`, il faut utiliser `...` comme premier argument : + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial006.py!} +``` + +!!! info + Si vous n'avez jamais vu ce `...` auparavant : c'est une des constantes natives de Python appelée "Ellipsis". + +Cela indiquera à **FastAPI** que la présence de ce paramètre est obligatoire. + +## Liste de paramètres / valeurs multiples via Query + +Quand on définit un paramètre de requête explicitement avec `Query` on peut aussi déclarer qu'il reçoit une liste de valeur, ou des "valeurs multiples". + +Par exemple, pour déclarer un paramètre de requête `q` qui peut apparaître plusieurs fois dans une URL, on écrit : + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial011.py!} +``` + +Ce qui fait qu'avec une URL comme : + +``` +http://localhost:8000/items/?q=foo&q=bar +``` + +vous recevriez les valeurs des multiples paramètres de requête `q` (`foo` et `bar`) dans une `list` Python au sein de votre fonction de **path operation**, dans le paramètre de fonction `q`. + +Donc la réponse de cette URL serait : + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +!!! tip "Astuce" + Pour déclarer un paramètre de requête de type `list`, comme dans l'exemple ci-dessus, il faut explicitement utiliser `Query`, sinon cela sera interprété comme faisant partie du corps de la requête. + +La documentation sera donc mise à jour automatiquement pour autoriser plusieurs valeurs : + + + +### Combiner liste de paramètres et valeurs par défaut + +Et l'on peut aussi définir une liste de valeurs par défaut si aucune n'est fournie : + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial012.py!} +``` + +Si vous allez à : + +``` +http://localhost:8000/items/ +``` + +la valeur par défaut de `q` sera : `["foo", "bar"]` + +et la réponse sera : + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +#### Utiliser `list` + +Il est aussi possible d'utiliser directement `list` plutôt que `List[str]` : + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial013.py!} +``` + +!!! note + Dans ce cas-là, **FastAPI** ne vérifiera pas le contenu de la liste. + + Par exemple, `List[int]` vérifiera (et documentera) que la liste est bien entièrement composée d'entiers. Alors qu'un simple `list` ne ferait pas cette vérification. + +## Déclarer des métadonnées supplémentaires + +On peut aussi ajouter plus d'informations sur le paramètre. + +Ces informations seront incluses dans le schéma `OpenAPI` généré et utilisées par la documentation interactive ou les outils externes utilisés. + +!!! note + Gardez en tête que les outils externes utilisés ne supportent pas forcément tous parfaitement OpenAPI. + + Il se peut donc que certains d'entre eux n'utilisent pas toutes les métadonnées que vous avez déclarées pour le moment, bien que dans la plupart des cas, les fonctionnalités manquantes ont prévu d'être implémentées. + +Vous pouvez ajouter un `title` : + +```Python hl_lines="10" +{!../../../docs_src/query_params_str_validations/tutorial007.py!} +``` + +Et une `description` : + +```Python hl_lines="13" +{!../../../docs_src/query_params_str_validations/tutorial008.py!} +``` + +## Alias de paramètres + +Imaginez que vous vouliez que votre paramètre se nomme `item-query`. + +Comme dans la requête : + +``` +http://127.0.0.1:8000/items/?item-query=foobaritems +``` + +Mais `item-query` n'est pas un nom de variable valide en Python. + +Le nom le plus proche serait `item_query`. + +Mais vous avez vraiment envie que ce soit exactement `item-query`... + +Pour cela vous pouvez déclarer un `alias`, et cet alias est ce qui sera utilisé pour trouver la valeur du paramètre : + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial009.py!} +``` + +## Déprécier des paramètres + +Disons que vous ne vouliez plus utiliser ce paramètre désormais. + +Il faut qu'il continue à exister pendant un certain temps car vos clients l'utilisent, mais vous voulez que la documentation mentionne clairement que ce paramètre est déprécié. + +On utilise alors l'argument `deprecated=True` de `Query` : + +```Python hl_lines="18" +{!../../../docs_src/query_params_str_validations/tutorial010.py!} +``` + +La documentation le présentera comme il suit : + + + +## Pour résumer + +Il est possible d'ajouter des validateurs et métadonnées pour vos paramètres. + +Validateurs et métadonnées génériques: + +* `alias` +* `title` +* `description` +* `deprecated` + +Validateurs spécifiques aux chaînes de caractères : + +* `min_length` +* `max_length` +* `regex` + +Parmi ces exemples, vous avez pu voir comment déclarer des validateurs pour les chaînes de caractères. + +Dans les prochains chapitres, vous verrez comment déclarer des validateurs pour d'autres types, comme les nombres. diff --git a/docs/fr/docs/tutorial/query-params.md b/docs/fr/docs/tutorial/query-params.md index 7bf3b9e794..962135f63e 100644 --- a/docs/fr/docs/tutorial/query-params.md +++ b/docs/fr/docs/tutorial/query-params.md @@ -75,7 +75,7 @@ Ici, le paramètre `q` sera optionnel, et aura `None` comme valeur par défaut. !!! note **FastAPI** saura que `q` est optionnel grâce au `=None`. - Le `Optional` dans `Optional[str]` n'est pas utilisé par **FastAPI** (**FastAPI** n'en utilisera que la partie `str`), mais il servira tout de même à votre editeur de texte pour détecter des erreurs dans votre code. + Le `Optional` dans `Optional[str]` n'est pas utilisé par **FastAPI** (**FastAPI** n'en utilisera que la partie `str`), mais il servira tout de même à votre éditeur de texte pour détecter des erreurs dans votre code. ## Conversion des types des paramètres de requête diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 7dce4b1276..de18856f44 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -1,169 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/fr/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: fr -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- Tutoriel - Guide utilisateur: - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/background-tasks.md -- Guide utilisateur avancé: - - advanced/additional-status-codes.md - - advanced/additional-responses.md -- async.md -- Déploiement: - - deployment/index.md - - deployment/versions.md - - deployment/https.md - - deployment/deta.md - - deployment/docker.md -- project-generation.md -- alternatives.md -- history-design-future.md -- external-links.md -- help-fastapi.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs/he/docs/index.md b/docs/he/docs/index.md index 19f2f20413..802dbe8b5d 100644 --- a/docs/he/docs/index.md +++ b/docs/he/docs/index.md @@ -440,7 +440,6 @@ item: Item בשימוש Pydantic: -- ujson - "פרסור" JSON. - email_validator - לאימות כתובות אימייל. בשימוש Starlette: diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index 3279099b5a..de18856f44 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -1,145 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/he/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: he -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs/hu/docs/index.md b/docs/hu/docs/index.md new file mode 100644 index 0000000000..29c3c05ac6 --- /dev/null +++ b/docs/hu/docs/index.md @@ -0,0 +1,469 @@ +

+ FastAPI +

+

+ FastAPI keretrendszer, nagy teljesítmény, könnyen tanulható, gyorsan kódolható, productionre kész +

+

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

+ +--- + +**Dokumentáció**: https://fastapi.tiangolo.com + +**Forrás kód**: https://github.com/tiangolo/fastapi + +--- +A FastAPI egy modern, gyors (nagy teljesítményű), webes keretrendszer API-ok építéséhez Python 3.8+-al, a Python szabványos típusjelöléseire építve. + + +Kulcs funkciók: + +* **Gyors**: Nagyon nagy teljesítmény, a **NodeJS**-el és a **Go**-val egyenrangú (a Starlettenek és a Pydantic-nek köszönhetően). [Az egyik leggyorsabb Python keretrendszer](#performance). +* **Gyorsan kódolható**: A funkciók fejlesztési sebességét 200-300 százalékkal megnöveli. * +* **Kevesebb hiba**: Körülbelül 40%-al csökkenti az emberi (fejlesztői) hibák számát. * +* **Intuitív**: Kiváló szerkesztő támogatás. Kiegészítés mindenhol. Kevesebb hibakereséssel töltött idő. +* **Egyszerű**: Egyszerű tanulásra és használatra tervezve. Kevesebb dokumentáció olvasással töltött idő. +* **Rövid**: Kód duplikáció minimalizálása. Több funkció minden paraméter deklarálásával. Kevesebb hiba. +* **Robosztus**: Production ready kód. Automatikus interaktív dokumentáció val. +* **Szabvány alapú**: Az API-ok nyílt szabványaira alapuló (és azokkal teljesen kompatibilis): OpenAPI (korábban Swagger néven ismert) és a JSON Schema. + +* Egy production alkalmazásokat építő belső fejlesztői csapat tesztjein alapuló becslés. + +## Szponzorok + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +További szponzorok + +## Vélemények + +"_[...] 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)
+ +--- + +"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## **Typer**, a CLI-ok FastAPI-ja + + + +Ha egy olyan CLI alkalmazást fejlesztesz amit a parancssorban kell használni webes API helyett, tekintsd meg: **Typer**. + +**Typer** a FastAPI kistestvére. A **CLI-k FastAPI-ja**. ⌨️ 🚀 + +## Követelmények + +Python 3.8+ + +A FastAPI óriások vállán áll: + +* Starlette a webes részekhez. +* Pydantic az adat részekhez. + +## Telepítés + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +A production-höz egy ASGI szerverre is szükség lesz, mint például az Uvicorn vagy a Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## Példa + +### Hozd létre + +* Hozz létre a `main.py` fájlt a következő tartalommal: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+Vagy használd az async def-et... + +Ha a kódod `async` / `await`-et, használ `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Megjegyzés**: + +Ha nem tudod, tekintsd meg a _"Sietsz?"_ szekciót `async` és `await`-ről dokumentációba. + +
+ +### Futtasd le + +Indítsd el a szervert a következő paranccsal: + +
+ +```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. +``` + +
+ +
+A parancsról uvicorn main:app --reload... + +A `uvicorn main:app` parancs a következőre utal: + +* `main`: fájl `main.py` (a Python "modul"). +* `app`: a `main.py`-ban a `app = FastAPI()` sorral létrehozott objektum. +* `--reload`: kód változtatás esetén újra indítja a szervert. Csak fejlesztés közben használandó. + +
+ +### Ellenőrizd + +Nyisd meg a böngésződ a következő címen: http://127.0.0.1:8000/items/5?q=somequery. + +A következő JSON választ fogod látni: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +Máris létrehoztál egy API-t ami: + +* HTTP kéréseket fogad a `/` és `/items/{item_id}` _útvonalakon_. +* Mindkét _útvonal_ a `GET` műveletet használja (másik elnevezés: HTTP _metódus_). +* A `/items/{item_id}` _útvonalnak_ van egy _path paramétere_, az `item_id`, aminek `int` típusúnak kell lennie. +* A `/items/{item_id}` _útvonalnak_ még van egy opcionális, `str` típusú _query paramétere_ is, a `q`. + +### Interaktív API dokumentáció + +Most nyisd meg a http://127.0.0.1:8000/docs címet. + +Az automatikus interaktív API dokumentációt fogod látni (amit a Swagger UI-al hozunk létre): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternatív API dokumentáció + +És most menj el a http://127.0.0.1:8000/redoc címre. + +Az alternatív automatikus dokumentációt fogod látni. (lásd ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Példa frissítése + +Módosítsuk a `main.py` fájlt, hogy `PUT` kérések esetén tudjon body-t fogadni. + +Deklaráld a body-t standard Python típusokkal, a Pydantic-nak köszönhetően. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +A szerver automatikusan újraindul (mert hozzáadtuk a --reload paramétert a fenti `uvicorn` parancshoz). + +### Interaktív API dokumentáció frissítése + +Most menj el a http://127.0.0.1:8000/docs címre. + +* Az interaktív API dokumentáció automatikusan frissült így már benne van az új body. + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Kattints rá a "Try it out" gombra, ennek segítségével kitöltheted a paramétereket és közvetlen használhatod az API-t: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Ezután kattints az "Execute" gompra, a felhasználói felület kommunikálni fog az API-oddal. Elküldi a paramétereket és a visszakapott választ megmutatja a képernyődön. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternatív API dokumentáció frissítés + +Most menj el a http://127.0.0.1:8000/redoc címre. + +* Az alternatív dokumentáció szintúgy tükrözni fogja az új kérési paraméter és body-t. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Összefoglalás + +Összegzésül, deklarálod **egyszer** a paraméterek, body, stb típusát funkciós paraméterekként. + +Ezt standard modern Python típusokkal csinálod. + +Nem kell új szintaxist, vagy specifikus könyvtár mert metódósait, stb. megtanulnod. + +Csak standard **Python 3.8+**. + +Például egy `int`-nek: + +```Python +item_id: int +``` + +Egy komplexebb `Item` modellnek: + +```Python +item: Item +``` + +... És csupán egy deklarációval megkapod a: + +* Szerkesztő támogatást, beleértve: + * Szövegkiegészítés. + * Típus ellenőrzés. +* Adatok validációja: + * Automatikus és érthető hibák amikor az adatok hibásak. + * Validáció mélyen ágyazott objektumok esetén is. +* Bemeneti adatok átváltása : a hálózatról érkező Python adatokká és típusokká. Adatok olvasása következő forrásokból: + * JSON. + * Cím paraméterek. + * Query paraméterek. + * Cookie-k. + * Header-ök. + * Formok. + * Fájlok. +* Kimeneti adatok átváltása: Python adatok is típusokról hálózati adatokká: + * válts át Python típusokat (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` csak objektumokat. + * `UUID` objektumokat. + * Adatbázis modelleket. + * ...És sok mást. +* Automatikus interaktív dokumentáció, beleértve két alternatív dokumentációt is: + * Swagger UI. + * ReDoc. + +--- + +Visszatérve az előző kód példához. A **FastAPI**: + +* Validálja hogy van egy `item_id` mező a `GET` és `PUT` kérésekben. +* Validálja hogy az `item_id` `int` típusú a `GET` és `PUT` kérésekben. + * Ha nem akkor látni fogunk egy tiszta hibát ezzel kapcsolatban. +* ellenőrzi hogyha van egy opcionális query paraméter `q` névvel (azaz `http://127.0.0.1:8000/items/foo?q=somequery`) `GET` kérések esetén. + * Mivel a `q` paraméter `= None`-al van deklarálva, ezért opcionális. + * `None` nélkül ez a mező kötelező lenne (mint például a body `PUT` kérések esetén). +* a `/items/{item_id}` címre érkező `PUT` kérések esetén, a JSON-t a következőképpen olvassa be: + * Ellenőrzi hogy létezik a kötelező `name` nevű attribútum és `string`. + * Ellenőrzi hogy létezik a kötelező `price` nevű attribútum és `float`. + * Ellenőrzi hogy létezik a `is_offer` nevű opcionális paraméter, ami ha létezik akkor `bool` + * Ez ágyazott JSON objektumokkal is működik +* JSONről való automatikus konvertálás. +* dokumentáljuk mindent OpenAPI-al amit használható: + * Interaktív dokumentációs rendszerekkel. + * Automatikus kliens kód generáló a rendszerekkel, több nyelven. +* Hozzá tartozik kettő interaktív dokumentációs web felület. + +--- + +Eddig csak a felszínt kapargattuk, de a lényeg hogy most már könnyebben érthető hogyan működik. + +Próbáld kicserélni a következő sorban: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...ezt: + +```Python + ... "item_name": item.name ... +``` + +...erre: + +```Python + ... "item_price": item.price ... +``` + +... És figyeld meg hogy a szerkesztő automatikusan tudni fogja a típusokat és kiegészíti azokat: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +Teljesebb példákért és funkciókért tekintsd meg a Tutorial - User Guide -t. + +**Spoiler veszély**: a Tutorial - User Guidehoz tartozik: + +* **Paraméterek** deklarációja különböző helyekről: **header-ök**, **cookie-k**, **form mezők** és **fájlok**. +* Hogyan állíts be **validációs feltételeket** mint a `maximum_length` vagy a `regex`. +* Nagyon hatékony és erős **Függőség Injekció** rendszerek. +* Biztonság és autentikáció beleértve, **OAuth2**, **JWT tokens** és **HTTP Basic** támogatást. +* Több haladó (de ugyanannyira könnyű) technika **mélyen ágyazott JSON modellek deklarációjára** (Pydantic-nek köszönhetően). +* **GraphQL** integráció Strawberry-vel és más könyvtárakkal. +* több extra funkció (Starlette-nek köszönhetően) pl.: + * **WebSockets** + * rendkívül könnyű tesztek HTTPX és `pytest` alapokra építve + * **CORS** + * **Cookie Sessions** + * ...és több. + +## Teljesítmény + +A független TechEmpower benchmarkok szerint az Uvicorn alatt futó **FastAPI** alkalmazások az egyik leggyorsabb Python keretrendszerek közé tartoznak, éppen lemaradva a Starlette és az Uvicorn (melyeket a FastAPI belsőleg használ) mögött.(*) + +Ezeknek a további megértéséhez: Benchmarks. + +## Opcionális követelmények + +Pydantic által használt: + +* email_validator - e-mail validációkra. +* pydantic-settings - Beállítások követésére. +* pydantic-extra-types - Extra típusok Pydantic-hoz. + +Starlette által használt: + +* httpx - Követelmény ha a `TestClient`-et akarod használni. +* jinja2 - Követelmény ha az alap template konfigurációt akarod használni. +* python-multipart - Követelmény ha "parsing"-ot akarsz támogatni, `request.form()`-al. +* itsdangerous - Követelmény `SessionMiddleware` támogatáshoz. +* pyyaml - Követelmény a Starlette `SchemaGenerator`-ának támogatásához (valószínűleg erre nincs szükség FastAPI használása esetén). +* ujson - Követelmény ha `UJSONResponse`-t akarsz használni. + +FastAPI / Starlette által használt + +* uvicorn - Szerverekhez amíg betöltik és szolgáltatják az applikációdat. +* orjson - Követelmény ha `ORJSONResponse`-t akarsz használni. + +Ezeket mind telepítheted a `pip install "fastapi[all]"` paranccsal. + +## Licensz +Ez a projekt az MIT license, licensz alatt fut diff --git a/docs/hu/mkdocs.yml b/docs/hu/mkdocs.yml new file mode 100644 index 0000000000..de18856f44 --- /dev/null +++ b/docs/hu/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml diff --git a/docs/id/docs/index.md b/docs/id/docs/index.md deleted file mode 100644 index 66fc2859e7..0000000000 --- a/docs/id/docs/index.md +++ /dev/null @@ -1,466 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**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. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## 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.7+ - -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[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Optional - -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: Optional[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="9 14" -from typing import Optional - -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: Optional[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="4 9-12 25-27" -from typing import Optional - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Optional[bool] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[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 HTTPX 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: - -* httpx - Required if you want to use the `TestClient`. -* 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/id/docs/tutorial/index.md b/docs/id/docs/tutorial/index.md index 8fec3c087e..b8ed96ae1f 100644 --- a/docs/id/docs/tutorial/index.md +++ b/docs/id/docs/tutorial/index.md @@ -10,9 +10,9 @@ Sehingga kamu dapat kembali lagi dan mencari apa yang kamu butuhkan dengan tepat ## Jalankan kode -Semua blok-blok kode dapat dicopy dan digunakan langsung (Mereka semua sebenarnya adalah file python yang sudah teruji). +Semua blok-blok kode dapat disalin dan digunakan langsung (Mereka semua sebenarnya adalah file python yang sudah teruji). -Untuk menjalankan setiap contoh, copy kode ke file `main.py`, dan jalankan `uvicorn` dengan: +Untuk menjalankan setiap contoh, salin kode ke file `main.py`, dan jalankan `uvicorn` dengan:
@@ -28,7 +28,7 @@ $ uvicorn main:app --reload
-**SANGAT disarankan** agar kamu menulis atau meng-copy kode, meng-editnya dan menjalankannya secara lokal. +**SANGAT disarankan** agar kamu menulis atau menyalin kode, mengubahnya dan menjalankannya secara lokal. Dengan menggunakannya di dalam editor, benar-benar memperlihatkan manfaat dari FastAPI, melihat bagaimana sedikitnya kode yang harus kamu tulis, semua pengecekan tipe, pelengkapan otomatis, dll. @@ -38,7 +38,7 @@ Dengan menggunakannya di dalam editor, benar-benar memperlihatkan manfaat dari F Langkah pertama adalah dengan meng-install FastAPI. -Untuk tutorial, kamu mungkin hendak meng-instalnya dengan semua pilihan fitur dan dependensinya: +Untuk tutorial, kamu mungkin hendak meng-installnya dengan semua pilihan fitur dan dependensinya:
@@ -53,15 +53,15 @@ $ pip install "fastapi[all]" ...yang juga termasuk `uvicorn`, yang dapat kamu gunakan sebagai server yang menjalankan kodemu. !!! catatan - Kamu juga dapat meng-instalnya bagian demi bagian. + Kamu juga dapat meng-installnya bagian demi bagian. - Hal ini mungkin yang akan kamu lakukan ketika kamu hendak men-deploy aplikasimu ke tahap produksi: + Hal ini mungkin yang akan kamu lakukan ketika kamu hendak menyebarkan (men-deploy) aplikasimu ke tahap produksi: ``` pip install fastapi ``` - Juga install `uvicorn` untk menjalankan server" + Juga install `uvicorn` untuk menjalankan server" ``` pip install "uvicorn[standard]" @@ -77,4 +77,4 @@ Tersedia juga **Pedoman Pengguna Lanjutan** yang dapat kamu baca nanti setelah * Tetapi kamu harus membaca terlebih dahulu **Tutorial - Pedoman Pengguna** (apa yang sedang kamu baca sekarang). -Hal ini didesain sehingga kamu dapat membangun aplikasi lengkap dengan hanya **Tutorial - Pedoman Pengguna**, dan kemudian mengembangkannya ke banyak cara yang berbeda, tergantung dari kebutuhanmu, menggunakan beberapa ide-ide tambahan dari **Pedoman Pengguna Lanjutan**. +Hal ini dirancang supaya kamu dapat membangun aplikasi lengkap dengan hanya **Tutorial - Pedoman Pengguna**, dan kemudian mengembangkannya ke banyak cara yang berbeda, tergantung dari kebutuhanmu, menggunakan beberapa ide-ide tambahan dari **Pedoman Pengguna Lanjutan**. diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index abb31252f6..de18856f44 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -1,145 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/id/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: id -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs/it/docs/index.md b/docs/it/docs/index.md deleted file mode 100644 index 9d95dd6d72..0000000000 --- a/docs/it/docs/index.md +++ /dev/null @@ -1,463 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Build Status - - - Coverage - - - Package version - -

- ---- - -**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. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## 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.7+ - -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[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from fastapi import FastAPI -from typing import Optional - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: str = Optional[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 -from typing import Optional - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[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-10 23-25" -from fastapi import FastAPI -from pydantic import BaseModel -from typing import Optional - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: bool = Optional[None] - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[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 HTTPX 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: - -* httpx - Required if you want to use the `TestClient`. -* 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 deleted file mode 100644 index 532b5bc52c..0000000000 --- a/docs/it/mkdocs.yml +++ /dev/null @@ -1,145 +0,0 @@ -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 - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - 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: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/ja/docs/advanced/index.md b/docs/ja/docs/advanced/index.md index 676f60359f..0732fc405a 100644 --- a/docs/ja/docs/advanced/index.md +++ b/docs/ja/docs/advanced/index.md @@ -1,4 +1,4 @@ -# ユーザーガイド 応用編 +# 高度なユーザーガイド ## さらなる機能 diff --git a/docs/ja/docs/contributing.md b/docs/ja/docs/contributing.md index 9affea443a..31db51c52b 100644 --- a/docs/ja/docs/contributing.md +++ b/docs/ja/docs/contributing.md @@ -97,7 +97,7 @@ $ python -m venv env
```console -$ pip install -e ."[dev,doc,test]" +$ pip install -r requirements.txt ---> 100% ``` diff --git a/docs/ja/docs/deployment/concepts.md b/docs/ja/docs/deployment/concepts.md new file mode 100644 index 0000000000..38cbca2192 --- /dev/null +++ b/docs/ja/docs/deployment/concepts.md @@ -0,0 +1,323 @@ +# デプロイメントのコンセプト + +**FastAPI**を用いたアプリケーションをデプロイするとき、もしくはどのようなタイプのWeb APIであっても、おそらく気になるコンセプトがいくつかあります。 + +それらを活用することでアプリケーションを**デプロイするための最適な方法**を見つけることができます。 + +重要なコンセプトのいくつかを紹介します: + +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* レプリケーション(実行中のプロセス数) +* メモリー +* 開始前の事前のステップ + +これらが**デプロイメント**にどのような影響を与えるかを見ていきましょう。 + +最終的な目的は、**安全な方法で**APIクライアントに**サービスを提供**し、**中断を回避**するだけでなく、**計算リソース**(例えばリモートサーバー/仮想マシン)を可能な限り効率的に使用することです。🚀 + +この章では前述した**コンセプト**についてそれぞれ説明します。 + +この説明を通して、普段とは非常に異なる環境や存在しないであろう**将来の**環境に対し、デプロイの方法を決める上で必要な**直感**を与えてくれることを願っています。 + +これらのコンセプトを意識することにより、**あなた自身のAPI**をデプロイするための最適な方法を**評価**し、**設計**することができるようになるでしょう。 + +次の章では、FastAPIアプリケーションをデプロイするための**具体的なレシピ**を紹介します。 + +しかし、今はこれらの重要な**コンセプトに基づくアイデア**を確認しましょう。これらのコンセプトは、他のどのタイプのWeb APIにも当てはまります。💡 + +## セキュリティ - HTTPS + + +[前チャプターのHTTPSについて](./https.md){.internal-link target=_blank}では、HTTPSがどのようにAPIを暗号化するのかについて学びました。 + +通常、アプリケーションサーバにとって**外部の**コンポーネントである**TLS Termination Proxy**によって提供されることが一般的です。このプロキシは通信の暗号化を担当します。 + +さらにセキュアな通信において、HTTPS証明書の定期的な更新を行いますが、これはTLS Termination Proxyと同じコンポーネントが担当することもあれば、別のコンポーネントが担当することもあります。 + +### HTTPS 用ツールの例 +TLS Termination Proxyとして使用できるツールには以下のようなものがあります: + +* Traefik + * 証明書の更新を自動的に処理 ✨ +* Caddy + * 証明書の更新を自動的に処理 ✨ +* Nginx + * 証明書更新のためにCertbotのような外部コンポーネントを使用 +* HAProxy + * 証明書更新のためにCertbotのような外部コンポーネントを使用 +* Nginx のような Ingress Controller を持つ Kubernetes + * 証明書の更新に cert-manager のような外部コンポーネントを使用 +* クラウド・プロバイダーがサービスの一部として内部的に処理(下記を参照👇) + +もう1つの選択肢は、HTTPSのセットアップを含んだより多くの作業を行う**クラウド・サービス**を利用することです。 このサービスには制限があったり、料金が高くなったりする可能性があります。しかしその場合、TLS Termination Proxyを自分でセットアップする必要はないです。 + +次の章で具体例をいくつか紹介します。 + +--- + +次に考慮すべきコンセプトは、実際のAPIを実行するプログラム(例:Uvicorn)に関連するものすべてです。 + +## プログラム と プロセス + +私たちは「**プロセス**」という言葉についてたくさん話すので、その意味や「**プログラム**」という言葉との違いを明確にしておくと便利です。 + +### プログラムとは何か + +**プログラム**という言葉は、一般的にいろいろなものを表現するのに使われます: + +* プログラマが書く**コード**、**Pythonファイル** +* OSによって実行することができるファイル(例: `python`, `python.exe` or `uvicorn`) +* OS上で**実行**している間、CPUを使用し、メモリ上に何かを保存する特定のプログラム(**プロセス**とも呼ばれる) + +### プロセスとは何か + +**プロセス**という言葉は通常、より具体的な意味で使われ、OSで実行されているものだけを指します(先ほどの最後の説明のように): + +* OS上で**実行**している特定のプログラム + * これはファイルやコードを指すのではなく、OSによって**実行**され、管理されているものを指します。 +* どんなプログラムやコードも、それが**実行されているときにだけ機能**します。つまり、**プロセスとして実行されているときだけ**です。 +* プロセスは、ユーザーにあるいはOSによって、 **終了**(あるいは "kill")させることができます。その時点で、プロセスは実行/実行されることを停止し、それ以降は**何もできなくなります**。 +* コンピュータで実行されている各アプリケーションは、実行中のプログラムや各ウィンドウなど、その背後にいくつかのプロセスを持っています。そして通常、コンピュータが起動している間、**多くのプロセスが**同時に実行されています。 +* **同じプログラム**の**複数のプロセス**が同時に実行されていることがあります。 + +OSの「タスク・マネージャー」や「システム・モニター」(または同様のツール)を確認すれば、これらのプロセスの多くが実行されているの見ることができるでしょう。 + +例えば、同じブラウザプログラム(Firefox、Chrome、Edgeなど)を実行しているプロセスが複数あることがわかります。通常、1つのタブにつき1つのプロセスが実行され、さらに他のプロセスも実行されます。 + + + +--- + +さて、**プロセス**と**プログラム**という用語の違いを確認したところで、デプロイメントについて話を続けます。 + +## 起動時の実行 + +ほとんどの場合、Web APIを作成するときは、クライアントがいつでもアクセスできるように、**常に**中断されることなく**実行される**ことを望みます。もちろん、特定の状況でのみ実行させたい特別な理由がある場合は別ですが、その時間のほとんどは、常に実行され、**利用可能**であることを望みます。 + +### リモートサーバー上での実行 + +リモートサーバー(クラウドサーバー、仮想マシンなど)をセットアップするときにできる最も簡単なことは、ローカルで開発するときと同じように、Uvicorn(または同様のもの)を手動で実行することです。 この方法は**開発中**には役に立つと思われます。 + +しかし、サーバーへの接続が切れた場合、**実行中のプロセス**はおそらくダウンしてしまうでしょう。 + +そしてサーバーが再起動された場合(アップデートやクラウドプロバイダーからのマイグレーションの後など)、おそらくあなたはそれに**気づかないでしょう**。そのため、プロセスを手動で再起動しなければならないことすら気づかないでしょう。つまり、APIはダウンしたままなのです。😱 + +### 起動時に自動的に実行 + +一般的に、サーバープログラム(Uvicornなど)はサーバー起動時に自動的に開始され、**人の介入**を必要とせずに、APIと一緒にプロセスが常に実行されるようにしたいと思われます(UvicornがFastAPIアプリを実行するなど)。 + +### 別のプログラムの用意 + +これを実現するために、通常は**別のプログラム**を用意し、起動時にアプリケーションが実行されるようにします。そして多くの場合、他のコンポーネントやアプリケーション、例えばデータベースも実行されるようにします。 + +### 起動時に実行するツールの例 + +実行するツールの例をいくつか挙げます: + +* Docker +* Kubernetes +* Docker Compose +* Swarm モードによる Docker +* Systemd +* Supervisor +* クラウドプロバイダーがサービスの一部として内部的に処理 +* そのほか... + +次の章で、より具体的な例を挙げていきます。 + +## 再起動 + +起動時にアプリケーションが実行されることを確認するのと同様に、失敗後にアプリケーションが**再起動**されることも確認したいと思われます。 + +### 我々は間違いを犯す + +私たち人間は常に**間違い**を犯します。ソフトウェアには、ほとんど常に**バグ**があらゆる箇所に隠されています。🐛 + +### 小さなエラーは自動的に処理される + +FastAPIでWeb APIを構築する際に、コードにエラーがある場合、FastAPIは通常、エラーを引き起こした単一のリクエストにエラーを含めます。🛡 + +クライアントはそのリクエストに対して**500 Internal Server Error**を受け取りますが、アプリケーションは完全にクラッシュするのではなく、次のリクエストのために動作を続けます。 + +### 重大なエラー - クラッシュ + +しかしながら、**アプリケーション全体をクラッシュさせるようなコードを書いて**UvicornとPythonをクラッシュさせるようなケースもあるかもしれません。💥 + +それでも、ある箇所でエラーが発生したからといって、アプリケーションを停止させたままにしたくないでしょう。 少なくとも壊れていない*パスオペレーション*については、**実行し続けたい**はずです。 + +### クラッシュ後の再起動 + +しかし、実行中の**プロセス**をクラッシュさせるような本当にひどいエラーの場合、少なくとも2〜3回ほどプロセスを**再起動**させる外部コンポーネントが必要でしょう。 + +!!! tip + ...とはいえ、アプリケーション全体が**すぐにクラッシュする**のであれば、いつまでも再起動し続けるのは意味がないでしょう。しかし、その場合はおそらく開発中か少なくともデプロイ直後に気づくと思われます。 + + そこで、**将来**クラッシュする可能性があり、それでも再スタートさせることに意味があるような、主なケースに焦点を当ててみます。 + +あなたはおそらく**外部コンポーネント**がアプリケーションの再起動を担当することを望むと考えます。 なぜなら、その時点でUvicornとPythonを使った同じアプリケーションはすでにクラッシュしており、同じアプリケーションの同じコードに対して何もできないためです。 + +### 自動的に再起動するツールの例 + +ほとんどの場合、前述した**起動時にプログラムを実行する**ために使用されるツールは、自動で**再起動**することにも利用されます。 + +例えば、次のようなものがあります: + +* Docker +* Kubernetes +* Docker Compose +* Swarm モードによる Docker +* Systemd +* Supervisor +* クラウドプロバイダーがサービスの一部として内部的に処理 +* そのほか... + +## レプリケーション - プロセスとメモリー + +FastAPI アプリケーションでは、Uvicorn のようなサーバープログラムを使用し、**1つのプロセス**で1度に複数のクライアントに同時に対応できます。 + +しかし、多くの場合、複数のワーカー・プロセスを同時に実行したいと考えるでしょう。 + +### 複数のプロセス - Worker + +クライアントの数が単一のプロセスで処理できる数を超えており(たとえば仮想マシンがそれほど大きくない場合)、かつサーバーの CPU に**複数のコア**がある場合、同じアプリケーションで同時に**複数のプロセス**を実行させ、すべてのリクエストを分散させることができます。 + +同じAPIプログラムの**複数のプロセス**を実行する場合、それらは一般的に**Worker/ワーカー**と呼ばれます。 + +### ワーカー・プロセス と ポート + + +[HTTPSについて](./https.md){.internal-link target=_blank}のドキュメントで、1つのサーバーで1つのポートとIPアドレスの組み合わせでリッスンできるのは1つのプロセスだけであることを覚えていますでしょうか? + +これはいまだに同じです。 + +そのため、**複数のプロセス**を同時に持つには**ポートでリッスンしている単一のプロセス**が必要であり、それが何らかの方法で各ワーカー・プロセスに通信を送信することが求められます。 + +### プロセスあたりのメモリー + +さて、プログラムがメモリにロードする際には、例えば機械学習モデルや大きなファイルの内容を変数に入れたりする場合では、**サーバーのメモリ(RAM)**を少し消費します。 + +そして複数のプロセスは通常、**メモリを共有しません**。これは、実行中の各プロセスがそれぞれ独自の変数やメモリ等を持っていることを意味します。つまり、コード内で大量のメモリを消費している場合、**各プロセス**は同等の量のメモリを消費することになります。 + +### サーバーメモリー + +例えば、あなたのコードが **1GBのサイズの機械学習モデル**をロードする場合、APIで1つのプロセスを実行すると、少なくとも1GBのRAMを消費します。 + +また、**4つのプロセス**(4つのワーカー)を起動すると、それぞれが1GBのRAMを消費します。つまり、合計でAPIは**4GBのRAM**を消費することになります。 + +リモートサーバーや仮想マシンのRAMが3GBしかない場合、4GB以上のRAMをロードしようとすると問題が発生します。🚨 + +### 複数プロセス - 例 + +この例では、2つの**ワーカー・プロセス**を起動し制御する**マネージャー・ プロセス**があります。 + +このマネージャー・ プロセスは、おそらくIPの**ポート**でリッスンしているものです。そして、すべての通信をワーカー・プロセスに転送します。 + +これらのワーカー・プロセスは、アプリケーションを実行するものであり、**リクエスト**を受けて**レスポンス**を返すための主要な計算を行い、あなたが変数に入れたものは何でもRAMにロードします。 + + + +そしてもちろん、同じマシンでは、あなたのアプリケーションとは別に、**他のプロセス**も実行されているでしょう。 + +興味深いことに、各プロセスが使用する**CPU**の割合は時間とともに大きく**変動**する可能性がありますが、**メモリ(RAM)**は通常、多かれ少なかれ**安定**します。 + +毎回同程度の計算を行うAPIがあり、多くのクライアントがいるのであれば、**CPU使用率**もおそらく**安定**するでしょう(常に急激に上下するのではなく)。 + +### レプリケーション・ツールと戦略の例 + +これを実現するにはいくつかのアプローチがありますが、具体的な戦略については次の章(Dockerやコンテナの章など)で詳しく説明します。 + +考慮すべき主な制約は、**パブリックIP**の**ポート**を処理する**単一の**コンポーネントが存在しなければならないということです。 + +そして、レプリケートされた**プロセス/ワーカー**に通信を**送信**する方法を持つ必要があります。 + +考えられる組み合わせと戦略をいくつか紹介します: + +* **Gunicorn**が**Uvicornワーカー**を管理 + * Gunicornは**IP**と**ポート**をリッスンする**プロセスマネージャ**で、レプリケーションは**複数のUvicornワーカー・プロセス**を持つことによって行われる。 +* **Uvicorn**が**Uvicornワーカー**を管理 + * 1つのUvicornの**プロセスマネージャー**が**IP**と**ポート**をリッスンし、**複数のUvicornワーカー・プロセス**を起動する。 +* **Kubernetes**やその他の分散**コンテナ・システム** + * **Kubernetes**レイヤーの何かが**IP**と**ポート**をリッスンする。レプリケーションは、**複数のコンテナ**にそれぞれ**1つのUvicornプロセス**を実行させることで行われる。 +* **クラウド・サービス**によるレプリケーション + * クラウド・サービスはおそらく**あなたのためにレプリケーションを処理**します。**実行するプロセス**や使用する**コンテナイメージ**を定義できるかもしれませんが、いずれにせよ、それはおそらく**単一のUvicornプロセス**であり、クラウドサービスはそのレプリケーションを担当するでしょう。 + +!!! tip + これらの**コンテナ**やDockerそしてKubernetesに関する項目が、まだあまり意味をなしていなくても心配しないでください。 + + + コンテナ・イメージ、Docker、Kubernetesなどについては、次の章で詳しく説明します: [コンテナ内のFastAPI - Docker](./docker.md){.internal-link target=_blank}. + +## 開始前の事前のステップ + +アプリケーションを**開始する前**に、いくつかのステップを実行したい場合が多くあります。 + +例えば、**データベース・マイグレーション** を実行したいかもしれません。 + +しかしほとんどの場合、これらの手順を**1度**に実行したいと考えるでしょう。 + +そのため、アプリケーションを開始する前の**事前のステップ**を実行する**単一のプロセス**を用意したいと思われます。 + +そして、それらの事前のステップを実行しているのが単一のプロセスであることを確認する必要があります。このことはその後アプリケーション自体のために**複数のプロセス**(複数のワーカー)を起動した場合も同様です。 + +これらのステップが**複数のプロセス**によって実行された場合、**並列**に実行されることによって作業が**重複**することになります。そして、もしそのステップがデータベースのマイグレーションのような繊細なものであった場合、互いに競合を引き起こす可能性があります。 + +もちろん、事前のステップを何度も実行しても問題がない場合もあり、その際は対処がかなり楽になります。 + +!!! tip + また、セットアップによっては、アプリケーションを開始する前の**事前のステップ**が必要ない場合もあることを覚えておいてください。 + + その場合は、このようなことを心配する必要はないです。🤷 + +### 事前ステップの戦略例 + +これは**システムを**デプロイする方法に**大きく依存**するだろうし、おそらくプログラムの起動方法や再起動の処理などにも関係してくるでしょう。 + +考えられるアイデアをいくつか挙げてみます: + +* アプリコンテナの前に実行されるKubernetesのInitコンテナ +* 事前のステップを実行し、アプリケーションを起動するbashスクリプト + * 利用するbashスクリプトを起動/再起動したり、エラーを検出したりする方法は以前として必要になるでしょう。 + +!!! tip + + コンテナを使った具体的な例については、次の章で紹介します: [コンテナ内のFastAPI - Docker](./docker.md){.internal-link target=_blank}. + +## リソースの利用 + +あなたのサーバーは**リソース**であり、プログラムを実行しCPUの計算時間や利用可能なRAMメモリを消費または**利用**することができます。 + +システムリソースをどれくらい消費/利用したいですか? 「少ない方が良い」と考えるのは簡単かもしれないですが、実際には、**クラッシュせずに可能な限り**最大限に活用したいでしょう。 + +3台のサーバーにお金を払っているにも関わらず、そのRAMとCPUを少ししか使っていないとしたら、おそらく**お金を無駄にしている** 💸、おそらく**サーバーの電力を無駄にしている** 🌎ことになるでしょう。 + +その場合は、サーバーを2台だけにして、そのリソース(CPU、メモリ、ディスク、ネットワーク帯域幅など)をより高い割合で使用する方がよいでしょう。 + +一方、2台のサーバーがあり、そのCPUとRAMの**100%を使用している**場合、ある時点で1つのプロセスがより多くのメモリを要求し、サーバーはディスクを「メモリ」として使用しないといけません。(何千倍も遅くなる可能性があります。) +もしくは**クラッシュ**することもあれば、あるいはあるプロセスが何らかの計算をする必要があり、そしてCPUが再び空くまで待たなければならないかもしれません。 + +この場合、**1つ余分なサーバー**を用意し、その上でいくつかのプロセスを実行し、すべてのサーバーが**十分なRAMとCPU時間を持つようにする**のがよいでしょう。 + +また、何らかの理由でAPIの利用が急増する可能性もあります。もしかしたらそれが流行ったのかもしれないし、他のサービスやボットが使い始めたのかもしれないです。そのような場合に備えて、余分なリソースを用意しておくと安心でしょう。 + +例えば、リソース使用率の**50%から90%の範囲**で**任意の数字**をターゲットとすることができます。 + +重要なのは、デプロイメントを微調整するためにターゲットを設定し測定することが、おそらく使用したい主要な要素であることです。 + +`htop`のような単純なツールを使って、サーバーで使用されているCPUやRAM、あるいは各プロセスで使用されている量を見ることができます。あるいは、より複雑な監視ツールを使って、サーバに分散して使用することもできます。 + +## まとめ + +アプリケーションのデプロイ方法を決定する際に、考慮すべきであろう主要なコンセプトのいくつかを紹介していきました: + +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* レプリケーション(実行中のプロセス数) +* メモリー +* 開始前の事前ステップ + +これらの考え方とその適用方法を理解することで、デプロイメントを設定したり調整したりする際に必要な直感的な判断ができるようになるはずです。🤓 + +次のセクションでは、あなたが取り得る戦略について、より具体的な例を挙げます。🚀 diff --git a/docs/ja/docs/deployment/deta.md b/docs/ja/docs/deployment/deta.md deleted file mode 100644 index 723f169a00..0000000000 --- a/docs/ja/docs/deployment/deta.md +++ /dev/null @@ -1,240 +0,0 @@ -# Deta にデプロイ - -このセクションでは、**FastAPI** アプリケーションを Deta の無料プランを利用して、簡単にデプロイする方法を学習します。🎁 - -所要時間は約**10分**です。 - -!!! info "備考" - Deta は **FastAPI** のスポンサーです。🎉 - -## ベーシックな **FastAPI** アプリ - -* アプリのためのディレクトリ (例えば `./fastapideta/`) を作成し、その中に入ってください。 - -### FastAPI のコード - -* 以下の `main.py` ファイルを作成してください: - -```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): - return {"item_id": item_id} -``` - -### Requirements - -では、同じディレクトリに以下の `requirements.txt` ファイルを作成してください: - -```text -fastapi -``` - -!!! tip "豆知識" - アプリのローカルテストのために Uvicorn をインストールしたくなるかもしれませんが、Deta へのデプロイには不要です。 - -### ディレクトリ構造 - -以下の2つのファイルと1つの `./fastapideta/` ディレクトリがあるはずです: - -``` -. -└── main.py -└── requirements.txt -``` - -## Detaの無料アカウントの作成 - -それでは、Detaの無料アカウントを作成しましょう。必要なものはメールアドレスとパスワードだけです。 - -クレジットカードさえ必要ありません。 - -## CLIのインストール - -アカウントを取得したら、Deta CLI をインストールしてください: - -=== "Linux, macOS" - -
- - ```console - $ curl -fsSL https://get.deta.dev/cli.sh | sh - ``` - -
- -=== "Windows PowerShell" - -
- - ```console - $ iwr https://get.deta.dev/cli.ps1 -useb | iex - ``` - -
- -インストールしたら、インストールした CLI を有効にするために新たなターミナルを開いてください。 - -新たなターミナル上で、正しくインストールされたか確認します: - -
- -```console -$ deta --help - -Deta command line interface for managing deta micros. -Complete documentation available at https://docs.deta.sh - -Usage: - deta [flags] - deta [command] - -Available Commands: - auth Change auth settings for a deta micro - -... -``` - -
- -!!! tip "豆知識" - CLI のインストールに問題が発生した場合は、Deta 公式ドキュメントを参照してください。 - -## CLIでログイン - -CLI から Deta にログインしてみましょう: - -
- -```console -$ deta login - -Please, log in from the web page. Waiting.. -Logged in successfully. -``` - -
- -自動的にウェブブラウザが開いて、認証処理が行われます。 - -## Deta でデプロイ - -次に、アプリケーションを Deta CLIでデプロイしましょう: - -
- -```console -$ deta new - -Successfully created a new micro - -// Notice the "endpoint" 🔍 - -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} - -Adding dependencies... - - ----> 100% - - -Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6 -``` - -
- -次のようなJSONメッセージが表示されます: - -```JSON hl_lines="4" -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} -``` - -!!! tip "豆知識" - あなたのデプロイでは異なる `"endpoint"` URLが表示されるでしょう。 - -## 確認 - -それでは、`endpoint` URLをブラウザで開いてみましょう。上記の例では `https://qltnci.deta.dev` ですが、あなたのURLは異なるはずです。 - -FastAPIアプリから返ってきたJSONレスポンスが表示されます: - -```JSON -{ - "Hello": "World" -} -``` - -そして `/docs` へ移動してください。上記の例では、`https://qltnci.deta.dev/docs` です。 - -次のようなドキュメントが表示されます: - - - -## パブリックアクセスの有効化 - -デフォルトでは、Deta はクッキーを用いてアカウントの認証を行います。 - -しかし、準備が整えば、以下の様に公開できます: - -
- -```console -$ deta auth disable - -Successfully disabled http auth -``` - -
- -ここで、URLを共有するとAPIにアクセスできるようになります。🚀 - -## HTTPS - -おめでとうございます!あなたの FastAPI アプリが Deta へデプロイされました!🎉 🍰 - -また、DetaがHTTPSを正しく処理するため、その処理を行う必要がなく、クライアントは暗号化された安全な通信が利用できます。✅ 🔒 - -## Visor を確認 - -ドキュメントUI (`https://qltnci.deta.dev/docs` のようなURLにある) は *path operation* `/items/{item_id}` へリクエストを送ることができます。 - -ID `5` の例を示します。 - -まず、https://web.deta.sh へアクセスします。 - -左側に各アプリの 「Micros」 というセクションが表示されます。 - -また、「Details」や「Visor」タブが表示されています。「Visor」タブへ移動してください。 - -そこでアプリに送られた直近のリクエストが調べられます。 - -また、それらを編集してリプレイできます。 - - - -## さらに詳しく知る - -様々な箇所で永続的にデータを保存したくなるでしょう。そのためには Deta Base を使用できます。惜しみない **無料利用枠** もあります。 - -詳しくは Deta ドキュメントを参照してください。 diff --git a/docs/ja/docs/deployment/docker.md b/docs/ja/docs/deployment/docker.md index f10312b516..ca9dedc3c1 100644 --- a/docs/ja/docs/deployment/docker.md +++ b/docs/ja/docs/deployment/docker.md @@ -1,71 +1,157 @@ -# Dockerを使用したデプロイ +# コンテナ内のFastAPI - Docker -このセクションでは以下の使い方の紹介とガイドへのリンクが確認できます: +FastAPIアプリケーションをデプロイする場合、一般的なアプローチは**Linuxコンテナ・イメージ**をビルドすることです。 -* **5分**程度で、**FastAPI** のアプリケーションを、パフォーマンスを最大限に発揮するDockerイメージ (コンテナ)にする。 -* (オプション) 開発者として必要な範囲でHTTPSを理解する。 -* **20分**程度で、自動的なHTTPS生成とともにDockerのSwarmモード クラスタをセットアップする (月5ドルのシンプルなサーバー上で)。 -* **10分**程度で、DockerのSwarmモード クラスタを使って、HTTPSなどを使用した完全な**FastAPI** アプリケーションの作成とデプロイ。 +基本的には **Docker**を用いて行われます。生成されたコンテナ・イメージは、いくつかの方法のいずれかでデプロイできます。 -デプロイのために、**Docker** を利用できます。セキュリティ、再現性、開発のシンプルさなどに利点があります。 +Linuxコンテナの使用には、**セキュリティ**、**反復可能性(レプリカビリティ)**、**シンプリシティ**など、いくつかの利点があります。 -Dockerを使う場合、公式のDockerイメージが利用できます: +!!! tip + TODO: なぜか遷移できない + お急ぎで、すでにこれらの情報をご存じですか? [以下の`Dockerfile`の箇所👇](#build-a-docker-image-for-fastapi)へジャンプしてください。 -## tiangolo/uvicorn-gunicorn-fastapi - -このイメージは「自動チューニング」機構を含んでいます。犠牲を払うことなく、ただコードを加えるだけで自動的に高パフォーマンスを実現できます。 - -ただし、環境変数や設定ファイルを使って全ての設定の変更や更新を行えます。 - -!!! tip "豆知識" - 全ての設定とオプションを確認するには、Dockerイメージページを開いて下さい: tiangolo/uvicorn-gunicorn-fastapi. - -## `Dockerfile` の作成 - -* プロジェクトディレクトリへ移動。 -* 以下の`Dockerfile` を作成: +
+Dockerfile プレビュー 👀 ```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 +FROM python:3.9 -COPY ./app /app -``` +WORKDIR /code -### より大きなアプリケーション +COPY ./requirements.txt /code/requirements.txt -[Bigger Applications with Multiple Files](tutorial/bigger-applications.md){.internal-link target=_blank} セクションに倣う場合は、`Dockerfile` は上記の代わりに、以下の様になるかもしれません: +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 - -COPY ./app /app/app -``` - -### Raspberry Piなどのアーキテクチャ - -Raspberry Pi (ARMプロセッサ搭載)やそれ以外のアーキテクチャでDockerが作動している場合、(マルチアーキテクチャである) Pythonベースイメージを使って、一から`Dockerfile`を作成し、Uvicornを単体で使用できます。 - -この場合、`Dockerfile` は以下の様になるかもしれません: - -```Dockerfile -FROM python:3.7 - -RUN pip install fastapi uvicorn - -EXPOSE 80 - -COPY ./app /app +COPY ./app /code/app CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] + +# If running behind a proxy like Nginx or Traefik add --proxy-headers +# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"] ``` -## **FastAPI** コードの作成 +
-* `app` ディレクトリを作成し、移動。 -* 以下の`main.py` ファイルを作成: +## コンテナとは何か + +コンテナ(主にLinuxコンテナ)は、同じシステム内の他のコンテナ(他のアプリケーションやコンポーネント)から隔離された状態を保ちながら、すべての依存関係や必要なファイルを含むアプリケーションをパッケージ化する非常に**軽量**な方法です。 + +Linuxコンテナは、ホスト(マシン、仮想マシン、クラウドサーバーなど)の同じLinuxカーネルを使用して実行されます。これは、(OS全体をエミュレートする完全な仮想マシンと比べて)非常に軽量であることを意味します。 + +このように、コンテナは**リソースをほとんど消費しません**が、プロセスを直接実行するのに匹敵する量です(仮想マシンはもっと消費します)。 + +コンテナはまた、独自の**分離された**実行プロセス(通常は1つのプロセスのみ)や、ファイルシステム、ネットワークを持ちます。 このことはデプロイ、セキュリティ、開発などを簡素化させます。 + +## コンテナ・イメージとは何か + +**コンテナ**は、**コンテナ・イメージ**から実行されます。 + +コンテナ・イメージは、コンテナ内に存在すべきすべてのファイルや環境変数、そしてデフォルトのコマンド/プログラムを**静的に**バージョン化したものです。 ここでの**静的**とは、コンテナ**イメージ**は実行されておらず、パッケージ化されたファイルとメタデータのみであることを意味します。 + +保存された静的コンテンツである「**コンテナイメージ**」とは対照的に、「**コンテナ**」は通常、実行中のインスタンス、つまり**実行**されているものを指します。 + +**コンテナ**が起動され実行されるとき(**コンテナイメージ**から起動されるとき)、ファイルや環境変数などが作成されたり変更されたりする可能性があります。 + +これらの変更はそのコンテナ内にのみ存在しますが、基盤となるコンテナ・イメージには残りません(ディスクに保存されません)。 + +コンテナイメージは **プログラム** ファイルやその内容、例えば `python` と `main.py` ファイルに匹敵します。 + +そして、**コンテナ**自体は(**コンテナイメージ**とは対照的に)イメージをもとにした実際の実行中のインスタンスであり、**プロセス**に匹敵します。 + +実際、コンテナが実行されているのは、**プロセスが実行されている**ときだけです(通常は単一のプロセスだけです)。 コンテナ内で実行中のプロセスがない場合、コンテナは停止します。 + +## コンテナ・イメージ + +Dockerは、**コンテナ・イメージ**と**コンテナ**を作成・管理するための主要なツールの1つです。 + +そして、DockerにはDockerイメージ(コンテナ)を共有するDocker Hubというものがあります。 + +Docker Hubは 多くのツールや環境、データベース、アプリケーションに対応している予め作成された**公式のコンテナ・イメージ**をパブリックに提供しています。 + +例えば、公式イメージの1つにPython Imageがあります。 + +その他にも、データベースなどさまざまなイメージがあります: + +* PostgreSQL +* MySQL +* MongoDB +* Redis, etc. + +予め作成されたコンテナ・イメージを使用することで、異なるツールを**組み合わせて**使用することが非常に簡単になります。例えば、新しいデータベースを試す場合に特に便利です。ほとんどの場合、**公式イメージ**を使い、環境変数で設定するだけで良いです。 + +そうすれば多くの場合、コンテナとDockerについて学び、その知識をさまざまなツールやコンポーネントによって再利用することができます。 + +つまり、データベース、Pythonアプリケーション、Reactフロントエンド・アプリケーションを備えたウェブ・サーバーなど、さまざまなものを**複数のコンテナ**で実行し、それらを内部ネットワーク経由で接続します。 + +すべてのコンテナ管理システム(DockerやKubernetesなど)には、こうしたネットワーキング機能が統合されています。 + +## コンテナとプロセス + +通常、**コンテナ・イメージ**はそのメタデータに**コンテナ**の起動時に実行されるデフォルトのプログラムまたはコマンドと、そのプログラムに渡されるパラメータを含みます。コマンドラインでの操作とよく似ています。 + +**コンテナ**が起動されると、そのコマンド/プログラムが実行されます(ただし、別のコマンド/プログラムをオーバーライドして実行させることもできます)。 + +コンテナは、**メイン・プロセス**(コマンドまたはプログラム)が実行されている限り実行されます。 + +コンテナは通常**1つのプロセス**を持ちますが、メイン・プロセスからサブ・プロセスを起動することも可能で、そうすれば同じコンテナ内に**複数のプロセス**を持つことになります。 + +しかし、**少なくとも1つの実行中のプロセス**がなければ、実行中のコンテナを持つことはできないです。メイン・プロセスが停止すれば、コンテナも停止します。 + +## Build a Docker Image for FastAPI + +ということで、何か作りましょう!🚀 + +FastAPI用の**Dockerイメージ**を、**公式Python**イメージに基づいて**ゼロから**ビルドする方法をお見せします。 + +これは**ほとんどの場合**にやりたいことです。例えば: + +* **Kubernetes**または同様のツールを使用する場合 +* **Raspberry Pi**で実行する場合 +* コンテナ・イメージを実行してくれるクラウド・サービスなどを利用する場合 + +### パッケージ要件(package requirements) + +アプリケーションの**パッケージ要件**は通常、何らかのファイルに記述されているはずです。 + +パッケージ要件は主に**インストール**するために使用するツールに依存するでしょう。 + +最も一般的な方法は、`requirements.txt` ファイルにパッケージ名とそのバージョンを 1 行ずつ書くことです。 + +もちろん、[FastAPI バージョンについて](./versions.md){.internal-link target=_blank}で読んだのと同じアイデアを使用して、バージョンの範囲を設定します。 + +例えば、`requirements.txt` は次のようになります: + +``` +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +``` + +そして通常、例えば `pip` を使ってこれらのパッケージの依存関係をインストールします: + +
+ +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic uvicorn +``` + +
+ +!!! info + パッケージの依存関係を定義しインストールするためのフォーマットやツールは他にもあります。 + + Poetryを使った例は、後述するセクションでご紹介します。👇 + +### **FastAPI**コードを作成する + +* `app` ディレクトリを作成し、その中に入ります +* 空のファイル `__init__.py` を作成します +* `main.py` ファイルを作成します: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -78,23 +164,136 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` -* ここでは、以下の様なディレクトリ構造になっているはずです: +### Dockerfile + +同じプロジェクト・ディレクトリに`Dockerfile`というファイルを作成します: + +```{ .dockerfile .annotate } +# (1) +FROM python:3.9 + +# (2) +WORKDIR /code + +# (3) +COPY ./requirements.txt /code/requirements.txt + +# (4) +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (5) +COPY ./app /code/app + +# (6) +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +``` + +1. 公式のPythonベースイメージから始めます + +2. 現在の作業ディレクトリを `/code` に設定します + + ここに `requirements.txt` ファイルと `app` ディレクトリを置きます。 + +3. 要件が書かれたファイルを `/code` ディレクトリにコピーします + + 残りのコードではなく、最初に必要なファイルだけをコピーしてください。 + + このファイルは**頻繁には変更されない**ので、Dockerはこのステップではそれを検知し**キャッシュ**を使用し、次のステップでもキャッシュを有効にします。 + +4. 要件ファイルにあるパッケージの依存関係をインストールします + `--no-cache-dir` オプションはダウンロードしたパッケージをローカルに保存しないように `pip` に指示します。これは、同じパッケージをインストールするために `pip` を再度実行する場合にのみ有効ですが、コンテナで作業する場合はそうではないです。 + + !!! note + `--no-cache-dir`は`pip`に関連しているだけで、Dockerやコンテナとは何の関係もないです。 + + `--upgrade` オプションは、パッケージが既にインストールされている場合、`pip` にアップグレードするように指示します。 + + 何故ならファイルをコピーする前のステップは**Dockerキャッシュ**によって検出される可能性があるためであり、このステップも利用可能な場合は**Dockerキャッシュ**を使用します。 + + このステップでキャッシュを使用すると、開発中にイメージを何度もビルドする際に、**毎回**すべての依存関係を**ダウンロードしてインストールする**代わりに多くの**時間**を**節約**できます。 + +5. ./app` ディレクトリを `/code` ディレクトリの中にコピーする。 + + これには**最も頻繁に変更される**すべてのコードが含まれているため、Dockerの**キャッシュ**は**これ以降のステップ**に簡単に使用されることはありません。 + + そのため、コンテナイメージのビルド時間を最適化するために、`Dockerfile`の **最後** にこれを置くことが重要です。 + +6. `uvicorn`サーバーを実行するための**コマンド**を設定します + + `CMD` は文字列のリストを取り、それぞれの文字列はスペースで区切られたコマンドラインに入力するものです。 + + このコマンドは **現在の作業ディレクトリ**から実行され、上記の `WORKDIR /code` にて設定した `/code` ディレクトリと同じです。 + + そのためプログラムは `/code` で開始しその中にあなたのコードがある `./app` ディレクトリがあるので、**Uvicorn** は `app.main` から `app` を参照し、**インポート** することができます。 + +!!! tip + コード内の"+"の吹き出しをクリックして、各行が何をするのかをレビューしてください。👆 + +これで、次のようなディレクトリ構造になるはずです: ``` . ├── app +│   ├── __init__.py │ └── main.py -└── Dockerfile +├── Dockerfile +└── requirements.txt ``` -## Dockerイメージをビルド +#### TLS Termination Proxyの裏側 -* プロジェクトディレクトリ (`app` ディレクトリを含んだ、`Dockerfile` のある場所) へ移動 -* FastAPIイメージのビルド: +Nginx や Traefik のような TLS Termination Proxy (ロードバランサ) の後ろでコンテナを動かしている場合は、`--proxy-headers`オプションを追加します。 + +このオプションは、Uvicornにプロキシ経由でHTTPSで動作しているアプリケーションに対して、送信されるヘッダを信頼するよう指示します。 + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +#### Dockerキャッシュ + +この`Dockerfile`には重要なトリックがあり、まず**依存関係だけのファイル**をコピーします。その理由を説明します。 + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +Dockerや他のツールは、これらのコンテナイメージを**段階的に**ビルドし、**1つのレイヤーを他のレイヤーの上に**追加します。`Dockerfile`の先頭から開始し、`Dockerfile`の各命令によって作成されたファイルを追加していきます。 + +Dockerや同様のツールは、イメージをビルドする際に**内部キャッシュ**も使用します。前回コンテナイメージを構築したときからファイルが変更されていない場合、ファイルを再度コピーしてゼロから新しいレイヤーを作成する代わりに、**前回作成した同じレイヤーを再利用**します。 + +ただファイルのコピーを避けるだけではあまり改善されませんが、そのステップでキャッシュを利用したため、**次のステップ**でキャッシュを使うことができます。 + +例えば、依存関係をインストールする命令のためにキャッシュを使うことができます: + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +パッケージ要件のファイルは**頻繁に変更されることはありません**。そのため、そのファイルだけをコピーすることで、Dockerはそのステップでは**キャッシュ**を使用することができます。 + +そして、Dockerは**次のステップのためにキャッシュ**を使用し、それらの依存関係をダウンロードしてインストールすることができます。そして、ここで**多くの時間を節約**します。✨ ...そして退屈な待ち時間を避けることができます。😪😆 + +パッケージの依存関係をダウンロードしてインストールするには**数分**かかりますが、**キャッシュ**を使えば**せいぜい数秒**です。 + +加えて、開発中にコンテナ・イメージを何度もビルドして、コードの変更が機能しているかどうかをチェックすることになるため、多くの時間を節約することができます。 + +そして`Dockerfile`の最終行の近くですべてのコードをコピーします。この理由は、**最も頻繁に**変更されるものなので、このステップの後にあるものはほとんどキャッシュを使用することができないのためです。 + +```Dockerfile +COPY ./app /code/app +``` + +### Dockerイメージをビルドする + +すべてのファイルが揃ったので、コンテナ・イメージをビルドしましょう。 + +* プロジェクトディレクトリに移動します(`Dockerfile`がある場所で、`app`ディレクトリがあります) +* FastAPI イメージをビルドします:
@@ -106,9 +305,14 @@ $ docker build -t myimage .
-## Dockerコンテナを起動 +!!! tip + 末尾の `.` に注目してほしいです。これは `./` と同じ意味です。 これはDockerにコンテナイメージのビルドに使用するディレクトリを指示します。 -* 用意したイメージを基にしたコンテナの起動: + この場合、同じカレント・ディレクトリ(`.`)です。 + +### Dockerコンテナの起動する + +* イメージに基づいてコンテナを実行します:
@@ -118,62 +322,394 @@ $ docker run -d --name mycontainer -p 80:80 myimage
-これで、Dockerコンテナ内に最適化されたFastAPIサーバが動作しています。使用しているサーバ (そしてCPUコア数) に沿った自動チューニングが行われています。 +## 確認する -## 確認 +Dockerコンテナのhttp://192.168.99.100/items/5?q=somequeryhttp://127.0.0.1/items/5?q=somequery (またはそれに相当するDockerホストを使用したもの)といったURLで確認できるはずです。 -DockerコンテナのURLで確認できるはずです。例えば: http://192.168.99.100/items/5?q=somequeryhttp://127.0.0.1/items/5?q=somequery (もしくはDockerホストを使用したこれらと同等のもの)。 - -以下の様なものが返されます: +アクセスすると以下のようなものが表示されます: ```JSON {"item_id": 5, "q": "somequery"} ``` -## 対話的APIドキュメント +## インタラクティブなAPIドキュメント -ここで、http://192.168.99.100/docshttp://127.0.0.1/docs (もしくはDockerホストを使用したこれらと同等のもの) を開いて下さい。 +これらのURLにもアクセスできます: http://192.168.99.100/docshttp://127.0.0.1/docs (またはそれに相当するDockerホストを使用したもの) -自動生成された対話的APIドキュメントが確認できます (Swagger UIによって提供されます): +アクセスすると、自動対話型APIドキュメント(Swagger UIが提供)が表示されます: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -## その他のAPIドキュメント +## 代替のAPIドキュメント -また同様に、http://192.168.99.100/redochttp://127.0.0.1/redoc (もしくはDockerホストを使用したこれらと同等のもの) を開いて下さい。 +また、http://192.168.99.100/redochttp://127.0.0.1/redoc (またはそれに相当するDockerホストを使用したもの)にもアクセスできます。 -他の自動生成された対話的なAPIドキュメントが確認できます (ReDocによって提供されます): +代替の自動ドキュメント(ReDocによって提供される)が表示されます: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Traefik +## 単一ファイルのFastAPIでDockerイメージをビルドする -Traefikは、高性能なリバースプロキシ/ロードバランサーです。「TLSターミネーションプロキシ」ジョブを実行できます(他の機能と切り離して)。 +FastAPI が単一のファイル、例えば `./app` ディレクトリのない `main.py` の場合、ファイル構造は次のようになります: +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` -Let's Encryptと統合されています。そのため、証明書の取得と更新を含むHTTPSに関するすべての処理を実行できます。 +そうすれば、`Dockerfile`の中にファイルをコピーするために、対応するパスを変更するだけでよいです: -また、Dockerとも統合されています。したがって、各アプリケーション構成でドメインを宣言し、それらの構成を読み取って、HTTPS証明書を生成し、構成に変更を加えることなく、アプリケーションにHTTPSを自動的に提供できます。 +```{ .dockerfile .annotate hl_lines="10 13" } +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (1) +COPY ./main.py /code/ + +# (2) +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] +``` + +1. main.py`ファイルを `/code` ディレクトリに直接コピーします。 + +2. Uvicornを実行し、`main`から`app`オブジェクトをインポートするように指示します(`app.main`からインポートするのではなく)。 + +次にUvicornコマンドを調整して、`app.main` の代わりに新しいモジュール `main` を使用し、FastAPIオブジェクトである `app` をインポートします。 + +## デプロイメントのコンセプト + +コンテナという観点から、[デプロイのコンセプト](./concepts.md){.internal-link target=_blank}に共通するいくつかについて、もう一度説明しましょう。 + +コンテナは主に、アプリケーションの**ビルドとデプロイ**のプロセスを簡素化するためのツールですが、これらの**デプロイのコンセプト**を扱うための特定のアプローチを強制するものではないです。 + +**良いニュース**は、それぞれの異なる戦略には、すべてのデプロイメントのコンセプトをカバーする方法があるということです。🎉 + +これらの**デプロイメントのコンセプト**をコンテナの観点から見直してみましょう: + +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* **レプリケーション(実行中のプロセス数)** +* メモリ +* 開始前の事前ステップ + +## HTTPS + +FastAPI アプリケーションの **コンテナ・イメージ**(および後で実行中の **コンテナ**)だけに焦点を当てると、通常、HTTPSは別のツールを用いて**外部で**処理されます。 + +例えばTraefikのように、**HTTPS**と**証明書**の**自動**取得を扱う別のコンテナである可能性もあります。 + +!!! tip + TraefikはDockerやKubernetesなどと統合されているので、コンテナ用のHTTPSの設定や構成はとても簡単です。 + +あるいは、(コンテナ内でアプリケーションを実行しながら)クラウド・プロバイダーがサービスの1つとしてHTTPSを処理することもできます。 + +## 起動時および再起動時の実行 + +通常、コンテナの**起動と実行**を担当する別のツールがあります。 + +それは直接**Docker**であったり、**Docker Compose**であったり、**Kubernetes**であったり、**クラウドサービス**であったりします。 + +ほとんどの場合(またはすべての場合)、起動時にコンテナを実行し、失敗時に再起動を有効にする簡単なオプションがあります。例えばDockerでは、コマンドラインオプションの`--restart`が該当します。 + +コンテナを使わなければ、アプリケーションを起動時や再起動時に実行させるのは面倒で難しいかもしれません。しかし、**コンテナ**で作業する場合、ほとんどのケースでその機能はデフォルトで含まれています。✨ + +## レプリケーション - プロセス数 + +**Kubernetes** や Docker Swarm モード、Nomad、あるいは複数のマシン上で分散コンテナを管理するための同様の複雑なシステムを使ってマシンのクラスターを構成している場合、 各コンテナで(Workerを持つGunicornのような)**プロセスマネージャ**を使用する代わりに、**クラスター・レベル**で**レプリケーション**を処理したいと思うでしょう。 + +Kubernetesのような分散コンテナ管理システムの1つは通常、入ってくるリクエストの**ロードバランシング**をサポートしながら、**コンテナのレプリケーション**を処理する統合された方法を持っています。このことはすべて**クラスタレベル**にてです。 + +そのような場合、UvicornワーカーでGunicornのようなものを実行するのではなく、[上記の説明](#dockerfile)のように**Dockerイメージをゼロから**ビルドし、依存関係をインストールして、**単一のUvicornプロセス**を実行したいでしょう。 + +### ロードバランサー + +コンテナを使用する場合、通常はメイン・ポート**でリスニング**しているコンポーネントがあるはずです。それはおそらく、**HTTPS**を処理するための**TLS Termination Proxy**でもある別のコンテナであったり、同様のツールであったりするでしょう。 + +このコンポーネントはリクエストの **負荷** を受け、 (うまくいけば) その負荷を**バランスよく** ワーカーに分配するので、一般に **ロードバランサ** とも呼ばれます。 + +!!! tip +  HTTPSに使われるものと同じ**TLS Termination Proxy**コンポーネントは、おそらく**ロードバランサー**にもなるでしょう。 + +そしてコンテナで作業する場合、コンテナの起動と管理に使用する同じシステムには、**ロードバランサー**(**TLS Termination Proxy**の可能性もある)から**ネットワーク通信**(HTTPリクエストなど)をアプリのあるコンテナ(複数可)に送信するための内部ツールが既にあるはずです。 + +### 1つのロードバランサー - 複数のワーカーコンテナー + +**Kubernetes**や同様の分散コンテナ管理システムで作業する場合、その内部のネットワーキングのメカニズムを使用することで、メインの**ポート**でリッスンしている単一の**ロードバランサー**が、アプリを実行している可能性のある**複数のコンテナ**に通信(リクエスト)を送信できるようになります。 + +アプリを実行するこれらのコンテナには、通常**1つのプロセス**(たとえば、FastAPIアプリケーションを実行するUvicornプロセス)があります。これらはすべて**同一のコンテナ**であり同じものを実行しますが、それぞれが独自のプロセスやメモリなどを持ちます。そうすることで、CPUの**異なるコア**、あるいは**異なるマシン**での**並列化**を利用できます。 + +そして、**ロードバランサー**を備えた分散コンテナシステムは、**順番に**あなたのアプリを含む各コンテナに**リクエストを分配**します。つまり、各リクエストは、あなたのアプリを実行している複数の**レプリケートされたコンテナ**の1つによって処理されます。 + +そして通常、この**ロードバランサー**は、クラスタ内の*他の*アプリケーション(例えば、異なるドメインや異なるURLパスのプレフィックスの配下)へのリクエストを処理することができ、その通信をクラスタ内で実行されている*他の*アプリケーションのための適切なコンテナに送信します。 + +### 1コンテナにつき1プロセス + +この種のシナリオでは、すでにクラスタ・レベルでレプリケーションを処理しているため、おそらくコンテナごとに**単一の(Uvicorn)プロセス**を持ちたいでしょう。 + +この場合、Uvicornワーカーを持つGunicornのようなプロセスマネージャーや、Uvicornワーカーを使うUvicornは**避けたい**でしょう。**コンテナごとにUvicornのプロセスは1つだけ**にしたいでしょう(おそらく複数のコンテナが必要でしょう)。 + +(GunicornやUvicornがUvicornワーカーを管理するように)コンテナ内に別のプロセスマネージャーを持つことは、クラスターシステムですでに対処しているであろう**不要な複雑さ**を追加するだけです。 + +### Containers with Multiple Processes and Special Cases + +もちろん、**特殊なケース**として、**Gunicornプロセスマネージャ**を持つ**コンテナ**内で複数の**Uvicornワーカープロセス**を起動させたい場合があります。 + +このような場合、**公式のDockerイメージ**を使用することができます。このイメージには、複数の**Uvicornワーカープロセス**を実行するプロセスマネージャとして**Gunicorn**が含まれており、現在のCPUコアに基づいてワーカーの数を自動的に調整するためのデフォルト設定がいくつか含まれています。詳しくは後述の[Gunicornによる公式Dockerイメージ - Uvicorn](#gunicornによる公式dockerイメージ---Uvicorn)で説明します。 + +以下は、それが理にかなっている場合の例です: + +#### シンプルなアプリケーション + +アプリケーションを**シンプル**な形で実行する場合、プロセス数の細かい調整が必要ない場合、自動化されたデフォルトを使用するだけで、コンテナ内にプロセスマネージャが必要かもしれません。例えば、公式Dockerイメージでシンプルな設定が可能です。 + +#### Docker Compose + +Docker Composeで**シングルサーバ**(クラスタではない)にデプロイすることもできますので、共有ネットワークと**ロードバランシング**を維持しながら(Docker Composeで)コンテナのレプリケーションを管理する簡単な方法はないでしょう。 + +その場合、**単一のコンテナ**で、**プロセスマネージャ**が内部で**複数のワーカープロセス**を起動するようにします。 + +#### Prometheusとその他の理由 + +また、**1つのコンテナ**に**1つのプロセス**を持たせるのではなく、**1つのコンテナ**に**複数のプロセス**を持たせる方が簡単だという**他の理由**もあるでしょう。 + +例えば、(セットアップにもよりますが)Prometheusエクスポーターのようなツールを同じコンテナ内に持つことができます。 + +この場合、**複数のコンテナ**があると、デフォルトでは、Prometheusが**メトリクスを**読みに来たとき、すべてのレプリケートされたコンテナの**蓄積されたメトリクス**を取得するのではなく、毎回**単一のコンテナ**(その特定のリクエストを処理したコンテナ)のものを取得することになります。 + +その場合、**複数のプロセス**を持つ**1つのコンテナ**を用意し、同じコンテナ上のローカルツール(例えばPrometheusエクスポーター)がすべての内部プロセスのPrometheusメトリクスを収集し、その1つのコンテナ上でそれらのメトリクスを公開する方がシンプルかもしれません。 --- -次のセクションに進み、この情報とツールを使用して、すべてを組み合わせます。 +重要なのは、盲目的に従わなければならない普遍のルールはないということです。 -## TraefikとHTTPSを使用したDocker Swarmモードのクラスタ +これらのアイデアは、**あなた自身のユースケース**を評価し、あなたのシステムに最適なアプローチを決定するために使用することができます: -HTTPSを処理する(証明書の取得と更新を含む)Traefikを使用して、Docker Swarmモードのクラスタを数分(20分程度)でセットアップできます。 +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* **レプリケーション(実行中のプロセス数)** +* メモリ +* 開始前の事前ステップ -Docker Swarmモードを使用することで、1台のマシンの「クラスタ」から開始でき(1か月あたり5ドルのサーバーでもできます)、後から必要なだけサーバーを拡張できます。 +## メモリー -TraefikおよびHTTPS処理を備えたDocker Swarm Modeクラスターをセットアップするには、次のガイドに従います: +コンテナごとに**単一のプロセスを実行する**と、それらのコンテナ(レプリケートされている場合は1つ以上)によって消費される多かれ少なかれ明確に定義された、安定し制限された量のメモリを持つことになります。 -### Docker Swarm Mode and Traefik for an HTTPS cluster +そして、コンテナ管理システム(**Kubernetes**など)の設定で、同じメモリ制限と要件を設定することができます。 -### FastAPIアプリケーションのデプロイ +そうすれば、コンテナが必要とするメモリ量とクラスタ内のマシンで利用可能なメモリ量を考慮して、**利用可能なマシン**に**コンテナ**をレプリケートできるようになります。 -すべてを設定するための最も簡単な方法は、[**FastAPI** Project Generators](../project-generation.md){.internal-link target=_blank}を使用することでしょう。 +アプリケーションが**シンプル**なものであれば、これはおそらく**問題にはならない**でしょうし、ハードなメモリ制限を指定する必要はないかもしれないです。 -上述したTraefikとHTTPSを備えたDocker Swarm クラスタが統合されるように設計されています。 +しかし、**多くのメモリを使用**している場合(たとえば**機械学習**モデルなど)、どれだけのメモリを消費しているかを確認し、**各マシンで実行するコンテナの数**を調整する必要があります(そしておそらくクラスタにマシンを追加します)。 -2分程度でプロジェクトが生成されます。 +**コンテナごとに複数のプロセス**を実行する場合(たとえば公式のDockerイメージで)、起動するプロセスの数が**利用可能なメモリ以上に消費しない**ようにする必要があります。 -生成されたプロジェクトはデプロイの指示がありますが、それを実行するとさらに2分かかります。 +## 開始前の事前ステップとコンテナ + +コンテナ(DockerやKubernetesなど)を使っている場合、主に2つのアプローチがあります。 + +### 複数のコンテナ + +複数の**コンテナ**があり、おそらくそれぞれが**単一のプロセス**を実行している場合(**Kubernetes**クラスタなど)、レプリケートされたワーカーコンテナを実行する**前に**、単一のコンテナで**事前のステップ**の作業を行う**別のコンテナ**を持ちたいと思うでしょう。 + +!!! info + もしKubernetesを使用している場合, これはおそらくInit コンテナでしょう。 + +ユースケースが事前のステップを**並列で複数回**実行するのに問題がない場合(例:データベースの準備チェック)、メインプロセスを開始する前に、それらのステップを各コンテナに入れることが可能です。 + +### 単一コンテナ + +単純なセットアップで、**単一のコンテナ**で複数の**ワーカー・プロセス**(または1つのプロセスのみ)を起動する場合、アプリでプロセスを開始する直前に、同じコンテナで事前のステップを実行できます。公式Dockerイメージは、内部的にこれをサポートしています。 + +## Gunicornによる公式Dockerイメージ - Uvicorn + +前の章で詳しく説明したように、Uvicornワーカーで動作するGunicornを含む公式のDockerイメージがあります: [Server Workers - Gunicorn と Uvicorn](./server-workers.md){.internal-link target=_blank}で詳しく説明しています。 + +このイメージは、主に上記で説明した状況で役に立つでしょう: [複数のプロセスと特殊なケースを持つコンテナ(Containers with Multiple Processes and Special Cases)](#containers-with-multiple-processes-and-special-cases) + +* tiangolo/uvicorn-gunicorn-fastapi. + +!!! warning + このベースイメージや類似のイメージは**必要ない**可能性が高いので、[上記の: FastAPI用のDockerイメージをビルドする(Build a Docker Image for FastAPI)](#build-a-docker-image-for-fastapi)のようにゼロからイメージをビルドする方が良いでしょう。 + +このイメージには、利用可能なCPUコアに基づいて**ワーカー・プロセスの数**を設定する**オートチューニング**メカニズムが含まれています。 + +これは**賢明なデフォルト**を備えていますが、**環境変数**や設定ファイルを使ってすべての設定を変更したり更新したりすることができます。 + +また、スクリプトで**開始前の事前ステップ**を実行することもサポートしている。 + +!!! tip + すべての設定とオプションを見るには、Dockerイメージのページをご覧ください: tiangolo/uvicorn-gunicorn-fastapi + +### 公式Dockerイメージのプロセス数 + +このイメージの**プロセス数**は、利用可能なCPU**コア**から**自動的に計算**されます。 + +つまり、CPUから可能な限り**パフォーマンス**を**引き出そう**とします。 + +また、**環境変数**などを使った設定で調整することもできます。 + +しかし、プロセスの数はコンテナが実行しているCPUに依存するため、**消費されるメモリの量**もそれに依存することになります。 + +そのため、(機械学習モデルなどで)大量のメモリを消費するアプリケーションで、サーバーのCPUコアが多いが**メモリが少ない**場合、コンテナは利用可能なメモリよりも多くのメモリを使おうとすることになります。 + +その結果、パフォーマンスが大幅に低下する(あるいはクラッシュする)可能性があります。🚨 + +### Dockerfileを作成する + +この画像に基づいて`Dockerfile`を作成する方法を以下に示します: + +```Dockerfile +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 + +COPY ./requirements.txt /app/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +COPY ./app /app +``` + +### より大きなアプリケーション + +[複数のファイルを持つ大きなアプリケーション](../tutorial/bigger-applications.md){.internal-link target=_blank}を作成するセクションに従った場合、`Dockerfile`は次のようになります: + +```Dockerfile hl_lines="7" +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 + +COPY ./requirements.txt /app/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +COPY ./app /app/app +``` + +### いつ使うのか + +おそらく、**Kubernetes**(または他のもの)を使用していて、すでにクラスタレベルで複数の**コンテナ**で**レプリケーション**を設定している場合は、この公式ベースイメージ(または他の類似のもの)は**使用すべきではありません**。 + +そのような場合は、上記のように**ゼロから**イメージを構築する方がよいでしょう: [FastAPI用のDockerイメージをビルドする(Build a Docker Image for FastAPI)](#build-a-docker-image-for-fastapi) を参照してください。 + +このイメージは、主に上記の[複数のプロセスと特殊なケースを持つコンテナ(Containers with Multiple Processes and Special Cases)](#containers-with-multiple-processes-and-special-cases)で説明したような特殊なケースで役に立ちます。 + +例えば、アプリケーションが**シンプル**で、CPUに応じたデフォルトのプロセス数を設定すればうまくいく場合や、クラスタレベルでレプリケーションを手動で設定する手間を省きたい場合、アプリで複数のコンテナを実行しない場合などです。 + +または、**Docker Compose**でデプロイし、単一のサーバで実行している場合などです。 + +## コンテナ・イメージのデプロイ + +コンテナ(Docker)イメージを手に入れた後、それをデプロイするにはいくつかの方法があります。 + +例えば以下のリストの方法です: + +* 単一サーバーの**Docker Compose** +* **Kubernetes**クラスタ +* Docker Swarmモードのクラスター +* Nomadのような別のツール +* コンテナ・イメージをデプロイするクラウド・サービス + +## Poetryを利用したDockerイメージ + +もしプロジェクトの依存関係を管理するためにPoetryを利用する場合、マルチステージビルドを使うと良いでしょう。 + +```{ .dockerfile .annotate } +# (1) +FROM python:3.9 as requirements-stage + +# (2) +WORKDIR /tmp + +# (3) +RUN pip install poetry + +# (4) +COPY ./pyproject.toml ./poetry.lock* /tmp/ + +# (5) +RUN poetry export -f requirements.txt --output requirements.txt --without-hashes + +# (6) +FROM python:3.9 + +# (7) +WORKDIR /code + +# (8) +COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt + +# (9) +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (10) +COPY ./app /code/app + +# (11) +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +``` + +1. これは最初のステージで、`requirements-stage`と名付けられます +2. `/tmp` を現在の作業ディレクトリに設定します + ここで `requirements.txt` というファイルを生成します。 + +3. このDockerステージにPoetryをインストールします + +4. pyproject.toml`と`poetry.lock`ファイルを`/tmp` ディレクトリにコピーします + + `./poetry.lock*`(末尾に`*`)を使用するため、そのファイルがまだ利用できない場合でもクラッシュすることはないです。 +5. requirements.txt`ファイルを生成します + +6. これは最後のステージであり、ここにあるものはすべて最終的なコンテナ・イメージに保存されます +7. 現在の作業ディレクトリを `/code` に設定します +8. `requirements.txt`ファイルを `/code` ディレクトリにコピーします + このファイルは前のDockerステージにしか存在しないため、`--from-requirements-stage`を使ってコピーします。 +9. 生成された `requirements.txt` ファイルにあるパッケージの依存関係をインストールします +10. app` ディレクトリを `/code` ディレクトリにコピーします +11. uvicorn` コマンドを実行して、`app.main` からインポートした `app` オブジェクトを使用するように指示します +!!! tip + "+"の吹き出しをクリックすると、それぞれの行が何をするのかを見ることができます + +**Dockerステージ**は`Dockerfile`の一部で、**一時的なコンテナイメージ**として動作します。 + +最初のステージは **Poetryのインストール**と Poetry の `pyproject.toml` ファイルからプロジェクトの依存関係を含む**`requirements.txt`を生成**するためだけに使用されます。 + +この `requirements.txt` ファイルは後半の **次のステージ**で `pip` と共に使用されます。 + +最終的なコンテナイメージでは、**最終ステージ**のみが保存されます。前のステージは破棄されます。 + +Poetryを使用する場合、**Dockerマルチステージビルド**を使用することは理にかなっています。 + +なぜなら、最終的なコンテナイメージにPoetryとその依存関係がインストールされている必要はなく、**必要なのは**プロジェクトの依存関係をインストールするために生成された `requirements.txt` ファイルだけだからです。 + +そして次の(そして最終的な)ステージでは、前述とほぼ同じ方法でイメージをビルドします。 + +### TLS Termination Proxyの裏側 - Poetry + +繰り返しになりますが、NginxやTraefikのようなTLS Termination Proxy(ロードバランサー)の後ろでコンテナを動かしている場合は、`--proxy-headers`オプションをコマンドに追加します: + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +## まとめ + +コンテナ・システム(例えば**Docker**や**Kubernetes**など)を使えば、すべての**デプロイメントのコンセプト**を扱うのがかなり簡単になります: + +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* **レプリケーション(実行中のプロセス数)** +* メモリ +* 開始前の事前ステップ + +ほとんどの場合、ベースとなるイメージは使用せず、公式のPython Dockerイメージをベースにした**コンテナイメージをゼロからビルド**します。 + +`Dockerfile`と**Dockerキャッシュ**内の命令の**順番**に注意することで、**ビルド時間を最小化**することができ、生産性を最大化することができます(そして退屈を避けることができます)。😎 + +特別なケースでは、FastAPI用の公式Dockerイメージを使いたいかもしれません。🤓 diff --git a/docs/ja/docs/deployment/https.md b/docs/ja/docs/deployment/https.md new file mode 100644 index 0000000000..a291f870fd --- /dev/null +++ b/docs/ja/docs/deployment/https.md @@ -0,0 +1,200 @@ +# HTTPS について + +HTTPSは単に「有効」か「無効」かで決まるものだと思いがちです。 + +しかし、それよりもはるかに複雑です。 + +!!! tip + もし急いでいたり、HTTPSの仕組みについて気にしないのであれば、次のセクションに進み、さまざまなテクニックを使ってすべてをセットアップするステップ・バイ・ステップの手順をご覧ください。 + +利用者の視点から **HTTPS の基本を学ぶ**に当たっては、次のリソースをオススメします: https://howhttps.works/. + +さて、**開発者の視点**から、HTTPSについて考える際に念頭に置くべきことをいくつかみていきましょう: + +* HTTPSの場合、**サーバ**は**第三者**によって生成された**「証明書」を持つ**必要があります。 + * これらの証明書は「生成」されたものではなく、実際には第三者から**取得**されたものです。 +* 証明書には**有効期限**があります。 + * つまりいずれ失効します。 + * そのため**更新**をし、第三者から**再度取得**する必要があります。 +* 接続の暗号化は**TCPレベル**で行われます。 + * それは**HTTPの1つ下**のレイヤーです。 + * つまり、**証明書と暗号化**の処理は、**HTTPの前**に行われます。 +* **TCPは "ドメイン "について知りません**。IPアドレスについてのみ知っています。 + * 要求された**特定のドメイン**に関する情報は、**HTTPデータ**に入ります。 +* **HTTPS証明書**は、**特定のドメイン**を「証明」しますが、プロトコルと暗号化はTCPレベルで行われ、どのドメインが扱われているかを**知る前**に行われます。 +* **デフォルトでは**、**IPアドレスごとに1つのHTTPS証明書**しか持てないことになります。 + * これは、サーバーの規模やアプリケーションの規模に寄りません。 + * しかし、これには**解決策**があります。 +* **TLS**プロトコル(HTTPの前に、TCPレベルで暗号化を処理するもの)には、**SNI**と呼ばれる**拡張**があります。 + * このSNI拡張機能により、1つのサーバー(**単一のIPアドレス**を持つ)が**複数のHTTPS証明書**を持ち、**複数のHTTPSドメイン/アプリケーション**にサービスを提供できるようになります。 + * これが機能するためには、**パブリックIPアドレス**でリッスンしている、サーバー上で動作している**単一の**コンポーネント(プログラム)が、サーバー内の**すべてのHTTPS証明書**を持っている必要があります。 + +* セキュアな接続を取得した**後**でも、通信プロトコルは**HTTPのまま**です。 + * コンテンツは**HTTPプロトコル**で送信されているにもかかわらず、**暗号化**されています。 + + +サーバー(マシン、ホストなど)上で**1つのプログラム/HTTPサーバー**を実行させ、**HTTPSに関する全てのこと**を管理するのが一般的です。 + +**暗号化された HTTPS リクエスト** を受信し、**復号化された HTTP リクエスト** を同じサーバーで実行されている実際の HTTP アプリケーション(この場合は **FastAPI** アプリケーション)に送信し、アプリケーションから **HTTP レスポンス** を受け取り、適切な **HTTPS 証明書** を使用して **暗号化** し、そして**HTTPS** を使用してクライアントに送り返します。 + +このサーバーはしばしば **TLS Termination Proxy**と呼ばれます。 + +TLS Termination Proxyとして使えるオプションには、以下のようなものがあります: + +* Traefik(証明書の更新も対応) +* Caddy (証明書の更新も対応) +* Nginx +* HAProxy + + +## Let's Encrypt + +Let's Encrypt以前は、これらの**HTTPS証明書**は信頼できる第三者によって販売されていました。 + +これらの証明書を取得するための手続きは面倒で、かなりの書類を必要とし、証明書はかなり高価なものでした。 + +しかしその後、**Let's Encrypt** が作られました。 + +これはLinux Foundationのプロジェクトから生まれたものです。 自動化された方法で、**HTTPS証明書を無料で**提供します。これらの証明書は、すべての標準的な暗号化セキュリティを使用し、また短命(約3ヶ月)ですが、こういった寿命の短さによって、**セキュリティは実際に優れています**。 + +ドメインは安全に検証され、証明書は自動的に生成されます。また、証明書の更新も自動化されます。 + +このアイデアは、これらの証明書の取得と更新を自動化することで、**安全なHTTPSを、無料で、永遠に**利用できるようにすることです。 + +## 開発者のための HTTPS + +ここでは、HTTPS APIがどのように見えるかの例を、主に開発者にとって重要なアイデアに注意を払いながら、ステップ・バイ・ステップで説明します。 + +### ドメイン名 + +ステップの初めは、**ドメイン名**を**取得すること**から始まるでしょう。その後、DNSサーバー(おそらく同じクラウドプロバイダー)に設定します。 + +おそらくクラウドサーバー(仮想マシン)かそれに類するものを手に入れ、固定の **パブリックIPアドレス**を持つことになるでしょう。 + +DNSサーバーでは、**取得したドメイン**をあなたのサーバーのパプリック**IPアドレス**に向けるレコード(「`Aレコード`」)を設定します。 + +これはおそらく、最初の1回だけあり、すべてをセットアップするときに行うでしょう。 + +!!! tip + ドメイン名の話はHTTPSに関する話のはるか前にありますが、すべてがドメインとIPアドレスに依存するため、ここで言及する価値があります。 + +### DNS + +では、実際のHTTPSの部分に注目してみよう。 + +まず、ブラウザは**DNSサーバー**に**ドメインに対するIP**が何であるかを確認します。今回は、`someapp.example.com`とします。 + +DNSサーバーは、ブラウザに特定の**IPアドレス**を使用するように指示します。このIPアドレスは、DNSサーバーで設定した、あなたのサーバーが使用するパブリックIPアドレスになります。 + + + +### TLS Handshake の開始 + +ブラウザはIPアドレスと**ポート443**(HTTPSポート)で通信します。 + +通信の最初の部分は、クライアントとサーバー間の接続を確立し、使用する暗号鍵などを決めるだけです。 + + + +TLS接続を確立するためのクライアントとサーバー間のこのやりとりは、**TLSハンドシェイク**と呼ばれます。 + +### SNI拡張機能付きのTLS + +サーバー内の**1つのプロセス**だけが、特定 の**IPアドレス**の特定の**ポート** で待ち受けることができます。 + +同じIPアドレスの他のポートで他のプロセスがリッスンしている可能性もありますが、IPアドレスとポートの組み合わせごとに1つだけです。 + +TLS(HTTPS)はデフォルトで`443`という特定のポートを使用する。つまり、これが必要なポートです。 + +このポートをリッスンできるのは1つのプロセスだけなので、これを実行するプロセスは**TLS Termination Proxy**となります。 + +TLS Termination Proxyは、1つ以上の**TLS証明書**(HTTPS証明書)にアクセスできます。 + +前述した**SNI拡張機能**を使用して、TLS Termination Proxy は、利用可能なTLS (HTTPS)証明書のどれを接続先として使用すべきかをチェックし、クライアントが期待するドメインに一致するものを使用します。 + +今回は、`someapp.example.com`の証明書を使うことになります。 + + + +クライアントは、そのTLS証明書を生成したエンティティ(この場合はLet's Encryptですが、これについては後述します)をすでに**信頼**しているため、その証明書が有効であることを**検証**することができます。 + +次に証明書を使用して、クライアントとTLS Termination Proxy は、 **TCP通信**の残りを**どのように暗号化するかを決定**します。これで**TLSハンドシェイク**の部分が完了します。 + +この後、クライアントとサーバーは**暗号化されたTCP接続**を持ちます。そして、その接続を使って実際の**HTTP通信**を開始することができます。 + +これが**HTTPS**であり、純粋な(暗号化されていない)TCP接続ではなく、**セキュアなTLS接続**の中に**HTTP**があるだけです。 + +!!! tip + 通信の暗号化は、HTTPレベルではなく、**TCPレベル**で行われることに注意してください。 + +### HTTPS リクエスト + +これでクライアントとサーバー(具体的にはブラウザとTLS Termination Proxy)は**暗号化されたTCP接続**を持つことになり、**HTTP通信**を開始することができます。 + +そこで、クライアントは**HTTPSリクエスト**を送信します。これは、暗号化されたTLSコネクションを介した単なるHTTPリクエストです。 + + + +### リクエストの復号化 + +TLS Termination Proxy は、合意が取れている暗号化を使用して、**リクエストを復号化**し、**プレーン (復号化された) HTTP リクエスト** をアプリケーションを実行しているプロセス (例えば、FastAPI アプリケーションを実行している Uvicorn を持つプロセス) に送信します。 + + + +### HTTP レスポンス + +アプリケーションはリクエストを処理し、**プレーン(暗号化されていない)HTTPレスポンス** をTLS Termination Proxyに送信します。 + + + +### HTTPS レスポンス + +TLS Termination Proxyは次に、事前に合意が取れている暗号(`someapp.example.com`の証明書から始まる)を使って**レスポンスを暗号化し**、ブラウザに送り返す。 + +その後ブラウザでは、レスポンスが有効で正しい暗号キーで暗号化されていることなどを検証します。そして、ブラウザはレスポンスを**復号化**して処理します。 + + + +クライアント(ブラウザ)は、レスポンスが正しいサーバーから来たことを知ることができます。 なぜなら、そのサーバーは、以前に**HTTPS証明書**を使って合意した暗号を使っているからです。 + +### 複数のアプリケーション + +同じサーバー(または複数のサーバー)に、例えば他のAPIプログラムやデータベースなど、**複数のアプリケーション**が存在する可能性があります。 + +特定のIPとポート(この例ではTLS Termination Proxy)を扱うことができるのは1つのプロセスだけですが、他のアプリケーション/プロセスも、同じ**パブリックIPとポート**の組み合わせを使用しようとしない限り、サーバー上で実行することができます。 + + + +そうすれば、TLS Termination Proxy は、**複数のドメイン**や複数のアプリケーションのHTTPSと証明書を処理し、それぞれのケースで適切なアプリケーションにリクエストを送信することができます。 + +### 証明書の更新 + +将来のある時点で、各証明書は(取得後約3ヶ月で)**失効**します。 + +その後、Let's Encryptと通信する別のプログラム(別のプログラムである場合もあれば、同じTLS Termination Proxyである場合もある)によって、証明書を更新します。 + + + +**TLS証明書**は、IPアドレスではなく、**ドメイン名に関連付けられて**います。 + +したがって、証明書を更新するために、更新プログラムは、認証局(Let's Encrypt)に対して、**そのドメインが本当に「所有」し、管理している**ことを**証明**する必要があります。 + +そのために、またさまざまなアプリケーションのニーズに対応するために、いくつかの方法があります。よく使われる方法としては: + +* **いくつかのDNSレコードを修正します。** + * これをするためには、更新プログラムはDNSプロバイダーのAPIをサポートする必要があります。したがって、使用しているDNSプロバイダーによっては、このオプションが使える場合もあれば、使えない場合もあります。 +* ドメインに関連付けられたパブリックIPアドレス上で、(少なくとも証明書取得プロセス中は)**サーバー**として実行します。 + * 上で述べたように、特定のIPとポートでリッスンできるプロセスは1つだけです。 + * これは、同じTLS Termination Proxyが証明書の更新処理も行う場合に非常に便利な理由の1つです。 + * そうでなければ、TLS Termination Proxyを一時的に停止し、証明書を取得するために更新プログラムを起動し、TLS Termination Proxyで証明書を設定し、TLS Termination Proxyを再起動しなければならないかもしれません。TLS Termination Proxyが停止している間はアプリが利用できなくなるため、これは理想的ではありません。 + + +アプリを提供しながらこのような更新処理を行うことは、アプリケーション・サーバー(Uvicornなど)でTLS証明書を直接使用するのではなく、TLS Termination Proxyを使用して**HTTPSを処理する別のシステム**を用意したくなる主な理由の1つです。 + +## まとめ + +**HTTPS**を持つことは非常に重要であり、ほとんどの場合、かなり**クリティカル**です。開発者として HTTPS に関わる労力のほとんどは、これらの**概念とその仕組みを理解する**ことです。 + +しかし、ひとたび**開発者向けHTTPS**の基本的な情報を知れば、簡単な方法ですべてを管理するために、さまざまなツールを組み合わせて設定することができます。 + +次の章では、**FastAPI** アプリケーションのために **HTTPS** をセットアップする方法について、いくつかの具体例を紹介します。🔒 diff --git a/docs/ja/docs/deployment/index.md b/docs/ja/docs/deployment/index.md index 40710a93a1..897956e38f 100644 --- a/docs/ja/docs/deployment/index.md +++ b/docs/ja/docs/deployment/index.md @@ -1,4 +1,4 @@ -# デプロイ - イントロ +# デプロイ **FastAPI** 製のアプリケーションは比較的容易にデプロイできます。 diff --git a/docs/ja/docs/deployment/server-workers.md b/docs/ja/docs/deployment/server-workers.md new file mode 100644 index 0000000000..e1ea165a2c --- /dev/null +++ b/docs/ja/docs/deployment/server-workers.md @@ -0,0 +1,182 @@ +# Server Workers - Gunicorn と Uvicorn + +前回のデプロイメントのコンセプトを振り返ってみましょう: + +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* **レプリケーション(実行中のプロセス数)** +* メモリ +* 開始前の事前ステップ + +ここまでのドキュメントのチュートリアルでは、おそらくUvicornのような**サーバープログラム**を**単一のプロセス**で実行しています。 + +アプリケーションをデプロイする際には、**複数のコア**を利用し、そしてより多くのリクエストを処理できるようにするために、プロセスの**レプリケーション**を持つことを望むでしょう。 + +前のチャプターである[デプロイメントのコンセプト](./concepts.md){.internal-link target=_blank}にて見てきたように、有効な戦略がいくつかあります。 + +ここでは**Gunicorn**が**Uvicornのワーカー・プロセス**を管理する場合の使い方について紹介していきます。 + +!!! info + + DockerやKubernetesなどのコンテナを使用している場合は、次の章で詳しく説明します: [コンテナ内のFastAPI - Docker](./docker.md){.internal-link target=_blank} + + 特に**Kubernetes**上で実行する場合は、おそらく**Gunicornを使用せず**、**コンテナごとに単一のUvicornプロセス**を実行することになりますが、それについてはこの章の後半で説明します。 + +## GunicornによるUvicornのワーカー・プロセスの管理 + +**Gunicorn**は**WSGI標準**のアプリケーションサーバーです。このことは、GunicornはFlaskやDjangoのようなアプリケーションにサービスを提供できることを意味します。Gunicornそれ自体は**FastAPI**と互換性がないですが、というのもFastAPIは最新の**ASGI 標準**を使用しているためです。 + +しかし、Gunicornは**プロセスマネージャー**として動作し、ユーザーが特定の**ワーカー・プロセスクラス**を使用するように指示することができます。するとGunicornはそのクラスを使い1つ以上の**ワーカー・プロセス**を開始します。 + +そして**Uvicorn**には**Gunicorn互換のワーカークラス**があります。 + +この組み合わせで、Gunicornは**プロセスマネージャー**として動作し、**ポート**と**IP**をリッスンします。そして、**Uvicornクラス**を実行しているワーカー・プロセスに通信を**転送**します。 + +そして、Gunicorn互換の**Uvicornワーカー**クラスが、FastAPIが使えるように、Gunicornから送られてきたデータをASGI標準に変換する役割を担います。 + +## GunicornとUvicornをインストールする + +
+ +```console +$ pip install "uvicorn[standard]" gunicorn + +---> 100% +``` + +
+ +これによりUvicornと(高性能を得るための)標準(`standard`)の追加パッケージとGunicornの両方がインストールされます。 + +## UvicornのワーカーとともにGunicornを実行する + +Gunicornを以下のように起動させることができます: + +
+ +```console +$ gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 + +[19499] [INFO] Starting gunicorn 20.1.0 +[19499] [INFO] Listening at: http://0.0.0.0:80 (19499) +[19499] [INFO] Using worker: uvicorn.workers.UvicornWorker +[19511] [INFO] Booting worker with pid: 19511 +[19513] [INFO] Booting worker with pid: 19513 +[19514] [INFO] Booting worker with pid: 19514 +[19515] [INFO] Booting worker with pid: 19515 +[19511] [INFO] Started server process [19511] +[19511] [INFO] Waiting for application startup. +[19511] [INFO] Application startup complete. +[19513] [INFO] Started server process [19513] +[19513] [INFO] Waiting for application startup. +[19513] [INFO] Application startup complete. +[19514] [INFO] Started server process [19514] +[19514] [INFO] Waiting for application startup. +[19514] [INFO] Application startup complete. +[19515] [INFO] Started server process [19515] +[19515] [INFO] Waiting for application startup. +[19515] [INFO] Application startup complete. +``` + +
+ +それぞれのオプションの意味を見てみましょう: + +* `main:app`: `main`は"`main`"という名前のPythonモジュール、つまりファイル`main.py`を意味します。そして `app` は **FastAPI** アプリケーションの変数名です。 + * main:app`はPythonの`import`文と同じようなものだと想像できます: + + ```Python + from main import app + ``` + + * つまり、`main:app`のコロンは、`from main import app`のPythonの`import`の部分と同じになります。 + +* `--workers`: 使用するワーカー・プロセスの数で、それぞれがUvicornのワーカーを実行します。 + +* `--worker-class`: ワーカー・プロセスで使用するGunicorn互換のワーカークラスです。 + * ここではGunicornがインポートして使用できるクラスを渡します: + + ```Python + import uvicorn.workers.UvicornWorker + ``` + +* `--bind`: GunicornにリッスンするIPとポートを伝えます。コロン(`:`)でIPとポートを区切ります。 + * Uvicornを直接実行している場合は、`--bind 0.0.0.0:80` (Gunicornのオプション)の代わりに、`--host 0.0.0.0`と `--port 80`を使います。 + +出力では、各プロセスの**PID**(プロセスID)が表示されているのがわかります(単なる数字です)。 + +以下の通りです: + +* Gunicornの**プロセス・マネージャー**はPID `19499`(あなたの場合は違う番号でしょう)で始まります。 +* 次に、`Listening at: http://0.0.0.0:80`を開始します。 +* それから `uvicorn.workers.UvicornWorker` でワーカークラスを使用することを検出します。 +* そして、**4つのワーカー**を起動します。それぞれのワーカーのPIDは、`19511`、`19513`、`19514`、`19515`です。 + +Gunicornはまた、ワーカーの数を維持するために必要であれば、**ダウンしたプロセス**を管理し、**新しいプロセスを**再起動**させます。そのため、上記のリストにある**再起動**の概念に一部役立ちます。 + +しかしながら、必要であればGunicornを**再起動**させ、**起動時に実行**させるなど、外部のコンポーネントを持たせることも必要かもしれません。 + +## Uvicornとワーカー + +Uvicornには複数の**ワーカー・プロセス**を起動し実行するオプションもあります。 + +とはいうものの、今のところUvicornのワーカー・プロセスを扱う機能はGunicornよりも制限されています。そのため、このレベル(Pythonレベル)でプロセスマネージャーを持ちたいのであれば、Gunicornをプロセスマネージャーとして使ってみた方が賢明かもしれないです。 + +どんな場合であれ、以下のように実行します: + +
+ +```console +$ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 +INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit) +INFO: Started parent process [27365] +INFO: Started server process [27368] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27369] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27370] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27367] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +ここで唯一の新しいオプションは `--workers` で、Uvicornに4つのワーカー・プロセスを起動するように指示しています。 + +各プロセスの **PID** が表示され、親プロセスの `27365` (これは **プロセスマネージャ**) と、各ワーカー・プロセスの **PID** が表示されます: `27368`、`27369`、`27370`、`27367`になります。 + +## デプロイメントのコンセプト + +ここでは、アプリケーションの実行を**並列化**し、CPUの**マルチコア**を活用し、**より多くのリクエスト**に対応できるようにするために、**Gunicorn**(またはUvicorn)を使用して**Uvicornワーカー・プロセス**を管理する方法を見ていきました。 + +上記のデプロイのコンセプトのリストから、ワーカーを使うことは主に**レプリケーション**の部分と、**再起動**を少し助けてくれます: + +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* レプリケーション(実行中のプロセス数) +* メモリー +* 開始前の事前のステップ + + +## コンテナとDocker + +次章の[コンテナ内のFastAPI - Docker](./docker.md){.internal-link target=_blank}では、その他の**デプロイのコンセプト**を扱うために実施するであろう戦略をいくつか紹介します。 + +また、**GunicornとUvicornワーカー**を含む**公式Dockerイメージ**と、簡単なケースに役立ついくつかのデフォルト設定も紹介します。 + +また、(Gunicornを使わずに)Uvicornプロセスを1つだけ実行するために、**ゼロから独自のイメージを**構築する方法も紹介します。これは簡単なプロセスで、おそらく**Kubernetes**のような分散コンテナ管理システムを使うときにやりたいことでしょう。 + +## まとめ + +Uvicornワーカーを使ったプロセスマネージャとして**Gunicorn**(またはUvicorn)を使えば、**マルチコアCPU**を活用して**複数のプロセスを並列実行**できます。 + +これらのツールやアイデアは、**あなた自身のデプロイシステム**をセットアップしながら、他のデプロイコンセプトを自分で行う場合にも使えます。 + +次の章では、コンテナ(DockerやKubernetesなど)を使った**FastAPI**について学んでいきましょう。これらのツールには、他の**デプロイのコンセプト**も解決する簡単な方法があることがわかるでしょう。✨ diff --git a/docs/ja/docs/external-links.md b/docs/ja/docs/external-links.md index 6703f5fc26..aca5d5b34b 100644 --- a/docs/ja/docs/external-links.md +++ b/docs/ja/docs/external-links.md @@ -9,70 +9,21 @@ !!! tip "豆知識" ここにまだ載っていない**FastAPI**に関連する記事、プロジェクト、ツールなどがある場合は、 プルリクエストして下さい。 -## 記事 +{% for section_name, section_content in external_links.items() %} -### 英語 +## {{ section_name }} -{% if external_links %} -{% for article in external_links.articles.english %} +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* {{ item.title }} by {{ item.author }}. -* {{ article.title }} by {{ article.author }}. {% endfor %} -{% endif %} - -### 日本語 - -{% if external_links %} -{% for article in external_links.articles.japanese %} - -* {{ article.title }} by {{ article.author }}. {% endfor %} -{% endif %} - -### ベトナム語 - -{% if external_links %} -{% for article in external_links.articles.vietnamese %} - -* {{ article.title }} by {{ article.author }}. {% endfor %} -{% endif %} - -### ロシア語 - -{% if external_links %} -{% for article in external_links.articles.russian %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} - -### ドイツ語 - -{% if external_links %} -{% for article in external_links.articles.german %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} - -## ポッドキャスト - -{% if external_links %} -{% for article in external_links.podcasts.english %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} - -## トーク - -{% if external_links %} -{% for article in external_links.talks.english %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} ## プロジェクト diff --git a/docs/ja/docs/features.md b/docs/ja/docs/features.md index a40b48cf0e..853364f117 100644 --- a/docs/ja/docs/features.md +++ b/docs/ja/docs/features.md @@ -24,7 +24,7 @@ ### 現代的なPython -FastAPIの機能はすべて、標準のPython 3.6型宣言に基づいています(Pydanticの功績)。新しい構文はありません。ただの現代的な標準のPythonです。 +FastAPIの機能はすべて、標準のPython 3.8型宣言に基づいています(Pydanticの功績)。新しい構文はありません。ただの現代的な標準のPythonです。 (FastAPIを使用しない場合でも)Pythonの型の使用方法について簡単な復習が必要な場合は、短いチュートリアル([Python Types](python-types.md){.internal-link target=_blank})を参照してください。 diff --git a/docs/ja/docs/help-fastapi.md b/docs/ja/docs/help-fastapi.md index 166acb5869..e753b7ce37 100644 --- a/docs/ja/docs/help-fastapi.md +++ b/docs/ja/docs/help-fastapi.md @@ -82,20 +82,6 @@ GitHubレポジトリでhttps://gitter.im/tiangolo/fastapi. - -そこで、他の人と手早く会話したり、手助けやアイデアの共有などができます。 - -しかし、「自由な会話」が許容されているので一般的すぎて回答が難しい質問もしやすくなります。そのせいで回答を得られないかもしれません。 - -GitHub issuesでは良い回答を得やすい質問ができるように、もしくは、質問する前に自身で解決できるようにテンプレートがガイドしてくれます。そして、GitHubではたとえ時間がかかっても全てに答えているか確認できます。個人的にはGitterチャットでは同じことはできないです。😅 - -Gitterでの会話はGitHubほど簡単に検索できないので、質問と回答が会話の中に埋もれてしまいます。 - -一方、チャットには1000人以上いるので、いつでも話し相手が見つかる可能性が高いです。😄 - ## 開発者のスポンサーになる GitHub sponsorsを通して開発者を経済的にサポートできます。 diff --git a/docs/ja/docs/advanced/conditional-openapi.md b/docs/ja/docs/how-to/conditional-openapi.md similarity index 100% rename from docs/ja/docs/advanced/conditional-openapi.md rename to docs/ja/docs/how-to/conditional-openapi.md diff --git a/docs/ja/docs/index.md b/docs/ja/docs/index.md index f3a159f700..22c31e7ca9 100644 --- a/docs/ja/docs/index.md +++ b/docs/ja/docs/index.md @@ -24,7 +24,7 @@ --- -FastAPI は、Pythonの標準である型ヒントに基づいてPython 3.6 以降でAPI を構築するための、モダンで、高速(高パフォーマンス)な、Web フレームワークです。 +FastAPI は、Pythonの標準である型ヒントに基づいてPython 3.8 以降でAPI を構築するための、モダンで、高速(高パフォーマンス)な、Web フレームワークです。 主な特徴: @@ -107,7 +107,7 @@ FastAPI は、Pythonの標準である型ヒントに基づいてPython 3.6 以 ## 必要条件 -Python 3.7+ +Python 3.8+ FastAPI は巨人の肩の上に立っています。 @@ -317,7 +317,7 @@ def update_item(item_id: int, item: Item): 新しい構文や特定のライブラリのメソッドやクラスなどを覚える必要はありません。 -単なる標準的な**3.6 以降の Python**です。 +単なる標準的な**3.8 以降の Python**です。 例えば、`int`の場合: @@ -431,7 +431,6 @@ item: Item Pydantic によって使用されるもの: -- ujson - より速い JSON への"変換". - email_validator - E メールの検証 Starlette によって使用されるもの: diff --git a/docs/ja/docs/python-types.md b/docs/ja/docs/python-types.md new file mode 100644 index 0000000000..bbfef2adf1 --- /dev/null +++ b/docs/ja/docs/python-types.md @@ -0,0 +1,315 @@ +# Pythonの型の紹介 + +**Python 3.6以降** では「型ヒント」オプションがサポートされています。 + +これらの **"型ヒント"** は変数のを宣言することができる新しい構文です。(Python 3.6以降) + +変数に型を宣言することでエディターやツールがより良いサポートを提供することができます。 + +ここではPythonの型ヒントについての **クイックチュートリアル/リフレッシュ** で、**FastAPI**でそれらを使用するために必要な最低限のことだけをカバーしています。...実際には本当に少ないです。 + +**FastAPI** はすべてこれらの型ヒントに基づいており、多くの強みと利点を与えてくれます。 + +しかしたとえまったく **FastAPI** を使用しない場合でも、それらについて少し学ぶことで利点を得ることができるでしょう。 + +!!! note "備考" + もしあなたがPythonの専門家で、すでに型ヒントについてすべて知っているのであれば、次の章まで読み飛ばしてください。 + +## 動機 + +簡単な例から始めてみましょう: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +このプログラムを実行すると以下が出力されます: + +``` +John Doe +``` + +この関数は以下のようなことを行います: + +* `first_name`と`last_name`を取得します。 +* `title()`を用いて、それぞれの最初の文字を大文字に変換します。 +* 真ん中にスペースを入れて連結します。 + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### 編集 + +これはとても簡単なプログラムです。 + +しかし、今、あなたがそれを一から書いていたと想像してみてください。 + +パラメータの準備ができていたら、そのとき、関数の定義を始めていたことでしょう... + +しかし、そうすると「最初の文字を大文字に変換するあのメソッド」を呼び出す必要があります。 + +それは`upper`でしたか?`uppercase`でしたか?それとも`first_uppercase`?または`capitalize`? + +そして、古くからプログラマーの友人であるエディタで自動補完を試してみます。 + +関数の最初のパラメータ`first_name`を入力し、ドット(`.`)を入力してから、`Ctrl+Space`を押すと補完が実行されます。 + +しかし、悲しいことに、これはなんの役にも立ちません: + + + +### 型の追加 + +先ほどのコードから一行変更してみましょう。 + +以下の関数のパラメータ部分を: + +```Python + first_name, last_name +``` + +以下へ変更します: + +```Python + first_name: str, last_name: str +``` + +これだけです。 + +それが「型ヒント」です: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +これは、以下のようにデフォルト値を宣言するのと同じではありません: + +```Python + first_name="john", last_name="doe" +``` + +それとは別物です。 + +イコール(`=`)ではなく、コロン(`:`)を使用します。 + +そして、通常、型ヒントを追加しても、それらがない状態と起こることは何も変わりません。 + +しかし今、あなたが再びその関数を作成している最中に、型ヒントを使っていると想像してみて下さい。 + +同じタイミングで`Ctrl+Space`で自動補完を実行すると、以下のようになります: + + + +これであれば、あなたは「ベルを鳴らす」一つを見つけるまで、オプションを見て、スクロールすることができます: + + + +## より強い動機 + +この関数を見てください。すでに型ヒントを持っています: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +エディタは変数の型を知っているので、補完だけでなく、エラーチェックをすることもできます。 + + + +これで`age`を`str(age)`で文字列に変換して修正する必要があることがわかります: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## 型の宣言 + +関数のパラメータとして、型ヒントを宣言している主な場所を確認しました。 + +これは **FastAPI** で使用する主な場所でもあります。 + +### 単純な型 + +`str`だけでなく、Pythonの標準的な型すべてを宣言することができます。 + +例えば、以下を使用可能です: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### 型パラメータを持つジェネリック型 + +データ構造の中には、`dict`、`list`、`set`、そして`tuple`のように他の値を含むことができるものがあります。また内部の値も独自の型を持つことができます。 + +これらの型や内部の型を宣言するには、Pythonの標準モジュール`typing`を使用します。 + +これらの型ヒントをサポートするために特別に存在しています。 + +#### `List` + +例えば、`str`の`list`の変数を定義してみましょう。 + +`typing`から`List`をインポートします(大文字の`L`を含む): + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial006.py!} +``` + +同じようにコロン(`:`)の構文で変数を宣言します。 + +型として、`List`を入力します。 + +リストはいくつかの内部の型を含む型なので、それらを角括弧で囲んでいます。 + +```Python hl_lines="4" +{!../../../docs_src/python_types/tutorial006.py!} +``` + +!!! tip "豆知識" + 角括弧内の内部の型は「型パラメータ」と呼ばれています。 + + この場合、`str`は`List`に渡される型パラメータです。 + +つまり: 変数`items`は`list`であり、このリストの各項目は`str`です。 + +そうすることで、エディタはリストの項目を処理している間にもサポートを提供できます。 + + + +タイプがなければ、それはほぼ不可能です。 + +変数`item`はリスト`items`の要素の一つであることに注意してください。 + +それでも、エディタはそれが`str`であることを知っていて、そのためのサポートを提供しています。 + +#### `Tuple` と `Set` + +`tuple`と`set`の宣言も同様です: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial007.py!} +``` + +つまり: + +* 変数`items_t`は`int`、`int`、`str`の3つの項目を持つ`tuple`です + +* 変数`items_s`はそれぞれの項目が`bytes`型である`set`です。 + +#### `Dict` + +`dict`を宣言するためには、カンマ区切りで2つの型パラメータを渡します。 + +最初の型パラメータは`dict`のキーです。 + +2番目の型パラメータは`dict`の値です。 + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial008.py!} +``` + +つまり: + +* 変数`prices`は`dict`であり: + * この`dict`のキーは`str`型です。(つまり、各項目の名前) + * この`dict`の値は`float`型です。(つまり、各項目の価格) + +#### `Optional` + +また、`Optional`を使用して、変数が`str`のような型を持つことを宣言することもできますが、それは「オプション」であり、`None`にすることもできます。 + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +ただの`str`の代わりに`Optional[str]`を使用することで、エディタは値が常に`str`であると仮定している場合に実際には`None`である可能性があるエラーを検出するのに役立ちます。 + +#### ジェネリック型 + +以下のように角括弧で型パラメータを取る型を: + +* `List` +* `Tuple` +* `Set` +* `Dict` +* `Optional` +* ...など + +**ジェネリック型** または **ジェネリクス** と呼びます。 + +### 型としてのクラス + +変数の型としてクラスを宣言することもできます。 + +例えば、`Person`クラスという名前のクラスがあるとしましょう: + +```Python hl_lines="1 2 3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +変数の型を`Person`として宣言することができます: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +そして、再び、すべてのエディタのサポートを得ることができます: + + + +## Pydanticのモデル + +Pydantic はデータ検証を行うためのPythonライブラリです。 + +データの「形」を属性付きのクラスとして宣言します。 + +そして、それぞれの属性は型を持ちます。 + +さらに、いくつかの値を持つクラスのインスタンスを作成すると、その値を検証し、適切な型に変換して(もしそうであれば)全てのデータを持つオブジェクトを提供してくれます。 + +また、その結果のオブジェクトですべてのエディタのサポートを受けることができます。 + +Pydanticの公式ドキュメントから引用: + +```Python +{!../../../docs_src/python_types/tutorial011.py!} +``` + +!!! info "情報" + Pydanticについてより学びたい方はドキュメントを参照してください. + +**FastAPI** はすべてPydanticをベースにしています。 + +すべてのことは[チュートリアル - ユーザーガイド](tutorial/index.md){.internal-link target=_blank}で実際に見ることができます。 + +## **FastAPI**での型ヒント + +**FastAPI** はこれらの型ヒントを利用していくつかのことを行います。 + +**FastAPI** では型ヒントを使って型パラメータを宣言すると以下のものが得られます: + +* **エディタサポート**. +* **型チェック**. + +...そして **FastAPI** は同じように宣言をすると、以下のことを行います: + +* **要件の定義**: リクエストパスパラメータ、クエリパラメータ、ヘッダー、ボディ、依存関係などから要件を定義します。 +* **データの変換**: リクエストのデータを必要な型に変換します。 +* **データの検証**: リクエストごとに: + * データが無効な場合にクライアントに返される **自動エラー** を生成します。 +* **ドキュメント** OpenAPIを使用したAPI: + * 自動的に対話型ドキュメントのユーザーインターフェイスで使用されます。 + +すべてが抽象的に聞こえるかもしれません。心配しないでください。 この全ての動作は [チュートリアル - ユーザーガイド](tutorial/index.md){.internal-link target=_blank}で見ることができます。 + +重要なのは、Pythonの標準的な型を使うことで、(クラスやデコレータなどを追加するのではなく)1つの場所で **FastAPI** が多くの作業を代わりにやってくれているということです。 + +!!! info "情報" + すでにすべてのチュートリアルを終えて、型についての詳細を見るためにこのページに戻ってきた場合は、`mypy`のチートシートを参照してください diff --git a/docs/ja/docs/tutorial/background-tasks.md b/docs/ja/docs/tutorial/background-tasks.md new file mode 100644 index 0000000000..6094c370fd --- /dev/null +++ b/docs/ja/docs/tutorial/background-tasks.md @@ -0,0 +1,94 @@ +# バックグラウンドタスク + +レスポンスを返した *後に* 実行されるバックグラウンドタスクを定義できます。 + +これは、リクエスト後に処理を開始する必要があるが、クライアントがレスポンスを受け取る前に処理を終える必要のない操作に役立ちます。 + +これには、たとえば次のものが含まれます。 + +* 作業実行後のメール通知: + * メールサーバーへの接続とメールの送信は「遅い」(数秒) 傾向があるため、すぐにレスポンスを返し、バックグラウンドでメール通知ができます。 +* データ処理: + * たとえば、時間のかかる処理を必要とするファイル受信時には、「受信済み」(HTTP 202) のレスポンスを返し、バックグラウンドで処理できます。 + +## `BackgroundTasks` の使用 + +まず初めに、`BackgroundTasks` をインポートし、` BackgroundTasks` の型宣言と共に、*path operation 関数* のパラメーターを定義します: + +```Python hl_lines="1 13" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +**FastAPI** は、`BackgroundTasks` 型のオブジェクトを作成し、そのパラメーターに渡します。 + +## タスク関数の作成 + +バックグラウンドタスクとして実行される関数を作成します。 + +これは、パラメーターを受け取ることができる単なる標準的な関数です。 + +これは `async def` または通常の `def` 関数であり、**FastAPI** はこれを正しく処理します。 + +ここで、タスク関数はファイル書き込みを実行します (メール送信のシミュレーション)。 + +また、書き込み操作では `async` と `await` を使用しないため、通常の `def` で関数を定義します。 + +```Python hl_lines="6-9" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +## バックグラウンドタスクの追加 + +*path operations 関数* 内で、`.add_task()` メソッドを使用してタスク関数を *background tasks* オブジェクトに渡します。 + +```Python hl_lines="14" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +`.add_task()` は以下の引数を受け取ります: + +* バックグラウンドで実行されるタスク関数 (`write_notification`)。 +* タスク関数に順番に渡す必要のある引数の列 (`email`)。 +* タスク関数に渡す必要のあるキーワード引数 (`message="some notification"`)。 + +## 依存性注入 + +`BackgroundTasks` の使用は依存性注入システムでも機能し、様々な階層 (*path operations 関数*、依存性 (依存可能性)、サブ依存性など) で `BackgroundTasks` 型のパラメーターを宣言できます。 + +**FastAPI** は、それぞれの場合の処理​​方法と同じオブジェクトの再利用方法を知っているため、すべてのバックグラウンドタスクがマージされ、バックグラウンドで後で実行されます。 + +```Python hl_lines="13 15 22 25" +{!../../../docs_src/background_tasks/tutorial002.py!} +``` + +この例では、レスポンスが送信された *後* にメッセージが `log.txt` ファイルに書き込まれます。 + +リクエストにクエリがあった場合、バックグラウンドタスクでログに書き込まれます。 + +そして、*path operations 関数* で生成された別のバックグラウンドタスクは、`email` パスパラメータを使用してメッセージを書き込みます。 + +## 技術的な詳細 + +`BackgroundTasks` クラスは、`starlette.background`から直接取得されます。 + +これは、FastAPI に直接インポート/インクルードされるため、`fastapi` からインポートできる上に、`starlette.background`から別の `BackgroundTask` (末尾に `s` がない) を誤ってインポートすることを回避できます。 + +`BackgroundTasks`のみを使用することで (`BackgroundTask` ではなく)、`Request` オブジェクトを直接使用する場合と同様に、それを *path operations 関数* パラメーターとして使用し、**FastAPI** に残りの処理を任せることができます。 + +それでも、FastAPI で `BackgroundTask` を単独で使用することは可能ですが、コード内でオブジェクトを作成し、それを含むStarlette `Response` を返す必要があります。 + +詳細については、バックグラウンドタスクに関する Starlette の公式ドキュメントを参照して下さい。 + +## 警告 + +大量のバックグラウンド計算が必要であり、必ずしも同じプロセスで実行する必要がない場合 (たとえば、メモリや変数などを共有する必要がない場合)、Celery のようなより大きな他のツールを使用するとメリットがあるかもしれません。 + +これらは、より複雑な構成、RabbitMQ や Redis などのメッセージ/ジョブキューマネージャーを必要とする傾向がありますが、複数のプロセス、特に複数のサーバーでバックグラウンドタスクを実行できます。 + +例を確認するには、[Project Generators](../project-generation.md){.internal-link target=_blank} を参照してください。これらにはすべて、Celery が構築済みです。 + +ただし、同じ **FastAPI** アプリから変数とオブジェクトにアクセスする必要がある場合、または小さなバックグラウンドタスク (電子メール通知の送信など) を実行する必要がある場合は、単に `BackgroundTasks` を使用できます。 + +## まとめ + +`BackgroundTasks` をインポートして、*path operations 関数* や依存関係のパラメータに `BackgroundTasks`を使用し、バックグラウンドタスクを追加して下さい。 diff --git a/docs/ja/docs/tutorial/body-fields.md b/docs/ja/docs/tutorial/body-fields.md new file mode 100644 index 0000000000..8f01e82162 --- /dev/null +++ b/docs/ja/docs/tutorial/body-fields.md @@ -0,0 +1,48 @@ +# ボディ - フィールド + +`Query`や`Path`、`Body`を使って *path operation関数* のパラメータに追加のバリデーションやメタデータを宣言するのと同じように、Pydanticの`Field`を使ってPydanticモデルの内部でバリデーションやメタデータを宣言することができます。 + +## `Field`のインポート + +まず、以下のようにインポートします: + +```Python hl_lines="4" +{!../../../docs_src/body_fields/tutorial001.py!} +``` + +!!! warning "注意" + `Field`は他の全てのもの(`Query`、`Path`、`Body`など)とは違い、`fastapi`からではなく、`pydantic`から直接インポートされていることに注意してください。 + +## モデルの属性の宣言 + +以下のように`Field`をモデルの属性として使用することができます: + +```Python hl_lines="11 12 13 14" +{!../../../docs_src/body_fields/tutorial001.py!} +``` + +`Field`は`Query`や`Path`、`Body`と同じように動作し、全く同様のパラメータなどを持ちます。 + +!!! note "技術詳細" + 実際には次に見る`Query`や`Path`などは、共通の`Param`クラスのサブクラスのオブジェクトを作成しますが、それ自体はPydanticの`FieldInfo`クラスのサブクラスです。 + + また、Pydanticの`Field`は`FieldInfo`のインスタンスも返します。 + + `Body`は`FieldInfo`のサブクラスのオブジェクトを直接返すこともできます。そして、他にも`Body`クラスのサブクラスであるものがあります。 + + `fastapi`から`Query`や`Path`などをインポートする場合、これらは実際には特殊なクラスを返す関数であることに注意してください。 + +!!! tip "豆知識" + 型、デフォルト値、`Field`を持つ各モデルの属性が、`Path`や`Query`、`Body`の代わりに`Field`を持つ、*path operation 関数の*パラメータと同じ構造になっていることに注目してください。 + +## 追加情報の追加 + +追加情報は`Field`や`Query`、`Body`などで宣言することができます。そしてそれは生成されたJSONスキーマに含まれます。 + +後に例を用いて宣言を学ぶ際に、追加情報を句悪方法を学べます。 + +## まとめ + +Pydanticの`Field`を使用して、モデルの属性に追加のバリデーションやメタデータを宣言することができます。 + +追加のキーワード引数を使用して、追加のJSONスキーマのメタデータを渡すこともできます。 diff --git a/docs/ja/docs/tutorial/body-multiple-params.md b/docs/ja/docs/tutorial/body-multiple-params.md new file mode 100644 index 0000000000..2ba10c583e --- /dev/null +++ b/docs/ja/docs/tutorial/body-multiple-params.md @@ -0,0 +1,169 @@ +# ボディ - 複数のパラメータ + +これまで`Path`と`Query`をどう使うかを見てきましたが、リクエストボディの宣言のより高度な使い方を見てみましょう。 + +## `Path`、`Query`とボディパラメータを混ぜる + +まず、もちろん、`Path`と`Query`とリクエストボディのパラメータの宣言は自由に混ぜることができ、 **FastAPI** は何をするべきかを知っています。 + +また、デフォルトの`None`を設定することで、ボディパラメータをオプションとして宣言することもできます: + +```Python hl_lines="19 20 21" +{!../../../docs_src/body_multiple_params/tutorial001.py!} +``` + +!!! note "備考" + この場合、ボディから取得する`item`はオプションであることに注意してください。デフォルト値は`None`です。 + +## 複数のボディパラメータ + +上述の例では、*path operations*は`item`の属性を持つ以下のようなJSONボディを期待していました: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +しかし、`item`と`user`のように複数のボディパラメータを宣言することもできます: + +```Python hl_lines="22" +{!../../../docs_src/body_multiple_params/tutorial002.py!} +``` + +この場合、**FastAPI**は関数内に複数のボディパラメータ(Pydanticモデルである2つのパラメータ)があることに気付きます。 + +そのため、パラメータ名をボディのキー(フィールド名)として使用し、以下のようなボディを期待しています: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + } +} +``` + +!!! note "備考" + 以前と同じように`item`が宣言されていたにもかかわらず、`item`はキー`item`を持つボディの内部にあることが期待されていることに注意してください。 + +**FastAPI** はリクエストから自動で変換を行い、パラメータ`item`が特定の内容を受け取り、`user`も同じように特定の内容を受け取ります。 + +複合データの検証を行い、OpenAPIスキーマや自動ドキュメントのように文書化してくれます。 + +## ボディ内の単数値 + +クエリとパスパラメータの追加データを定義するための `Query` と `Path` があるのと同じように、 **FastAPI** は同等の `Body` を提供します。 + +例えば、前のモデルを拡張して、同じボディに `item` と `user` の他にもう一つのキー `importance` を入れたいと決めることができます。 + +単数値なのでそのまま宣言すると、**FastAPI** はそれがクエリパラメータであるとみなします。 + +しかし、`Body`を使用して、**FastAPI** に別のボディキーとして扱うように指示することができます: + + +```Python hl_lines="23" +{!../../../docs_src/body_multiple_params/tutorial003.py!} +``` + +この場合、**FastAPI** は以下のようなボディを期待します: + + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + }, + "importance": 5 +} +``` + +繰り返しになりますが、データ型の変換、検証、文書化などを行います。 + +## 複数のボディパラメータとクエリ + +もちろん、ボディパラメータに加えて、必要に応じて追加のクエリパラメータを宣言することもできます。 + +デフォルトでは、単数値はクエリパラメータとして解釈されるので、明示的に `Query` を追加する必要はありません。 + +```Python +q: str = None +``` + +以下において: + +```Python hl_lines="27" +{!../../../docs_src/body_multiple_params/tutorial004.py!} +``` + +!!! info "情報" + `Body`もまた、後述する `Query` や `Path` などと同様に、すべての検証パラメータとメタデータパラメータを持っています。 + + +## 単一のボディパラメータの埋め込み + +Pydanticモデル`Item`のボディパラメータ`item`を1つだけ持っているとしましょう。 + +デフォルトでは、**FastAPI**はそのボディを直接期待します。 + +しかし、追加のボディパラメータを宣言したときのように、キー `item` を持つ JSON とその中のモデルの内容を期待したい場合は、特別な `Body` パラメータ `embed` を使うことができます: + +```Python +item: Item = Body(..., embed=True) +``` + +以下において: + +```Python hl_lines="17" +{!../../../docs_src/body_multiple_params/tutorial005.py!} +``` + +この場合、**FastAPI** は以下のようなボディを期待します: + +```JSON hl_lines="2" +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + } +} +``` + +以下の代わりに: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +## まとめ + +リクエストが単一のボディしか持てない場合でも、*path operation関数*に複数のボディパラメータを追加することができます。 + +しかし、**FastAPI** はそれを処理し、関数内の正しいデータを与え、*path operation*内の正しいスキーマを検証し、文書化します。 + +また、ボディの一部として受け取る単数値を宣言することもできます。 + +また、単一のパラメータしか宣言されていない場合でも、ボディをキーに埋め込むように **FastAPI** に指示することができます。 diff --git a/docs/ja/docs/tutorial/body-nested-models.md b/docs/ja/docs/tutorial/body-nested-models.md new file mode 100644 index 0000000000..7f916c47a1 --- /dev/null +++ b/docs/ja/docs/tutorial/body-nested-models.md @@ -0,0 +1,244 @@ +# ボディ - ネストされたモデル + +**FastAPI** を使用すると、深くネストされた任意のモデルを定義、検証、文書化、使用することができます(Pydanticのおかげです)。 + +## リストのフィールド + +属性をサブタイプとして定義することができます。例えば、Pythonの`list`は以下のように定義できます: + +```Python hl_lines="12" +{!../../../docs_src/body_nested_models/tutorial001.py!} +``` + +これにより、各項目の型は宣言されていませんが、`tags`はある項目のリストになります。 + +## タイプパラメータを持つリストのフィールド + +しかし、Pythonには型や「タイプパラメータ」を使ってリストを宣言する方法があります: + +### typingの`List`をインポート + +まず、Pythonの標準の`typing`モジュールから`List`をインポートします: + +```Python hl_lines="1" +{!../../../docs_src/body_nested_models/tutorial002.py!} +``` + +### タイプパラメータを持つ`List`の宣言 + +`list`や`dict`、`tuple`のようなタイプパラメータ(内部の型)を持つ型を宣言するには: + +* `typing`モジュールからそれらをインストールします。 +* 角括弧(`[`と`]`)を使って「タイプパラメータ」として内部の型を渡します: + +```Python +from typing import List + +my_list: List[str] +``` + +型宣言の標準的なPythonの構文はこれだけです。 + +内部の型を持つモデルの属性にも同じ標準の構文を使用してください。 + +そのため、以下の例では`tags`を具体的な「文字列のリスト」にすることができます: + +```Python hl_lines="14" +{!../../../docs_src/body_nested_models/tutorial002.py!} +``` + +## セット型 + +しかし、よく考えてみると、タグは繰り返すべきではなく、おそらくユニークな文字列になるのではないかと気付いたとします。 + +そして、Pythonにはユニークな項目のセットのための特別なデータ型`set`があります。 + +そのため、以下のように、`Set`をインポートして`str`の`set`として`tags`を宣言することができます: + +```Python hl_lines="1 14" +{!../../../docs_src/body_nested_models/tutorial003.py!} +``` + +これを使えば、データが重複しているリクエストを受けた場合でも、ユニークな項目のセットに変換されます。 + +そして、そのデータを出力すると、たとえソースに重複があったとしても、固有の項目のセットとして出力されます。 + +また、それに応じて注釈をつけたり、文書化したりします。 + +## ネストされたモデル + +Pydanticモデルの各属性には型があります。 + +しかし、その型はそれ自体が別のPydanticモデルである可能性があります。 + +そのため、特定の属性名、型、バリデーションを指定して、深くネストしたJSON`object`を宣言することができます。 + +すべては、任意のネストにされています。 + +### サブモデルの定義 + +例えば、`Image`モデルを定義することができます: + +```Python hl_lines="9 10 11" +{!../../../docs_src/body_nested_models/tutorial004.py!} +``` + +### サブモデルを型として使用 + +そして、それを属性の型として使用することができます: + +```Python hl_lines="20" +{!../../../docs_src/body_nested_models/tutorial004.py!} +``` + +これは **FastAPI** が以下のようなボディを期待することを意味します: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": ["rock", "metal", "bar"], + "image": { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + } +} +``` + +繰り返しになりますが、**FastAPI** を使用して、その宣言を行うだけで以下のような恩恵を受けられます: + +* ネストされたモデルでも対応可能なエディタのサポート(補完など) +* データ変換 +* データの検証 +* 自動文書化 + +## 特殊な型とバリデーション + +`str`や`int`、`float`のような通常の単数型の他にも、`str`を継承したより複雑な単数型を使うこともできます。 + +すべてのオプションをみるには、Pydanticのエキゾチック な型のドキュメントを確認してください。次の章でいくつかの例をみることができます。 + +例えば、`Image`モデルのように`url`フィールドがある場合、`str`の代わりにPydanticの`HttpUrl`を指定することができます: + +```Python hl_lines="4 10" +{!../../../docs_src/body_nested_models/tutorial005.py!} +``` + +文字列は有効なURLであることが確認され、そのようにJSONスキーマ・OpenAPIで文書化されます。 + +## サブモデルのリストを持つ属性 + +Pydanticモデルを`list`や`set`などのサブタイプとして使用することもできます: + +```Python hl_lines="20" +{!../../../docs_src/body_nested_models/tutorial006.py!} +``` + +これは、次のようなJSONボディを期待します(変換、検証、ドキュメントなど): + +```JSON hl_lines="11" +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": [ + "rock", + "metal", + "bar" + ], + "images": [ + { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + }, + { + "url": "http://example.com/dave.jpg", + "name": "The Baz" + } + ] +} +``` + +!!! info "情報" + `images`キーが画像オブジェクトのリストを持つようになったことに注目してください。 + +## 深くネストされたモデル + +深くネストされた任意のモデルを定義することができます: + +```Python hl_lines="9 14 20 23 27" +{!../../../docs_src/body_nested_models/tutorial007.py!} +``` + +!!! info "情報" + `Offer`は`Item`のリストであり、オプションの`Image`のリストを持っていることに注目してください。 + +## 純粋なリストのボディ + +期待するJSONボディのトップレベルの値がJSON`array`(Pythonの`list`)であれば、Pydanticモデルと同じように、関数のパラメータで型を宣言することができます: + +```Python +images: List[Image] +``` + +以下のように: + +```Python hl_lines="15" +{!../../../docs_src/body_nested_models/tutorial008.py!} +``` + +## あらゆる場所でのエディタサポート + +エディタのサポートもどこでも受けることができます。 + +以下のようにリストの中の項目でも: + + + +Pydanticモデルではなく、`dict`を直接使用している場合はこのようなエディタのサポートは得られません。 + +しかし、それらについて心配する必要はありません。入力された辞書は自動的に変換され、出力も自動的にJSONに変換されます。 + +## 任意の`dict`のボディ + +また、ある型のキーと別の型の値を持つ`dict`としてボディを宣言することもできます。 + +有効なフィールド・属性名を事前に知る必要がありません(Pydanticモデルの場合のように)。 + +これは、まだ知らないキーを受け取りたいときに便利だと思います。 + +--- + +他にも、`int`のように他の型のキーを持ちたい場合などに便利です。 + +それをここで見ていきましょう。 + +この場合、`int`のキーと`float`の値を持つものであれば、どんな`dict`でも受け入れることができます: + +```Python hl_lines="15" +{!../../../docs_src/body_nested_models/tutorial009.py!} +``` + +!!! tip "豆知識" + JSONはキーとして`str`しかサポートしていないことに注意してください。 + + しかしPydanticには自動データ変換機能があります。 + + これは、APIクライアントがキーとして文字列しか送信できなくても、それらの文字列に純粋な整数が含まれている限り、Pydanticが変換して検証することを意味します。 + + そして、`weights`として受け取る`dict`は、実際には`int`のキーと`float`の値を持つことになります。 + +## まとめ + +**FastAPI** を使用すると、Pydanticモデルが提供する最大限の柔軟性を持ちながら、コードをシンプルに短く、エレガントに保つことができます。 + +以下のような利点があります: + +* エディタのサポート(どこでも補完!) +* データ変換(別名:構文解析・シリアライズ) +* データの検証 +* スキーマ文書 +* 自動文書化 diff --git a/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md new file mode 100644 index 0000000000..5c150d00c5 --- /dev/null +++ b/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md @@ -0,0 +1,191 @@ +# 依存関係としてのクラス + +**依存性注入** システムを深く掘り下げる前に、先ほどの例をアップグレードしてみましょう。 + +## 前の例の`dict` + +前の例では、依存関係("dependable")から`dict`を返していました: + +```Python hl_lines="9" +{!../../../docs_src/dependencies/tutorial001.py!} +``` + +しかし、*path operation関数*のパラメータ`commons`に`dict`が含まれています。 + +また、エディタは`dict`のキーと値の型を知ることができないため、多くのサポート(補完のような)を提供することができません。 + +もっとうまくやれるはずです...。 + +## 依存関係を作るもの + +これまでは、依存関係が関数として宣言されているのを見てきました。 + +しかし、依存関係を定義する方法はそれだけではありません(その方が一般的かもしれませんが)。 + +重要なのは、依存関係が「呼び出し可能」なものであることです。 + +Pythonにおける「**呼び出し可能**」とは、Pythonが関数のように「呼び出す」ことができるものを指します。 + +そのため、`something`オブジェクト(関数ではないかもしれませんが)を持っていて、それを次のように「呼び出す」(実行する)ことができるとします: + +```Python +something() +``` + +または + +```Python +something(some_argument, some_keyword_argument="foo") +``` + +これを「呼び出し可能」なものと呼びます。 + +## 依存関係としてのクラス + +Pythonのクラスのインスタンスを作成する際に、同じ構文を使用していることに気づくかもしれません。 + +例えば: + +```Python +class Cat: + def __init__(self, name: str): + self.name = name + + +fluffy = Cat(name="Mr Fluffy") +``` + +この場合、`fluffy`は`Cat`クラスのインスタンスです。 + +そして`fluffy`を作成するために、`Cat`を「呼び出している」ことになります。 + +そのため、Pythonのクラスもまた「呼び出し可能」です。 + +そして、**FastAPI** では、Pythonのクラスを依存関係として使用することができます。 + +FastAPIが実際にチェックしているのは、それが「呼び出し可能」(関数、クラス、その他なんでも)であり、パラメータが定義されているかどうかということです。 + +**FastAPI** の依存関係として「呼び出し可能なもの」を渡すと、その「呼び出し可能なもの」のパラメータを解析し、サブ依存関係も含めて、*path operation関数*のパラメータと同じように処理します。 + +それは、パラメータが全くない呼び出し可能なものにも適用されます。パラメータのない*path operation関数*と同じように。 + +そこで、上で紹介した依存関係の`common_parameters`を`CommonQueryParams`クラスに変更します: + +```Python hl_lines="11 12 13 14 15" +{!../../../docs_src/dependencies/tutorial002.py!} +``` + +クラスのインスタンスを作成するために使用される`__init__`メソッドに注目してください: + +```Python hl_lines="12" +{!../../../docs_src/dependencies/tutorial002.py!} +``` + +...以前の`common_parameters`と同じパラメータを持っています: + +```Python hl_lines="8" +{!../../../docs_src/dependencies/tutorial001.py!} +``` + +これらのパラメータは **FastAPI** が依存関係を「解決」するために使用するものです。 + +どちらの場合も以下を持っています: + +* オプショナルの`q`クエリパラメータ。 +* `skip`クエリパラメータ、デフォルトは`0`。 +* `limit`クエリパラメータ、デフォルトは`100`。 + +どちらの場合も、データは変換され、検証され、OpenAPIスキーマなどで文書化されます。 + +## 使用 + +これで、このクラスを使用して依存関係を宣言することができます。 + +```Python hl_lines="19" +{!../../../docs_src/dependencies/tutorial002.py!} +``` + +**FastAPI** は`CommonQueryParams`クラスを呼び出します。これにより、そのクラスの「インスタンス」が作成され、インスタンスはパラメータ`commons`として関数に渡されます。 + +## 型注釈と`Depends` + +上のコードでは`CommonQueryParams`を2回書いていることに注目してください: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +以下にある最後の`CommonQueryParams`: + +```Python +... = Depends(CommonQueryParams) +``` + +...は、**FastAPI** が依存関係を知るために実際に使用するものです。 + +そこからFastAPIが宣言されたパラメータを抽出し、それが実際にFastAPIが呼び出すものです。 + +--- + +この場合、以下にある最初の`CommonQueryParams`: + +```Python +commons: CommonQueryParams ... +``` + +...は **FastAPI** に対して特別な意味をもちません。FastAPIはデータ変換や検証などには使用しません(それらのためには`= Depends(CommonQueryParams)`を使用しています)。 + +実際には以下のように書けばいいだけです: + +```Python +commons = Depends(CommonQueryParams) +``` + +以下にあるように: + +```Python hl_lines="19" +{!../../../docs_src/dependencies/tutorial003.py!} +``` + +しかし、型を宣言することは推奨されています。そうすれば、エディタは`commons`のパラメータとして何が渡されるかを知ることができ、コードの補完や型チェックなどを行うのに役立ちます: + + + +## ショートカット + +しかし、ここでは`CommonQueryParams`を2回書くというコードの繰り返しが発生していることがわかります: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +依存関係が、クラス自体のインスタンスを作成するために**FastAPI**が「呼び出す」*特定の*クラスである場合、**FastAPI** はこれらのケースのショートカットを提供しています。 + +それらの具体的なケースについては以下のようにします: + +以下のように書く代わりに: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +...以下のように書きます: + +```Python +commons: CommonQueryParams = Depends() +``` + +パラメータの型として依存関係を宣言し、`Depends()`の中でパラメータを指定せず、`Depends()`をその関数のパラメータの「デフォルト」値(`=`のあとの値)として使用することで、`Depends(CommonQueryParams)`の中でクラス全体を*もう一度*書かなくてもよくなります。 + +同じ例では以下のようになります: + +```Python hl_lines="19" +{!../../../docs_src/dependencies/tutorial004.py!} +``` + +...そして **FastAPI** は何をすべきか知っています。 + +!!! tip "豆知識" + 役に立つというよりも、混乱するようであれば無視してください。それをする*必要*はありません。 + + それは単なるショートカットです。なぜなら **FastAPI** はコードの繰り返しを最小限に抑えることに気を使っているからです。 diff --git a/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md new file mode 100644 index 0000000000..1684d9ca1e --- /dev/null +++ b/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -0,0 +1,62 @@ +# path operationデコレータの依存関係 + +場合によっては*path operation関数*の中で依存関係の戻り値を本当に必要としないこともあります。 + +もしくは、依存関係が値を返さない場合もあります。 + +しかし、それでも実行・解決する必要があります。 + +このような場合、*path operation関数*のパラメータを`Depends`で宣言する代わりに、*path operation decorator*に`dependencies`の`list`を追加することができます。 + +## *path operationデコレータ*への`dependencies`の追加 + +*path operationデコレータ*はオプショナルの引数`dependencies`を受け取ります。 + +それは`Depends()`の`list`であるべきです: + +```Python hl_lines="17" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +これらの依存関係は、通常の依存関係と同様に実行・解決されます。しかし、それらの値(何かを返す場合)は*path operation関数*には渡されません。 + +!!! tip "豆知識" + エディタによっては、未使用の関数パラメータをチェックしてエラーとして表示するものもあります。 + + `dependencies`を`path operationデコレータ`で使用することで、エディタやツールのエラーを回避しながら確実に実行することができます。 + + また、コードの未使用のパラメータがあるのを見て、それが不要だと思ってしまうような新しい開発者の混乱を避けるのにも役立つかもしれません。 + +## 依存関係のエラーと戻り値 + +通常使用している依存関係の*関数*と同じものを使用することができます。 + +### 依存関係の要件 + +これらはリクエストの要件(ヘッダのようなもの)やその他のサブ依存関係を宣言することができます: + +```Python hl_lines="6 11" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +### 例外の発生 + +これらの依存関係は通常の依存関係と同じように、例外を`raise`発生させることができます: + +```Python hl_lines="8 13" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +### 戻り値 + +そして、値を返すことも返さないこともできますが、値は使われません。 + +つまり、すでにどこかで使っている通常の依存関係(値を返すもの)を再利用することができ、値は使われなくても依存関係は実行されます: + +```Python hl_lines="9 14" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +## *path operations*のグループに対する依存関係 + +後で、より大きなアプリケーションの構造([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank})について読む時に、おそらく複数のファイルを使用して、*path operations*のグループに対して単一の`dependencies`パラメータを宣言する方法を学ぶでしょう。 diff --git a/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md new file mode 100644 index 0000000000..2a89e51d2b --- /dev/null +++ b/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md @@ -0,0 +1,225 @@ +# yieldを持つ依存関係 + +FastAPIは、いくつかの終了後の追加のステップを行う依存関係をサポートしています。 + +これを行うには、`return`の代わりに`yield`を使い、その後に追加のステップを書きます。 + +!!! tip "豆知識" + `yield`は必ず一度だけ使用するようにしてください。 + +!!! info "情報" + これを動作させるには、**Python 3.7** 以上を使用するか、**Python 3.6** では"backports"をインストールする必要があります: + + ``` + pip install async-exit-stack async-generator + ``` + + これによりasync-exit-stackasync-generatorがインストールされます。 + +!!! note "技術詳細" + 以下と一緒に使用できる関数なら何でも有効です: + + * `@contextlib.contextmanager`または + * `@contextlib.asynccontextmanager` + + これらは **FastAPI** の依存関係として使用するのに有効です。 + + 実際、FastAPIは内部的にこれら2つのデコレータを使用しています。 + +## `yield`を持つデータベースの依存関係 + +例えば、これを使ってデータベースセッションを作成し、終了後にそれを閉じることができます。 + +レスポンスを送信する前に`yield`文を含む前のコードのみが実行されます。 + +```Python hl_lines="2 3 4" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +生成された値は、*path operations*や他の依存関係に注入されるものです: + +```Python hl_lines="4" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +`yield`文に続くコードは、レスポンスが送信された後に実行されます: + +```Python hl_lines="5 6" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +!!! tip "豆知識" + `async`や通常の関数を使用することができます。 + + **FastAPI** は、通常の依存関係と同じように、それぞれで正しいことを行います。 + +## `yield`と`try`を持つ依存関係 + +`yield`を持つ依存関係で`try`ブロックを使用した場合、その依存関係を使用した際に発生した例外を受け取ることになります。 + +例えば、途中のどこかの時点で、別の依存関係や*path operation*の中で、データベーストランザクションを「ロールバック」したり、その他のエラーを作成したりするコードがあった場合、依存関係の中で例外を受け取ることになります。 + +そのため、依存関係の中にある特定の例外を`except SomeException`で探すことができます。 + +同様に、`finally`を用いて例外があったかどうかにかかわらず、終了ステップを確実に実行することができます。 + +```Python hl_lines="3 5" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +## `yield`を持つサブ依存関係 + +任意の大きさや形のサブ依存関係やサブ依存関係の「ツリー」を持つことができ、その中で`yield`を使用することができます。 + +**FastAPI** は、`yield`を持つ各依存関係の「終了コード」が正しい順番で実行されていることを確認します。 + +例えば、`dependency_c`は`dependency_b`と`dependency_b`に依存する`dependency_a`に、依存することができます: + +```Python hl_lines="4 12 20" +{!../../../docs_src/dependencies/tutorial008.py!} +``` + +そして、それらはすべて`yield`を使用することができます。 + +この場合、`dependency_c`は終了コードを実行するために、`dependency_b`(ここでは`dep_b`という名前)の値がまだ利用可能である必要があります。 + +そして、`dependency_b`は`dependency_a`(ここでは`dep_a`という名前)の値を終了コードで利用できるようにする必要があります。 + +```Python hl_lines="16 17 24 25" +{!../../../docs_src/dependencies/tutorial008.py!} +``` + +同様に、`yield`と`return`が混在した依存関係を持つこともできます。 + +また、単一の依存関係を持っていて、`yield`などの他の依存関係をいくつか必要とすることもできます。 + +依存関係の組み合わせは自由です。 + +**FastAPI** は、全てが正しい順序で実行されていることを確認します。 + +!!! note "技術詳細" + これはPythonのContext Managersのおかげで動作します。 + + **FastAPI** はこれを実現するために内部的に使用しています。 + +## `yield`と`HTTPException`を持つ依存関係 + +`yield`と例外をキャッチする`try`ブロックを持つことができる依存関係を使用することができることがわかりました。 + +`yield`の後の終了コードで`HTTPException`などを発生させたくなるかもしれません。しかし**それはうまくいきません** + +`yield`を持つ依存関係の終了コードは[例外ハンドラ](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}の*後に*実行されます。依存関係によって投げられた例外を終了コード(`yield`の後)でキャッチするものはなにもありません。 + +つまり、`yield`の後に`HTTPException`を発生させた場合、`HTTTPException`をキャッチしてHTTP 400のレスポンスを返すデフォルトの(あるいは任意のカスタムの)例外ハンドラは、その例外をキャッチすることができなくなります。 + +これは、依存関係に設定されているもの(例えば、DBセッション)を、例えば、バックグラウンドタスクで使用できるようにするものです。 + +バックグラウンドタスクはレスポンスが送信された*後*に実行されます。そのため、*すでに送信されている*レスポンスを変更する方法すらないので、`HTTPException`を発生させる方法はありません。 + +しかし、バックグラウンドタスクがDBエラーを発生させた場合、少なくとも`yield`で依存関係のセッションをロールバックしたり、きれいに閉じたりすることができ、エラーをログに記録したり、リモートのトラッキングシステムに報告したりすることができます。 + +例外が発生する可能性があるコードがある場合は、最も普通の「Python流」なことをして、コードのその部分に`try`ブロックを追加してください。 + +レスポンスを返したり、レスポンスを変更したり、`HTTPException`を発生させたりする*前に*処理したいカスタム例外がある場合は、[カスタム例外ハンドラ](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}を作成してください。 + +!!! tip "豆知識" + `HTTPException`を含む例外は、`yield`の*前*でも発生させることができます。ただし、後ではできません。 + +実行の順序は多かれ少なかれ以下の図のようになります。時間は上から下へと流れていきます。そして、各列はコードを相互作用させたり、実行したりしている部分の一つです。 + +```mermaid +sequenceDiagram + +participant client as Client +participant handler as Exception handler +participant dep as Dep with yield +participant operation as Path Operation +participant tasks as Background tasks + + Note over client,tasks: Can raise exception for dependency, handled after response is sent + Note over client,operation: Can raise HTTPException and can change the response + client ->> dep: Start request + Note over dep: Run code up to yield + opt raise + dep -->> handler: Raise HTTPException + handler -->> client: HTTP error response + dep -->> dep: Raise other exception + end + dep ->> operation: Run dependency, e.g. DB session + opt raise + operation -->> handler: Raise HTTPException + handler -->> client: HTTP error response + operation -->> dep: Raise other exception + end + operation ->> client: Return response to client + Note over client,operation: Response is already sent, can't change it anymore + opt Tasks + operation -->> tasks: Send background tasks + end + opt Raise other exception + tasks -->> dep: Raise other exception + end + Note over dep: After yield + opt Handle other exception + dep -->> dep: Handle exception, can't change response. E.g. close DB session. + end +``` + +!!! info "情報" + **1つのレスポンス** だけがクライアントに送信されます。それはエラーレスポンスの一つかもしれませんし、*path operation*からのレスポンスかもしれません。 + + いずれかのレスポンスが送信された後、他のレスポンスを送信することはできません。 + +!!! tip "豆知識" + この図は`HTTPException`を示していますが、[カスタム例外ハンドラ](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}を作成することで、他の例外を発生させることもできます。そして、その例外は依存関係の終了コードではなく、そのカスタム例外ハンドラによって処理されます。 + + しかし例外ハンドラで処理されない例外を発生させた場合は、依存関係の終了コードで処理されます。 + +## コンテキストマネージャ + +### 「コンテキストマネージャ」とは + +「コンテキストマネージャ」とは、`with`文の中で使用できるPythonオブジェクトのことです。 + +例えば、ファイルを読み込むには`with`を使用することができます: + +```Python +with open("./somefile.txt") as f: + contents = f.read() + print(contents) +``` + +その後の`open("./somefile.txt")`は「コンテキストマネージャ」と呼ばれるオブジェクトを作成します。 + +`with`ブロックが終了すると、例外があったとしてもファイルを確かに閉じます。 + +`yield`を依存関係を作成すると、**FastAPI** は内部的にそれをコンテキストマネージャに変換し、他の関連ツールと組み合わせます。 + +### `yield`を持つ依存関係でのコンテキストマネージャの使用 + +!!! warning "注意" + これは多かれ少なかれ、「高度な」発想です。 + + **FastAPI** を使い始めたばかりの方は、とりあえずスキップした方がよいかもしれません。 + +Pythonでは、以下の2つのメソッドを持つクラスを作成する: `__enter__()`と`__exit__()`ことでコンテキストマネージャを作成することができます。 + +また、依存関数の中で`with`や`async with`文を使用することによって`yield`を持つ **FastAPI** の依存関係の中でそれらを使用することができます: + +```Python hl_lines="1 2 3 4 5 6 7 8 9 13" +{!../../../docs_src/dependencies/tutorial010.py!} +``` + +!!! tip "豆知識" + コンテキストマネージャを作成するもう一つの方法はwithです: + + * `@contextlib.contextmanager` または + * `@contextlib.asynccontextmanager` + + これらを使って、関数を単一の`yield`でデコレートすることができます。 + + これは **FastAPI** が内部的に`yield`を持つ依存関係のために使用しているものです。 + + しかし、FastAPIの依存関係にデコレータを使う必要はありません(そして使うべきではありません)。 + + FastAPIが内部的にやってくれます。 diff --git a/docs/ja/docs/tutorial/dependencies/index.md b/docs/ja/docs/tutorial/dependencies/index.md new file mode 100644 index 0000000000..ec563a16d7 --- /dev/null +++ b/docs/ja/docs/tutorial/dependencies/index.md @@ -0,0 +1,209 @@ +# 依存関係 - 最初のステップ + +** FastAPI** は非常に強力でありながら直感的な **依存性注入** システムを持っています。 + +それは非常にシンプルに使用できるように設計されており、開発者が他のコンポーネント **FastAPI** と統合するのが非常に簡単になるように設計されています。 + +## 「依存性注入」とは + +**「依存性注入」** とは、プログラミングにおいて、コード(この場合は、*path operation関数*)が動作したり使用したりするために必要なもの(「依存関係」)を宣言する方法があることを意味します: + +そして、そのシステム(この場合は、**FastAPI**)は、必要な依存関係をコードに提供するために必要なことは何でも行います(依存関係を「注入」します)。 + +これは以下のようなことが必要な時にとても便利です: + +* ロジックを共有している。(同じコードロジックを何度も繰り返している)。 +* データベース接続を共有する。 +* セキュリティ、認証、ロール要件などを強制する。 +* そのほかにも多くのこと... + +これらすべてを、コードの繰り返しを最小限に抑えながら行います。 + +## 最初のステップ + +非常にシンプルな例を見てみましょう。あまりにもシンプルなので、今のところはあまり参考にならないでしょう。 + +しかし、この方法では **依存性注入** システムがどのように機能するかに焦点を当てることができます。 + +### 依存関係の作成 + +まずは依存関係に注目してみましょう。 + +以下のように、*path operation関数*と同じパラメータを全て取ることができる関数にすぎません: + +```Python hl_lines="8 9" +{!../../../docs_src/dependencies/tutorial001.py!} +``` + +これだけです。 + +**2行**。 + +そして、それはすべての*path operation関数*が持っているのと同じ形と構造を持っています。 + +「デコレータ」を含まない(`@app.get("/some-path")`を含まない)*path operation関数*と考えることもできます。 + +そして何でも返すことができます。 + +この場合、この依存関係は以下を期待しています: + +* オプショナルのクエリパラメータ`q`は`str`です。 +* オプショナルのクエリパラメータ`skip`は`int`で、デフォルトは`0`です。 +* オプショナルのクエリパラメータ`limit`は`int`で、デフォルトは`100`です。 + +そして、これらの値を含む`dict`を返します。 + +### `Depends`のインポート + +```Python hl_lines="3" +{!../../../docs_src/dependencies/tutorial001.py!} +``` + +### "dependant"での依存関係の宣言 + +*path operation関数*のパラメータに`Body`や`Query`などを使用するのと同じように、新しいパラメータに`Depends`を使用することができます: + +```Python hl_lines="13 18" +{!../../../docs_src/dependencies/tutorial001.py!} +``` + +関数のパラメータに`Depends`を使用するのは`Body`や`Query`などと同じですが、`Depends`の動作は少し異なります。 + +`Depends`は1つのパラメータしか与えられません。 + +このパラメータは関数のようなものである必要があります。 + +そして、その関数は、*path operation関数*が行うのと同じ方法でパラメータを取ります。 + +!!! tip "豆知識" + 次の章では、関数以外の「もの」が依存関係として使用できるものを見ていきます。 + +新しいリクエストが到着するたびに、**FastAPI** が以下のような処理を行います: + +* 依存関係("dependable")関数を正しいパラメータで呼び出します。 +* 関数の結果を取得します。 +* *path operation関数*のパラメータにその結果を代入してください。 + +```mermaid +graph TB + +common_parameters(["common_parameters"]) +read_items["/items/"] +read_users["/users/"] + +common_parameters --> read_items +common_parameters --> read_users +``` + +この方法では、共有されるコードを一度書き、**FastAPI** が*path operations*のための呼び出しを行います。 + +!!! check "確認" + 特別なクラスを作成してどこかで **FastAPI** に渡して「登録」する必要はないことに注意してください。 + + `Depends`を渡すだけで、**FastAPI** が残りの処理をしてくれます。 + +## `async`にするかどうか + +依存関係は **FastAPI**(*path operation関数*と同じ)からも呼び出されるため、関数を定義する際にも同じルールが適用されます。 + +`async def`や通常の`def`を使用することができます。 + +また、通常の`def`*path operation関数*の中に`async def`を入れて依存関係を宣言したり、`async def`*path operation関数*の中に`def`を入れて依存関係を宣言したりすることなどができます。 + +それは重要ではありません。**FastAPI** は何をすべきかを知っています。 + +!!! note "備考" + わからない場合は、ドキュメントの[Async: *"In a hurry?"*](../../async.md){.internal-link target=_blank}の中の`async`と`await`についてのセクションを確認してください。 + +## OpenAPIとの統合 + +依存関係(およびサブ依存関係)のすべてのリクエスト宣言、検証、および要件は、同じOpenAPIスキーマに統合されます。 + +つまり、対話型ドキュメントにはこれらの依存関係から得られる全ての情報も含まれているということです: + + + +## 簡単な使い方 + +見てみると、*path*と*operation*が一致した時に*path operation関数*が宣言されていて、**FastAPI** が正しいパラメータで関数を呼び出してリクエストからデータを抽出する処理をしています。 + +実は、すべての(あるいはほとんどの)Webフレームワークは、このように動作します。 + +これらの関数を直接呼び出すことはありません。これらの関数はフレームワーク(この場合は、**FastAPI**)によって呼び出されます。 + +依存性注入システムでは、**FastAPI** に*path operation*もまた、*path operation関数*の前に実行されるべき他の何かに「依存」していることを伝えることができ、**FastAPI** がそれを実行し、結果を「注入」することを引き受けます。 + +他にも、「依存性注入」と同じような考えの一般的な用語があります: + +* リソース +* プロバイダ +* サービス +* インジェクタブル +* コンポーネント + +## **FastAPI** プラグイン + +統合や「プラグイン」は **依存性注入** システムを使って構築することができます。しかし、実際には、**「プラグイン」を作成する必要はありません**。依存関係を使用することで、無限の数の統合やインタラクションを宣言することができ、それが**path operation関数*で利用可能になるからです。 + +依存関係は非常にシンプルで直感的な方法で作成することができ、必要なPythonパッケージをインポートするだけで、*文字通り*数行のコードでAPI関数と統合することができます。 + +次の章では、リレーショナルデータベースやNoSQLデータベース、セキュリティなどについて、その例を見ていきます。 + +## **FastAPI** 互換性 + +依存性注入システムがシンプルなので、**FastAPI** は以下のようなものと互換性があります: + +* すべてのリレーショナルデータベース +* NoSQLデータベース +* 外部パッケージ +* 外部API +* 認証・認可システム +* API利用状況監視システム +* レスポンスデータ注入システム +* など。 + +## シンプルでパワフル + +階層依存性注入システムは、定義や使用方法が非常にシンプルであるにもかかわらず、非常に強力なものとなっています。 + +依存関係事態を定義する依存関係を定義することができます。 + +最終的には、依存関係の階層ツリーが構築され、**依存性注入**システムが、これらの依存関係(およびそのサブ依存関係)をすべて解決し、各ステップで結果を提供(注入)します。 + +例えば、4つのAPIエンドポイント(*path operations*)があるとします: + +* `/items/public/` +* `/items/private/` +* `/users/{user_id}/activate` +* `/items/pro/` + +そして、依存関係とサブ依存関係だけで、それぞれに異なるパーミッション要件を追加することができます: + +```mermaid +graph TB + +current_user(["current_user"]) +active_user(["active_user"]) +admin_user(["admin_user"]) +paying_user(["paying_user"]) + +public["/items/public/"] +private["/items/private/"] +activate_user["/users/{user_id}/activate"] +pro_items["/items/pro/"] + +current_user --> active_user +active_user --> admin_user +active_user --> paying_user + +current_user --> public +active_user --> private +admin_user --> activate_user +paying_user --> pro_items +``` + +## **OpenAPI** との統合 + +これら全ての依存関係は、要件を宣言すると同時に、*path operations*にパラメータやバリデーションを追加します。 + +**FastAPI** はそれをすべてOpenAPIスキーマに追加して、対話型のドキュメントシステムに表示されるようにします。 diff --git a/docs/ja/docs/tutorial/dependencies/sub-dependencies.md b/docs/ja/docs/tutorial/dependencies/sub-dependencies.md new file mode 100644 index 0000000000..8848ac79ea --- /dev/null +++ b/docs/ja/docs/tutorial/dependencies/sub-dependencies.md @@ -0,0 +1,86 @@ +# サブ依存関係 + +**サブ依存関係** を持つ依存関係を作成することができます。 + +それらは必要なだけ **深く** することができます。 + +**FastAPI** はそれらを解決してくれます。 + +### 最初の依存関係「依存可能なもの」 + +以下のような最初の依存関係(「依存可能なもの」)を作成することができます: + +```Python hl_lines="8 9" +{!../../../docs_src/dependencies/tutorial005.py!} +``` + +これはオプショナルのクエリパラメータ`q`を`str`として宣言し、それを返すだけです。 + +これは非常にシンプルです(あまり便利ではありません)が、サブ依存関係がどのように機能するかに焦点を当てるのに役立ちます。 + +### 第二の依存関係 「依存可能なもの」と「依存」 + +そして、別の依存関数(「依存可能なもの」)を作成して、同時にそれ自身の依存関係を宣言することができます(つまりそれ自身も「依存」です): + +```Python hl_lines="13" +{!../../../docs_src/dependencies/tutorial005.py!} +``` + +宣言されたパラメータに注目してみましょう: + +* この関数は依存関係(「依存可能なもの」)そのものであるにもかかわらず、別の依存関係を宣言しています(何か他のものに「依存」しています)。 + * これは`query_extractor`に依存しており、それが返す値をパラメータ`q`に代入します。 +* また、オプショナルの`last_query`クッキーを`str`として宣言します。 + * ユーザーがクエリ`q`を提供しなかった場合、クッキーに保存していた最後に使用したクエリを使用します。 + +### 依存関係の使用 + +以下のように依存関係を使用することができます: + +```Python hl_lines="21" +{!../../../docs_src/dependencies/tutorial005.py!} +``` + +!!! info "情報" + *path operation関数*の中で宣言している依存関係は`query_or_cookie_extractor`の1つだけであることに注意してください。 + + しかし、**FastAPI** は`query_extractor`を最初に解決し、その結果を`query_or_cookie_extractor`を呼び出す時に渡す必要があることを知っています。 + +```mermaid +graph TB + +query_extractor(["query_extractor"]) +query_or_cookie_extractor(["query_or_cookie_extractor"]) + +read_query["/items/"] + +query_extractor --> query_or_cookie_extractor --> read_query +``` + +## 同じ依存関係の複数回の使用 + +依存関係の1つが同じ*path operation*に対して複数回宣言されている場合、例えば、複数の依存関係が共通のサブ依存関係を持っている場合、**FastAPI** はリクエストごとに1回だけそのサブ依存関係を呼び出します。 + +そして、返された値を「キャッシュ」に保存し、同じリクエストに対して依存関係を何度も呼び出す代わりに、特定のリクエストでそれを必要とする全ての「依存関係」に渡すことになります。 + +高度なシナリオでは、「キャッシュされた」値を使うのではなく、同じリクエストの各ステップ(おそらく複数回)で依存関係を呼び出す必要があることがわかっている場合、`Depens`を使用する際に、`use_cache=False`というパラメータを設定することができます。 + +```Python hl_lines="1" +async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): + return {"fresh_value": fresh_value} +``` + +## まとめ + +ここで使われている派手な言葉は別にして、**依存性注入** システムは非常にシンプルです。 + +*path operation関数*と同じように見えるただの関数です。 + +しかし、それでも非常に強力で、任意の深くネストされた依存関係「グラフ」(ツリー)を宣言することができます。 + +!!! tip "豆知識" + これらの単純な例では、全てが役に立つとは言えないかもしれません。 + + しかし、**security** についての章で、それがどれほど有用であるかがわかるでしょう。 + + そして、あなたを救うコードの量もみることになるでしょう。 diff --git a/docs/ja/docs/tutorial/extra-models.md b/docs/ja/docs/tutorial/extra-models.md new file mode 100644 index 0000000000..aa2e5ffdcb --- /dev/null +++ b/docs/ja/docs/tutorial/extra-models.md @@ -0,0 +1,195 @@ +# モデル - より詳しく + +先ほどの例に続き、複数の関連モデルを持つことが一般的です。 + +これはユーザーモデルの場合は特にそうです。なぜなら: + +* **入力モデル** にはパスワードが必要です。 +* **出力モデル**はパスワードをもつべきではありません。 +* **データベースモデル**はおそらくハッシュ化されたパスワードが必要になるでしょう。 + +!!! danger "危険" + ユーザーの平文のパスワードは絶対に保存しないでください。常に認証に利用可能な「安全なハッシュ」を保存してください。 + + 知らない方は、[セキュリティの章](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}で「パスワードハッシュ」とは何かを学ぶことができます。 + +## 複数のモデル + +ここでは、パスワードフィールドをもつモデルがどのように見えるのか、また、どこで使われるのか、大まかなイメージを紹介します: + +```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" +{!../../../docs_src/extra_models/tutorial001.py!} +``` + +### `**user_in.dict()`について + +#### Pydanticの`.dict()` + +`user_in`は`UserIn`クラスのPydanticモデルです。 + +Pydanticモデルには、モデルのデータを含む`dict`を返す`.dict()`メソッドがあります。 + +そこで、以下のようなPydanticオブジェクト`user_in`を作成すると: + +```Python +user_in = UserIn(username="john", password="secret", email="john.doe@example.com") +``` + +そして呼び出すと: + +```Python +user_dict = user_in.dict() +``` + +これで変数`user_dict`のデータを持つ`dict`ができました。(これはPydanticモデルのオブジェクトの代わりに`dict`です)。 + +そして呼び出すと: + +```Python +print(user_dict) +``` + +以下のようなPythonの`dict`を得ることができます: + +```Python +{ + 'username': 'john', + 'password': 'secret', + 'email': 'john.doe@example.com', + 'full_name': None, +} +``` + +#### `dict`の展開 + +`user_dict`のような`dict`を受け取り、それを`**user_dict`を持つ関数(またはクラス)に渡すと、Pythonはそれを「展開」します。これは`user_dict`のキーと値を直接キー・バリューの引数として渡します。 + +そこで上述の`user_dict`の続きを以下のように書くと: + +```Python +UserInDB(**user_dict) +``` + +以下と同等の結果になります: + +```Python +UserInDB( + username="john", + password="secret", + email="john.doe@example.com", + full_name=None, +) +``` + +もっと正確に言えば、`user_dict`を将来的にどんな内容であっても直接使用することになります: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], +) +``` + +#### 別のモデルからつくるPydanticモデル + +上述の例では`user_in.dict()`から`user_dict`をこのコードのように取得していますが: + +```Python +user_dict = user_in.dict() +UserInDB(**user_dict) +``` + +これは以下と同等です: + +```Python +UserInDB(**user_in.dict()) +``` + +...なぜなら`user_in.dict()`は`dict`であり、`**`を付与して`UserInDB`を渡してPythonに「展開」させているからです。 + +そこで、別のPydanticモデルのデータからPydanticモデルを取得します。 + +#### `dict`の展開と追加引数 + +そして、追加のキーワード引数`hashed_password=hashed_password`を以下のように追加すると: + +```Python +UserInDB(**user_in.dict(), hashed_password=hashed_password) +``` + +...以下のようになります: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], + hashed_password = hashed_password, +) +``` + +!!! warning "注意" + サポートしている追加機能は、データの可能な流れをデモするだけであり、もちろん本当のセキュリティを提供しているわけではありません。 + +## 重複の削減 + +コードの重複を減らすことは、**FastAPI**の中核的なアイデアの1つです。 + +コードの重複が増えると、バグやセキュリティの問題、コードの非同期化問題(ある場所では更新しても他の場所では更新されない場合)などが発生する可能性が高くなります。 + +そして、これらのモデルは全てのデータを共有し、属性名や型を重複させています。 + +もっと良い方法があります。 + +他のモデルのベースとなる`UserBase`モデルを宣言することができます。そして、そのモデルの属性(型宣言、検証など)を継承するサブクラスを作ることができます。 + +データの変換、検証、文書化などはすべて通常通りに動作します。 + +このようにして、モデル間の違いだけを宣言することができます: + +```Python hl_lines="9 15 16 19 20 23 24" +{!../../../docs_src/extra_models/tutorial002.py!} +``` + +## `Union`または`anyOf` + +レスポンスを2つの型の`Union`として宣言することができます。 + +OpenAPIでは`anyOf`で定義されます。 + +そのためには、標準的なPythonの型ヒント`typing.Union`を使用します: + +```Python hl_lines="1 14 15 18 19 20 33" +{!../../../docs_src/extra_models/tutorial003.py!} +``` + +## モデルのリスト + +同じように、オブジェクトのリストのレスポンスを宣言することができます。 + +そのためには、標準のPythonの`typing.List`を使用する: + +```Python hl_lines="1 20" +{!../../../docs_src/extra_models/tutorial004.py!} +``` + +## 任意の`dict`を持つレスポンス + +また、Pydanticモデルを使用せずに、キーと値の型だけを定義した任意の`dict`を使ってレスポンスを宣言することもできます。 + +これは、有効なフィールド・属性名(Pydanticモデルに必要なもの)を事前に知らない場合に便利です。 + +この場合、`typing.Dict`を使用することができます: + +```Python hl_lines="1 8" +{!../../../docs_src/extra_models/tutorial005.py!} +``` + +## まとめ + +複数のPydanticモデルを使用し、ケースごとに自由に継承します。 + +エンティティが異なる「状態」を持たなければならない場合は、エンティティごとに単一のデータモデルを持つ必要はありません。`password` や `password_hash` やパスワードなしなどのいくつかの「状態」をもつユーザー「エンティティ」の場合の様にすれば良いです。 diff --git a/docs/ja/docs/tutorial/handling-errors.md b/docs/ja/docs/tutorial/handling-errors.md new file mode 100644 index 0000000000..ec36e9880d --- /dev/null +++ b/docs/ja/docs/tutorial/handling-errors.md @@ -0,0 +1,265 @@ +# エラーハンドリング + +APIを使用しているクライアントにエラーを通知する必要がある状況はたくさんあります。 + +このクライアントは、フロントエンドを持つブラウザ、誰かのコード、IoTデバイスなどが考えられます。 + +クライアントに以下のようなことを伝える必要があるかもしれません: + +* クライアントにはその操作のための十分な権限がありません。 +* クライアントはそのリソースにアクセスできません。 +* クライアントがアクセスしようとしていた項目が存在しません。 +* など + +これらの場合、通常は **400**(400から499)の範囲内の **HTTPステータスコード** を返すことになります。 + +これは200のHTTPステータスコード(200から299)に似ています。これらの「200」ステータスコードは、何らかの形でリクエスト「成功」であったことを意味します。 + +400の範囲にあるステータスコードは、クライアントからのエラーがあったことを意味します。 + +**"404 Not Found"** のエラー(およびジョーク)を覚えていますか? + +## `HTTPException`の使用 + +HTTPレスポンスをエラーでクライアントに返すには、`HTTPException`を使用します。 + +### `HTTPException`のインポート + +```Python hl_lines="1" +{!../../../docs_src/handling_errors/tutorial001.py!} +``` + +### コード内での`HTTPException`の発生 + +`HTTPException`は通常のPythonの例外であり、APIに関連するデータを追加したものです。 + +Pythonの例外なので、`return`ではなく、`raise`です。 + +これはまた、*path operation関数*の内部で呼び出しているユーティリティ関数の内部から`HTTPException`を発生させた場合、*path operation関数*の残りのコードは実行されず、そのリクエストを直ちに終了させ、`HTTPException`からのHTTPエラーをクライアントに送信することを意味します。 + +値を返す`return`よりも例外を発生させることの利点は、「依存関係とセキュリティ」のセクションでより明確になります。 + +この例では、クライアントが存在しないIDでアイテムを要求した場合、`404`のステータスコードを持つ例外を発生させます: + +```Python hl_lines="11" +{!../../../docs_src/handling_errors/tutorial001.py!} +``` + +### レスポンス結果 + +クライアントが`http://example.com/items/foo`(`item_id` `"foo"`)をリクエストすると、HTTPステータスコードが200で、以下のJSONレスポンスが返されます: + +```JSON +{ + "item": "The Foo Wrestlers" +} +``` + +しかし、クライアントが`http://example.com/items/bar`(存在しない`item_id` `"bar"`)をリクエストした場合、HTTPステータスコード404("not found"エラー)と以下のJSONレスポンスが返されます: + +```JSON +{ + "detail": "Item not found" +} +``` + +!!! tip "豆知識" + `HTTPException`を発生させる際には、`str`だけでなく、JSONに変換できる任意の値を`detail`パラメータとして渡すことができます。 + + `dist`や`list`などを渡すことができます。 + + これらは **FastAPI** によって自動的に処理され、JSONに変換されます。 + +## カスタムヘッダーの追加 + +例えば、いくつかのタイプのセキュリティのために、HTTPエラーにカスタムヘッダを追加できると便利な状況がいくつかあります。 + +おそらくコードの中で直接使用する必要はないでしょう。 + +しかし、高度なシナリオのために必要な場合には、カスタムヘッダーを追加することができます: + +```Python hl_lines="14" +{!../../../docs_src/handling_errors/tutorial002.py!} +``` + +## カスタム例外ハンドラのインストール + +カスタム例外ハンドラはStarletteと同じ例外ユーティリティを使用して追加することができます。 + +あなた(または使用しているライブラリ)が`raise`するかもしれないカスタム例外`UnicornException`があるとしましょう。 + +そして、この例外をFastAPIでグローバルに処理したいと思います。 + +カスタム例外ハンドラを`@app.exception_handler()`で追加することができます: + +```Python hl_lines="5 6 7 13 14 15 16 17 18 24" +{!../../../docs_src/handling_errors/tutorial003.py!} +``` + +ここで、`/unicorns/yolo`をリクエストすると、*path operation*は`UnicornException`を`raise`します。 + +しかし、これは`unicorn_exception_handler`で処理されます。 + +そのため、HTTPステータスコードが`418`で、JSONの内容が以下のような明確なエラーを受け取ることになります: + +```JSON +{"message": "Oops! yolo did something. There goes a rainbow..."} +``` + +!!! note "技術詳細" + また、`from starlette.requests import Request`と`from starlette.responses import JSONResponse`を使用することもできます。 + + **FastAPI** は開発者の利便性を考慮して、`fastapi.responses`と同じ`starlette.responses`を提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接提供されます。これは`Request`と同じです。 + +## デフォルトの例外ハンドラのオーバーライド + +**FastAPI** にはいくつかのデフォルトの例外ハンドラがあります。 + +これらのハンドラは、`HTTPException`を`raise`させた場合や、リクエストに無効なデータが含まれている場合にデフォルトのJSONレスポンスを返す役割を担っています。 + +これらの例外ハンドラを独自のものでオーバーライドすることができます。 + +### リクエスト検証の例外のオーバーライド + +リクエストに無効なデータが含まれている場合、**FastAPI** は内部的に`RequestValidationError`を発生させます。 + +また、そのためのデフォルトの例外ハンドラも含まれています。 + +これをオーバーライドするには`RequestValidationError`をインポートして`@app.exception_handler(RequestValidationError)`と一緒に使用して例外ハンドラをデコレートします。 + +この例外ハンドラは`Requset`と例外を受け取ります。 + +```Python hl_lines="2 14 15 16" +{!../../../docs_src/handling_errors/tutorial004.py!} +``` + +これで、`/items/foo`にアクセスすると、デフォルトのJSONエラーの代わりに以下が返されます: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +以下のようなテキスト版を取得します: + +``` +1 validation error +path -> item_id + value is not a valid integer (type=type_error.integer) +``` + +#### `RequestValidationError`と`ValidationError` + +!!! warning "注意" + これらは今のあなたにとって重要でない場合は省略しても良い技術的な詳細です。 + +`RequestValidationError`はPydanticの`ValidationError`のサブクラスです。 + +**FastAPI** は`response_model`でPydanticモデルを使用していて、データにエラーがあった場合、ログにエラーが表示されるようにこれを使用しています。 + +しかし、クライアントやユーザーはそれを見ることはありません。その代わりに、クライアントはHTTPステータスコード`500`の「Internal Server Error」を受け取ります。 + +*レスポンス*やコードのどこか(クライアントの*リクエスト*ではなく)にPydanticの`ValidationError`がある場合、それは実際にはコードのバグなのでこのようにすべきです。 + +また、あなたがそれを修正している間は、セキュリティの脆弱性が露呈する場合があるため、クライアントやユーザーがエラーに関する内部情報にアクセスできないようにしてください。 + +### エラーハンドラ`HTTPException`のオーバーライド + +同様に、`HTTPException`ハンドラをオーバーライドすることもできます。 + +例えば、これらのエラーに対しては、JSONではなくプレーンテキストを返すようにすることができます: + +```Python hl_lines="3 4 9 10 11 22" +{!../../../docs_src/handling_errors/tutorial004.py!} +``` + +!!! note "技術詳細" + また、`from starlette.responses import PlainTextResponse`を使用することもできます。 + + **FastAPI** は開発者の利便性を考慮して、`fastapi.responses`と同じ`starlette.responses`を提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接提供されます。 + +### `RequestValidationError`のボディの使用 + +`RequestValidationError`には無効なデータを含む`body`が含まれています。 + +アプリ開発中に本体のログを取ってデバッグしたり、ユーザーに返したりなどに使用することができます。 + +```Python hl_lines="14" +{!../../../docs_src/handling_errors/tutorial005.py!} +``` + +ここで、以下のような無効な項目を送信してみてください: + +```JSON +{ + "title": "towel", + "size": "XL" +} +``` + +受信したボディを含むデータが無効であることを示すレスポンスが表示されます: + +```JSON hl_lines="12 13 14 15" +{ + "detail": [ + { + "loc": [ + "body", + "size" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ], + "body": { + "title": "towel", + "size": "XL" + } +} +``` + +#### FastAPIの`HTTPException`とStarletteの`HTTPException` + +**FastAPI**は独自の`HTTPException`を持っています。 + +また、 **FastAPI**のエラークラス`HTTPException`はStarletteのエラークラス`HTTPException`を継承しています。 + +唯一の違いは、**FastAPI** の`HTTPException`はレスポンスに含まれるヘッダを追加できることです。 + +これはOAuth 2.0といくつかのセキュリティユーティリティのために内部的に必要とされ、使用されています。 + +そのため、コード内では通常通り **FastAPI** の`HTTPException`を発生させ続けることができます。 + +しかし、例外ハンドラを登録する際には、Starletteの`HTTPException`を登録しておく必要があります。 + +これにより、Starletteの内部コードやStarletteの拡張機能やプラグインの一部が`HTTPException`を発生させた場合、ハンドラがそれをキャッチして処理することができるようになります。 + +以下の例では、同じコード内で両方の`HTTPException`を使用できるようにするために、Starletteの例外の名前を`StarletteHTTPException`に変更しています: + +```Python +from starlette.exceptions import HTTPException as StarletteHTTPException +``` + +### **FastAPI** の例外ハンドラの再利用 + +また、何らかの方法で例外を使用することもできますが、**FastAPI** から同じデフォルトの例外ハンドラを使用することもできます。 + +デフォルトの例外ハンドラを`fastapi.exception_handlers`からインポートして再利用することができます: + +```Python hl_lines="2 3 4 5 15 21" +{!../../../docs_src/handling_errors/tutorial006.py!} +``` + +この例では、非常に表現力のあるメッセージでエラーを`print`しています。 + +しかし、例外を使用して、デフォルトの例外ハンドラを再利用することができるということが理解できます。 diff --git a/docs/ja/docs/tutorial/index.md b/docs/ja/docs/tutorial/index.md index a2dd59c9b0..856cde44b7 100644 --- a/docs/ja/docs/tutorial/index.md +++ b/docs/ja/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# チュートリアル - ユーザーガイド - はじめに +# チュートリアル - ユーザーガイド このチュートリアルは**FastAPI**のほぼすべての機能の使い方を段階的に紹介します。 diff --git a/docs/ja/docs/tutorial/path-params-numeric-validations.md b/docs/ja/docs/tutorial/path-params-numeric-validations.md new file mode 100644 index 0000000000..551aeabb3a --- /dev/null +++ b/docs/ja/docs/tutorial/path-params-numeric-validations.md @@ -0,0 +1,122 @@ +# パスパラメータと数値の検証 + +クエリパラメータに対して`Query`でより多くのバリデーションとメタデータを宣言できるのと同じように、パスパラメータに対しても`Path`で同じ種類のバリデーションとメタデータを宣言することができます。 + +## Pathのインポート + +まず初めに、`fastapi`から`Path`をインポートします: + +```Python hl_lines="1" +{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} +``` + +## メタデータの宣言 + +パラメータは`Query`と同じものを宣言することができます。 + +例えば、パスパラメータ`item_id`に対して`title`のメタデータを宣言するには以下のようにします: + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} +``` + +!!! note "備考" + パスの一部でなければならないので、パスパラメータは常に必須です。 + + そのため、`...`を使用して必須と示す必要があります。 + + それでも、`None`で宣言しても、デフォルト値を設定しても、何の影響もなく、常に必要とされていることに変わりはありません。 + +## 必要に応じてパラメータを並び替える + +クエリパラメータ`q`を必須の`str`として宣言したいとしましょう。 + +また、このパラメータには何も宣言する必要がないので、`Query`を使う必要はありません。 + +しかし、パスパラメータ`item_id`のために`Path`を使用する必要があります。 + +Pythonは「デフォルト」を持たない値の前に「デフォルト」を持つ値を置くことができません。 + +しかし、それらを並び替えることができ、デフォルト値を持たない値(クエリパラメータ`q`)を最初に持つことができます。 + +**FastAPI**では関係ありません。パラメータは名前、型、デフォルトの宣言(`Query`、`Path`など)で検出され、順番は気にしません。 + +そのため、以下のように関数を宣言することができます: + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} +``` + +## 必要に応じてパラメータを並び替えるトリック + +クエリパラメータ`q`を`Query`やデフォルト値なしで宣言し、パスパラメータ`item_id`を`Path`を用いて宣言し、それらを別の順番に並びたい場合、Pythonには少し特殊な構文が用意されています。 + +関数の最初のパラメータとして`*`を渡します。 + +Pythonはその`*`で何かをすることはありませんが、それ以降のすべてのパラメータがキーワード引数(キーと値のペア)として呼ばれるべきものであると知っているでしょう。それはkwargsとしても知られています。たとえデフォルト値がなくても。 + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial003.py!} +``` + +## 数値の検証: 以上 + +`Query`と`Path`(、そして後述する他のもの)を用いて、文字列の制約を宣言することができますが、数値の制約も同様に宣言できます。 + +ここで、`ge=1`の場合、`item_id`は`1`「より大きい`g`か、同じ`e`」整数でなれけばなりません。 + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial004.py!} +``` + +## 数値の検証: より大きいと小なりイコール + +以下も同様です: + +* `gt`: より大きい(`g`reater `t`han) +* `le`: 小なりイコール(`l`ess than or `e`qual) + +```Python hl_lines="9" +{!../../../docs_src/path_params_numeric_validations/tutorial005.py!} +``` + +## 数値の検証: 浮動小数点、 大なり小なり + +数値のバリデーションは`float`の値に対しても有効です。 + +ここで重要になってくるのはgtだけでなくgeも宣言できることです。これと同様に、例えば、値が`1`より小さくても`0`より大きくなければならないことを要求することができます。 + +したがって、`0.5`は有効な値ですが、`0.0`や`0`はそうではありません。 + +これはltも同じです。 + +```Python hl_lines="11" +{!../../../docs_src/path_params_numeric_validations/tutorial006.py!} +``` + +## まとめ + +`Query`と`Path`(そしてまだ見たことない他のもの)では、[クエリパラメータと文字列の検証](query-params-str-validations.md){.internal-link target=_blank}と同じようにメタデータと文字列の検証を宣言することができます。 + +また、数値のバリデーションを宣言することもできます: + +* `gt`: より大きい(`g`reater `t`han) +* `ge`: 以上(`g`reater than or `e`qual) +* `lt`: より小さい(`l`ess `t`han) +* `le`: 以下(`l`ess than or `e`qual) + +!!! info "情報" + `Query`、`Path`などは後に共通の`Param`クラスのサブクラスを見ることになります。(使う必要はありません) + + そして、それらすべては、これまで見てきた追加のバリデーションとメタデータと同じパラメータを共有しています。 + +!!! note "技術詳細" + `fastapi`から`Query`、`Path`などをインポートすると、これらは実際には関数です。 + + 呼び出されると、同じ名前のクラスのインスタンスを返します。 + + そのため、関数である`Query`をインポートし、それを呼び出すと、`Query`という名前のクラスのインスタンスが返されます。 + + これらの関数は(クラスを直接使うのではなく)エディタが型についてエラーとしないようにするために存在します。 + + この方法によって、これらのエラーを無視するための設定を追加することなく、通常のエディタやコーディングツールを使用することができます。 diff --git a/docs/ja/docs/tutorial/response-model.md b/docs/ja/docs/tutorial/response-model.md new file mode 100644 index 0000000000..749b330610 --- /dev/null +++ b/docs/ja/docs/tutorial/response-model.md @@ -0,0 +1,208 @@ +# レスポンスモデル + +*path operations* のいずれにおいても、`response_model`パラメータを使用して、レスポンスのモデルを宣言することができます: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* など。 + +```Python hl_lines="17" +{!../../../docs_src/response_model/tutorial001.py!} +``` + +!!! note "備考" + `response_model`は「デコレータ」メソッド(`get`、`post`など)のパラメータであることに注意してください。すべてのパラメータやボディのように、*path operation関数* のパラメータではありません。 + +Pydanticモデルの属性に対して宣言するのと同じ型を受け取るので、Pydanticモデルになることもできますが、例えば、`List[Item]`のようなPydanticモデルの`list`になることもできます。 + +FastAPIは`response_model`を使って以下のことをします: + +* 出力データを型宣言に変換します。 +* データを検証します。 +* OpenAPIの *path operation* で、レスポンス用のJSON Schemaを追加します。 +* 自動ドキュメントシステムで使用されます。 + +しかし、最も重要なのは: + +* 出力データをモデルのデータに限定します。これがどのように重要なのか以下で見ていきましょう。 + +!!! note "技術詳細" + レスポンスモデルは、関数の戻り値のアノテーションではなく、このパラメータで宣言されています。なぜなら、パス関数は実際にはそのレスポンスモデルを返すのではなく、`dict`やデータベースオブジェクト、あるいは他のモデルを返し、`response_model`を使用してフィールドの制限やシリアライズを行うからです。 + +## 同じ入力データの返却 + +ここでは`UserIn`モデルを宣言しています。それには平文のパスワードが含まれています: + +```Python hl_lines="9 11" +{!../../../docs_src/response_model/tutorial002.py!} +``` + +そして、このモデルを使用して入力を宣言し、同じモデルを使って出力を宣言しています: + +```Python hl_lines="17 18" +{!../../../docs_src/response_model/tutorial002.py!} +``` + +これで、ブラウザがパスワードを使ってユーザーを作成する際に、APIがレスポンスで同じパスワードを返すようになりました。 + +この場合、ユーザー自身がパスワードを送信しているので問題ないかもしれません。 + +しかし、同じモデルを別の*path operation*に使用すると、すべてのクライアントにユーザーのパスワードを送信してしまうことになります。 + +!!! danger "危険" + ユーザーの平文のパスワードを保存したり、レスポンスで送信したりすることは絶対にしないでください。 + +## 出力モデルの追加 + +代わりに、平文のパスワードを持つ入力モデルと、パスワードを持たない出力モデルを作成することができます: + +```Python hl_lines="9 11 16" +{!../../../docs_src/response_model/tutorial003.py!} +``` + +ここでは、*path operation関数*がパスワードを含む同じ入力ユーザーを返しているにもかかわらず: + +```Python hl_lines="24" +{!../../../docs_src/response_model/tutorial003.py!} +``` + +...`response_model`を`UserOut`と宣言したことで、パスワードが含まれていません: + +```Python hl_lines="22" +{!../../../docs_src/response_model/tutorial003.py!} +``` + +そのため、**FastAPI** は出力モデルで宣言されていない全てのデータをフィルタリングしてくれます(Pydanticを使用)。 + +## ドキュメントを見る + +自動ドキュメントを見ると、入力モデルと出力モデルがそれぞれ独自のJSON Schemaを持っていることが確認できます。 + + + +そして、両方のモデルは、対話型のAPIドキュメントに使用されます: + + + +## レスポンスモデルのエンコーディングパラメータ + +レスポンスモデルにはデフォルト値を設定することができます: + +```Python hl_lines="11 13 14" +{!../../../docs_src/response_model/tutorial004.py!} +``` + +* `description: str = None`は`None`がデフォルト値です。 +* `tax: float = 10.5`は`10.5`がデフォルト値です。 +* `tags: List[str] = []` は空のリスト(`[]`)がデフォルト値です。 + +しかし、実際に保存されていない場合には結果からそれらを省略した方が良いかもしれません。 + +例えば、NoSQLデータベースに多くのオプション属性を持つモデルがあるが、デフォルト値でいっぱいの非常に長いJSONレスポンスを送信したくない場合です。 + +### `response_model_exclude_unset`パラメータの使用 + +*path operation デコレータ*に`response_model_exclude_unset=True`パラメータを設定することができます: + +```Python hl_lines="24" +{!../../../docs_src/response_model/tutorial004.py!} +``` + +そして、これらのデフォルト値はレスポンスに含まれず、実際に設定された値のみが含まれます。 + +そのため、*path operation*にID`foo`が設定されたitemのリクエストを送ると、レスポンスは以下のようになります(デフォルト値を含まない): + +```JSON +{ + "name": "Foo", + "price": 50.2 +} +``` + +!!! info "情報" + FastAPIはこれをするために、Pydanticモデルの`.dict()`をその`exclude_unset`パラメータで使用しています。 + +!!! info "情報" + 以下も使用することができます: + + * `response_model_exclude_defaults=True` + * `response_model_exclude_none=True` + + `exclude_defaults`と`exclude_none`については、Pydanticのドキュメントで説明されている通りです。 + +#### デフォルト値を持つフィールドの値を持つデータ + +しかし、ID`bar`のitemのように、デフォルト値が設定されているモデルのフィールドに値が設定されている場合: + +```Python hl_lines="3 5" +{ + "name": "Bar", + "description": "The bartenders", + "price": 62, + "tax": 20.2 +} +``` + +それらはレスポンスに含まれます。 + +#### デフォルト値と同じ値を持つデータ + +ID`baz`のitemのようにデフォルト値と同じ値を持つデータの場合: + +```Python hl_lines="3 5 6" +{ + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [] +} +``` + +FastAPIは十分に賢いので(実際には、Pydanticが十分に賢い)`description`や`tax`、`tags`はデフォルト値と同じ値を持っているにもかかわらず、明示的に設定されていることを理解しています。(デフォルトから取得するのではなく) + +そのため、それらはJSONレスポンスに含まれることになります。 + +!!! tip "豆知識" + デフォルト値は`None`だけでなく、なんでも良いことに注意してください。 + 例えば、リスト(`[]`)や`10.5`の`float`などです。 + +### `response_model_include`と`response_model_exclude` + +*path operationデコレータ*として`response_model_include`と`response_model_exclude`も使用することができます。 + +属性名を持つ`str`の`set`を受け取り、含める(残りを省略する)か、除外(残りを含む)します。 + +これは、Pydanticモデルが1つしかなく、出力からいくつかのデータを削除したい場合のクイックショートカットとして使用することができます。 + +!!! tip "豆知識" + それでも、これらのパラメータではなく、複数のクラスを使用して、上記のようなアイデアを使うことをおすすめします。 + + これは`response_model_include`や`response_mode_exclude`を使用していくつかの属性を省略しても、アプリケーションのOpenAPI(とドキュメント)で生成されたJSON Schemaが完全なモデルになるからです。 + + 同様に動作する`response_model_by_alias`にも当てはまります。 + +```Python hl_lines="31 37" +{!../../../docs_src/response_model/tutorial005.py!} +``` + +!!! tip "豆知識" + `{"name", "description"}`の構文はこれら2つの値をもつ`set`を作成します。 + + これは`set(["name", "description"])`と同等です。 + +#### `set`の代わりに`list`を使用する + +もし`set`を使用することを忘れて、代わりに`list`や`tuple`を使用しても、FastAPIはそれを`set`に変換して正しく動作します: + +```Python hl_lines="31 37" +{!../../../docs_src/response_model/tutorial006.py!} +``` + +## まとめ + +*path operationデコレータの*`response_model`パラメータを使用して、レスポンスモデルを定義し、特にプライベートデータがフィルタリングされていることを保証します。 + +明示的に設定された値のみを返すには、`response_model_exclude_unset`を使用します。 diff --git a/docs/ja/docs/tutorial/response-status-code.md b/docs/ja/docs/tutorial/response-status-code.md new file mode 100644 index 0000000000..ead2adddaa --- /dev/null +++ b/docs/ja/docs/tutorial/response-status-code.md @@ -0,0 +1,89 @@ +# レスポンスステータスコード + +レスポンスモデルを指定するのと同じ方法で、レスポンスに使用されるHTTPステータスコードを以下の*path operations*のいずれかの`status_code`パラメータで宣言することもできます。 + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* など。 + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +!!! note "備考" + `status_code`は「デコレータ」メソッド(`get`、`post`など)のパラメータであることに注意してください。すべてのパラメータやボディのように、*path operation関数*のものではありません。 + +`status_code`パラメータはHTTPステータスコードを含む数値を受け取ります。 + +!!! info "情報" + `status_code`は代わりに、Pythonの`http.HTTPStatus`のように、`IntEnum`を受け取ることもできます。 + +これは: + +* レスポンスでステータスコードを返します。 +* OpenAPIスキーマ(およびユーザーインターフェース)に以下のように文書化します: + + + +!!! note "備考" + いくつかのレスポンスコード(次のセクションを参照)は、レスポンスにボディがないことを示しています。 + + FastAPIはこれを知っていて、レスポンスボディがないというOpenAPIドキュメントを生成します。 + +## HTTPステータスコードについて + +!!! note "備考" + すでにHTTPステータスコードが何であるかを知っている場合は、次のセクションにスキップしてください。 + +HTTPでは、レスポンスの一部として3桁の数字のステータスコードを送信します。 + +これらのステータスコードは、それらを認識するために関連付けられた名前を持っていますが、重要な部分は番号です。 + +つまり: + +* `100`以上は「情報」のためのものです。。直接使うことはほとんどありません。これらのステータスコードを持つレスポンスはボディを持つことができません。 +* **`200`** 以上は「成功」のレスポンスのためのものです。これらは最も利用するであろうものです。 + * `200`はデフォルトのステータスコードで、すべてが「OK」であったことを意味します。 + * 別の例としては、`201`(Created)があります。これはデータベースに新しいレコードを作成した後によく使用されます。 + * 特殊なケースとして、`204`(No Content)があります。このレスポンスはクライアントに返すコンテンツがない場合に使用されます。そしてこのレスポンスはボディを持つことはできません。 +* **`300`** 以上は「リダイレクト」のためのものです。これらのステータスコードを持つレスポンスは`304`(Not Modified)を除き、ボディを持つことも持たないこともできます。 +* **`400`** 以上は「クライアントエラー」のレスポンスのためのものです。これらは、おそらく最も多用するであろう2番目のタイプです。 + * 例えば、`404`は「Not Found」レスポンスです。 + * クライアントからの一般的なエラーについては、`400`を使用することができます。 +* `500`以上はサーバーエラーのためのものです。これらを直接使うことはほとんどありません。アプリケーションコードやサーバーのどこかで何か問題が発生した場合、これらのステータスコードのいずれかが自動的に返されます。 + +!!! tip "豆知識" + それぞれのステータスコードとどのコードが何のためのコードなのかについて詳細はMDN HTTP レスポンスステータスコードについてのドキュメントを参照してください。 + +## 名前を覚えるための近道 + +先ほどの例をもう一度見てみましょう: + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +`201`は「作成完了」のためのステータスコードです。 + +しかし、それぞれのコードの意味を暗記する必要はありません。 + +`fastapi.status`の便利な変数を利用することができます。 + +```Python hl_lines="1 6" +{!../../../docs_src/response_status_code/tutorial002.py!} +``` + +それらは便利です。それらは同じ番号を保持しており、その方法ではエディタの自動補完を使用してそれらを見つけることができます。 + + + +!!! note "技術詳細" + また、`from starlette import status`を使うこともできます。 + + **FastAPI** は、`開発者の利便性を考慮して、fastapi.status`と同じ`starlette.status`を提供しています。しかし、これはStarletteから直接提供されています。 + +## デフォルトの変更 + +後に、[高度なユーザーガイド](../advanced/response-change-status-code.md){.internal-link target=_blank}で、ここで宣言しているデフォルトとは異なるステータスコードを返す方法を見ていきます。 diff --git a/docs/ja/docs/tutorial/schema-extra-example.md b/docs/ja/docs/tutorial/schema-extra-example.md new file mode 100644 index 0000000000..3102a49362 --- /dev/null +++ b/docs/ja/docs/tutorial/schema-extra-example.md @@ -0,0 +1,58 @@ +# スキーマの追加 - 例 + +JSON Schemaに追加する情報を定義することができます。 + +一般的なユースケースはこのドキュメントで示されているように`example`を追加することです。 + +JSON Schemaの追加情報を宣言する方法はいくつかあります。 + +## Pydanticの`schema_extra` + +Pydanticのドキュメント: スキーマのカスタマイズで説明されているように、`Config`と`schema_extra`を使ってPydanticモデルの例を宣言することができます: + +```Python hl_lines="15 16 17 18 19 20 21 22 23" +{!../../../docs_src/schema_extra_example/tutorial001.py!} +``` + +その追加情報はそのまま出力され、JSON Schemaに追加されます。 + +## `Field`の追加引数 + +後述する`Field`、`Path`、`Query`、`Body`などでは、任意の引数を関数に渡すことでJSON Schemaの追加情報を宣言することもできます: + +```Python hl_lines="4 10 11 12 13" +{!../../../docs_src/schema_extra_example/tutorial002.py!} +``` + +!!! warning "注意" + これらの追加引数が渡されても、文書化のためのバリデーションは追加されず、注釈だけが追加されることを覚えておいてください。 + +## `Body`の追加引数 + +追加情報を`Field`に渡すのと同じように、`Path`、`Query`、`Body`などでも同じことができます。 + +例えば、`Body`にボディリクエストの`example`を渡すことができます: + +```Python hl_lines="21 22 23 24 25 26" +{!../../../docs_src/schema_extra_example/tutorial003.py!} +``` + +## ドキュメントのUIの例 + +上記のいずれの方法でも、`/docs`の中では以下のようになります: + + + +## 技術詳細 + +`example` と `examples`について... + +JSON Schemaの最新バージョンでは`examples`というフィールドを定義していますが、OpenAPIは`examples`を持たない古いバージョンのJSON Schemaをベースにしています。 + +そのため、OpenAPIでは同じ目的のために`example`を独自に定義しており(`examples`ではなく`example`として)、それがdocs UI(Swagger UIを使用)で使用されています。 + +つまり、`example`はJSON Schemaの一部ではありませんが、OpenAPIの一部であり、それがdocs UIで使用されることになります。 + +## その他の情報 + +同じように、フロントエンドのユーザーインターフェースなどをカスタマイズするために、各モデルのJSON Schemaに追加される独自の追加情報を追加することができます。 diff --git a/docs/ja/docs/tutorial/security/oauth2-jwt.md b/docs/ja/docs/tutorial/security/oauth2-jwt.md index 348ffda016..d5b179aa05 100644 --- a/docs/ja/docs/tutorial/security/oauth2-jwt.md +++ b/docs/ja/docs/tutorial/security/oauth2-jwt.md @@ -167,7 +167,7 @@ JWTトークンの署名に使用するアルゴリズム`"HS256"`を指定し JWTアクセストークンを作成し、それを返します。 -```Python hl_lines="115-128" +```Python hl_lines="115-130" {!../../../docs_src/security/tutorial004.py!} ``` diff --git a/docs/ja/docs/tutorial/testing.md b/docs/ja/docs/tutorial/testing.md index 56f5cabac8..037e9628fb 100644 --- a/docs/ja/docs/tutorial/testing.md +++ b/docs/ja/docs/tutorial/testing.md @@ -74,18 +74,18 @@ これらの *path operation* には `X-Token` ヘッダーが必要です。 -=== "Python 3.6 and above" - - ```Python - {!> ../../../docs_src/app_testing/app_b/main.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python {!> ../../../docs_src/app_testing/app_b_py310/main.py!} ``` +=== "Python 3.6+" + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + ### 拡張版テストファイル 次に、先程のものに拡張版のテストを加えた、`test_main_b.py` を作成します。 diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 5bbcce6059..de18856f44 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -1,189 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/ja/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: ja -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- チュートリアル - ユーザーガイド: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/query-params-str-validations.md - - tutorial/cookie-params.md - - tutorial/header-params.md - - tutorial/request-forms.md - - tutorial/body-updates.md - - セキュリティ: - - tutorial/security/first-steps.md - - tutorial/security/oauth2-jwt.md - - tutorial/middleware.md - - tutorial/cors.md - - tutorial/static-files.md - - tutorial/testing.md - - tutorial/debugging.md -- 高度なユーザーガイド: - - advanced/index.md - - advanced/path-operation-advanced-configuration.md - - advanced/additional-status-codes.md - - advanced/response-directly.md - - advanced/custom-response.md - - advanced/nosql-databases.md - - advanced/websockets.md - - advanced/conditional-openapi.md -- async.md -- デプロイ: - - deployment/index.md - - deployment/versions.md - - deployment/deta.md - - deployment/docker.md - - deployment/manually.md -- project-generation.md -- alternatives.md -- history-design-future.md -- external-links.md -- benchmarks.md -- help-fastapi.md -- contributing.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs/ko/docs/async.md b/docs/ko/docs/async.md new file mode 100644 index 0000000000..47dbaa1b01 --- /dev/null +++ b/docs/ko/docs/async.md @@ -0,0 +1,404 @@ +# 동시성과 async / await + +*경로 작동 함수*에서의 `async def` 문법에 대한 세부사항과 비동기 코드, 동시성 및 병렬성에 대한 배경 + +## 바쁘신 경우 + +요약 + +다음과 같이 `await`를 사용해 호출하는 제3의 라이브러리를 사용하는 경우: + +```Python +results = await some_library() +``` + +다음처럼 *경로 작동 함수*를 `async def`를 사용해 선언하십시오: + +```Python hl_lines="2" +@app.get('/') +async def read_results(): + results = await some_library() + return results +``` + +!!! note "참고" + `async def`로 생성된 함수 내부에서만 `await`를 사용할 수 있습니다. + +--- + +데이터베이스, API, 파일시스템 등과 의사소통하는 제3의 라이브러리를 사용하고, 그것이 `await`를 지원하지 않는 경우(현재 거의 모든 데이터베이스 라이브러리가 그러합니다), *경로 작동 함수*를 일반적인 `def`를 사용해 선언하십시오: + +```Python hl_lines="2" +@app.get('/') +def results(): + results = some_library() + return results +``` + +--- + +만약 당신의 응용프로그램이 (어째서인지) 다른 무엇과 의사소통하고 그것이 응답하기를 기다릴 필요가 없다면 `async def`를 사용하십시오. + +--- + +모르겠다면, 그냥 `def`를 사용하십시오. + +--- + +**참고**: *경로 작동 함수*에서 필요한만큼 `def`와 `async def`를 혼용할 수 있고, 가장 알맞은 것을 선택해서 정의할 수 있습니다. FastAPI가 자체적으로 알맞은 작업을 수행할 것입니다. + +어찌되었든, 상기 어떠한 경우라도, FastAPI는 여전히 비동기적으로 작동하고 매우 빠릅니다. + +그러나 상기 작업을 수행함으로써 어느 정도의 성능 최적화가 가능합니다. + +## 기술적 세부사항 + +최신 파이썬 버전은 `async`와 `await` 문법과 함께 **“코루틴”**이라고 하는 것을 사용하는 **“비동기 코드”**를 지원합니다. + +아래 섹션들에서 해당 문장을 부분별로 살펴보겠습니다: + +* **비동기 코드** +* **`async`와 `await`** +* **코루틴** + +## 비동기 코드 + +비동기 코드란 언어 💬 가 코드의 어느 한 부분에서, 컴퓨터 / 프로그램🤖에게 *다른 무언가*가 어딘가에서 끝날 때까지 기다려야한다고 말하는 방식입니다. *다른 무언가*가 “느린-파일" 📝 이라고 불린다고 가정해봅시다. + +따라서 “느린-파일” 📝이 끝날때까지 컴퓨터는 다른 작업을 수행할 수 있습니다. + +그 다음 컴퓨터 / 프로그램 🤖 은 다시 기다리고 있기 때문에 기회가 있을 때마다 다시 돌아오거나, 혹은 당시에 수행해야하는 작업들이 완료될 때마다 다시 돌아옵니다. 그리고 그것 🤖 은 기다리고 있던 작업 중 어느 것이 이미 완료되었는지, 그것 🤖 이 해야하는 모든 작업을 수행하면서 확인합니다. + +다음으로, 그것 🤖 은 완료할 첫번째 작업에 착수하고(우리의 "느린-파일" 📝 이라고 가정합시다) 그에 대해 수행해야하는 작업을 계속합니다. + +"다른 무언가를 기다리는 것"은 일반적으로 비교적 "느린" (프로세서와 RAM 메모리 속도에 비해) I/O 작업을 의미합니다. 예를 들면 다음의 것들을 기다리는 것입니다: + +* 네트워크를 통해 클라이언트로부터 전송되는 데이터 +* 네트워크를 통해 클라이언트가 수신할, 당신의 프로그램으로부터 전송되는 데이터 +* 시스템이 읽고 프로그램에 전달할 디스크 내의 파일 내용 +* 당신의 프로그램이 시스템에 전달하는, 디스크에 작성될 내용 +* 원격 API 작업 +* 완료될 데이터베이스 작업 +* 결과를 반환하는 데이터베이스 쿼리 +* 기타 + +수행 시간의 대부분이 I/O 작업을 기다리는데에 소요되기 때문에, "I/O에 묶인" 작업이라고 불립니다. + +이것은 "비동기"라고 불리는데 컴퓨터 / 프로그램이 작업 결과를 가지고 일을 수행할 수 있도록, 느린 작업에 "동기화"되어 아무것도 하지 않으면서 작업이 완료될 정확한 시점만을 기다릴 필요가 없기 때문입니다. + +이 대신에, "비동기" 시스템에서는, 작업은 일단 완료되면, 컴퓨터 / 프로그램이 수행하고 있는 일을 완료하고 이후 다시 돌아와서 그것의 결과를 받아 이를 사용해 작업을 지속할 때까지 잠시 (몇 마이크로초) 대기할 수 있습니다. + +"동기"("비동기"의 반대)는 컴퓨터 / 프로그램이 상이한 작업들간 전환을 하기 전에 그것이 대기를 동반하게 될지라도 모든 순서를 따르기 때문에 "순차"라는 용어로도 흔히 불립니다. + +### 동시성과 버거 + +위에서 설명한 **비동기** 코드에 대한 개념은 종종 **"동시성"**이라고도 불립니다. 이것은 **"병렬성"**과는 다릅니다. + +**동시성**과 **병렬성**은 모두 "동시에 일어나는 서로 다른 일들"과 관련이 있습니다. + +하지만 *동시성*과 *병렬성*의 세부적인 개념에는 꽤 차이가 있습니다. + +차이를 확인하기 위해, 다음의 버거에 대한 이야기를 상상해보십시오: + +### 동시 버거 + +당신은 짝사랑 상대 😍 와 패스트푸드 🍔 를 먹으러 갔습니다. 당신은 점원 💁 이 당신 앞에 있는 사람들의 주문을 받을 동안 줄을 서서 기다리고 있습니다. + +이제 당신의 순서가 되어서, 당신은 당신과 짝사랑 상대 😍 를 위한 두 개의 고급스러운 버거 🍔 를 주문합니다. + +당신이 돈을 냅니다 💸. + +점원 💁 은 주방 👨‍🍳 에 요리를 하라고 전달하고, 따라서 그들은 당신의 버거 🍔 를 준비해야한다는 사실을 알게됩니다(그들이 지금은 당신 앞 고객들의 주문을 준비하고 있을지라도 말입니다). + +점원 💁 은 당신의 순서가 적힌 번호표를 줍니다. + +기다리는 동안, 당신은 짝사랑 상대 😍 와 함께 테이블을 고르고, 자리에 앉아 오랫동안 (당신이 주문한 버거는 꽤나 고급스럽기 때문에 준비하는데 시간이 조금 걸립니다 ✨🍔✨) 대화를 나눕니다. + +짝사랑 상대 😍 와 테이블에 앉아서 버거 🍔 를 기다리는 동안, 그 사람 😍 이 얼마나 멋지고, 사랑스럽고, 똑똑한지 감탄하며 시간을 보냅니다 ✨😍✨. + +짝사랑 상대 😍 와 기다리면서 얘기하는 동안, 때때로, 당신은 당신의 차례가 되었는지 보기 위해 카운터의 번호를 확인합니다. + +그러다 어느 순간, 당신의 차례가 됩니다. 카운터에 가서, 버거 🍔 를 받고, 테이블로 다시 돌아옵니다. + +당신과 짝사랑 상대 😍 는 버거 🍔 를 먹으며 좋은 시간을 보냅니다 ✨. + +--- + +당신이 이 이야기에서 컴퓨터 / 프로그램 🤖 이라고 상상해보십시오. + +줄을 서서 기다리는 동안, 당신은 아무것도 하지 않고 😴 당신의 차례를 기다리며, 어떠한 "생산적인" 일도 하지 않습니다. 하지만 점원 💁 이 (음식을 준비하지는 않고) 주문을 받기만 하기 때문에 줄이 빨리 줄어들어서 괜찮습니다. + +그다음, 당신이 차례가 오면, 당신은 실제로 "생산적인" 일 🤓 을 합니다. 당신은 메뉴를 보고, 무엇을 먹을지 결정하고, 짝사랑 상대 😍 의 선택을 묻고, 돈을 내고 💸 , 맞는 카드를 냈는지 확인하고, 비용이 제대로 지불되었는지 확인하고, 주문이 제대로 들어갔는지 확인을 하는 작업 등등을 수행합니다. + +하지만 이후에는, 버거 🍔 를 아직 받지 못했음에도, 버거가 준비될 때까지 기다려야 🕙 하기 때문에 점원 💁 과의 작업은 "일시정지" ⏸ 상태입니다. + +하지만 번호표를 받고 카운터에서 나와 테이블에 앉으면, 당신은 짝사랑 상대 😍 와 그 "작업" ⏯ 🤓 에 번갈아가며 🔀 집중합니다. 그러면 당신은 다시 짝사랑 상대 😍 에게 작업을 거는 매우 "생산적인" 일 🤓 을 합니다. + +점원 💁 이 카운터 화면에 당신의 번호를 표시함으로써 "버거 🍔 가 준비되었습니다"라고 해도, 당신은 즉시 뛰쳐나가지는 않을 것입니다. 당신은 당신의 번호를 갖고있고, 다른 사람들은 그들의 번호를 갖고있기 때문에, 아무도 당신의 버거 🍔 를 훔쳐가지 않는다는 사실을 알기 때문입니다. + +그래서 당신은 짝사랑 상대 😍 가 이야기를 끝낼 때까지 기다린 후 (현재 작업 완료 ⏯ / 진행 중인 작업 처리 🤓 ), 정중하게 미소짓고 버거를 가지러 가겠다고 말합니다 ⏸. + +그다음 당신은 카운터에 가서 🔀 , 초기 작업을 이제 완료하고 ⏯ , 버거 🍔 를 받고, 감사하다고 말하고 테이블로 가져옵니다. 이로써 카운터와의 상호작용 단계 / 작업이 종료됩니다 ⏹. + +이전 작업인 "버거 받기"가 종료되면 ⏹ "버거 먹기"라는 새로운 작업이 생성됩니다 🔀 ⏯. + +### 병렬 버거 + +이제 "동시 버거"가 아닌 "병렬 버거"를 상상해보십시오. + +당신은 짝사랑 상대 😍 와 함께 병렬 패스트푸드 🍔 를 먹으러 갔습니다. + +당신은 여러명(8명이라고 가정합니다)의 점원이 당신 앞 사람들의 주문을 받으며 동시에 요리 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳 도 하는 동안 줄을 서서 기다립니다. + +당신 앞 모든 사람들이 버거가 준비될 때까지 카운터에서 떠나지 않고 기다립니다 🕙 . 왜냐하면 8명의 직원들이 다음 주문을 받기 전에 버거를 준비하러 가기 때문입니다. + +마침내 당신의 차례가 왔고, 당신은 당신과 짝사랑 상대 😍 를 위한 두 개의 고급스러운 버거 🍔 를 주문합니다. + +당신이 비용을 지불합니다 💸 . + +점원이 주방에 갑니다 👨‍🍳 . + +당신은 번호표가 없기 때문에 누구도 당신의 버거 🍔 를 대신 가져갈 수 없도록 카운터에 서서 기다립니다 🕙 . + +당신과 짝사랑 상대 😍 는 다른 사람이 새치기해서 버거를 가져가지 못하게 하느라 바쁘기 때문에 🕙 , 짝사랑 상대에게 주의를 기울일 수 없습니다 😞 . + +이것은 "동기" 작업이고, 당신은 점원/요리사 👨‍🍳 와 "동기화" 되었습니다. 당신은 기다리고 🕙 , 점원/요리사 👨‍🍳 가 버거 🍔 준비를 완료한 후 당신에게 주거나, 누군가가 그것을 가져가는 그 순간에 그 곳에 있어야합니다. + +카운터 앞에서 오랫동안 기다린 후에 🕙 , 점원/요리사 👨‍🍳 가 당신의 버거 🍔 를 가지고 돌아옵니다. + +당신은 버거를 받고 짝사랑 상대와 함께 테이블로 돌아옵니다. + +단지 먹기만 하다가, 다 먹었습니다 🍔 ⏹. + +카운터 앞에서 기다리면서 🕙 너무 많은 시간을 허비했기 때문에 대화를 하거나 작업을 걸 시간이 거의 없었습니다 😞 . + +--- + +이 병렬 버거 시나리오에서, 당신은 기다리고 🕙 , 오랜 시간동안 "카운터에서 기다리는" 🕙 데에 주의를 기울이는 ⏯ 두 개의 프로세서(당신과 짝사랑 상대😍)를 가진 컴퓨터 / 프로그램 🤖 입니다. + +패스트푸드점에는 8개의 프로세서(점원/요리사) 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳 가 있습니다. 동시 버거는 단 두 개(한 명의 직원과 한 명의 요리사) 💁 👨‍🍳 만을 가지고 있었습니다. + +하지만 여전히, 병렬 버거 예시가 최선은 아닙니다 😞 . + +--- + +이 예시는 버거🍔 이야기와 결이 같습니다. + +더 "현실적인" 예시로, 은행을 상상해보십시오. + +최근까지, 대다수의 은행에는 다수의 은행원들 👨‍💼👨‍💼👨‍💼👨‍💼 과 긴 줄 🕙🕙🕙🕙🕙🕙🕙🕙 이 있습니다. + +모든 은행원들은 한 명 한 명의 고객들을 차례로 상대합니다 👨‍💼⏯ . + +그리고 당신은 오랫동안 줄에서 기다려야하고 🕙 , 그렇지 않으면 당신의 차례를 잃게 됩니다. + +아마 당신은 은행 🏦 심부름에 짝사랑 상대 😍 를 데려가고 싶지는 않을 것입니다. + +### 버거 예시의 결론 + +"짝사랑 상대와의 패스트푸드점 버거" 시나리오에서, 오랜 기다림 🕙 이 있기 때문에 동시 시스템 ⏸🔀⏯ 을 사용하는 것이 더 합리적입니다. + +대다수의 웹 응용프로그램의 경우가 그러합니다. + +매우 많은 수의 유저가 있지만, 서버는 그들의 요청을 전송하기 위해 그닥-좋지-않은 연결을 기다려야 합니다 🕙 . + +그리고 응답이 돌아올 때까지 다시 기다려야 합니다 🕙 . + +이 "기다림" 🕙 은 마이크로초 단위이지만, 모두 더해지면, 결국에는 매우 긴 대기시간이 됩니다. + +따라서 웹 API를 위해 비동기 ⏸🔀⏯ 코드를 사용하는 것이 합리적입니다. + +대부분의 존재하는 유명한 파이썬 프레임워크 (Flask와 Django 등)은 새로운 비동기 기능들이 파이썬에 존재하기 전에 만들어졌습니다. 그래서, 그들의 배포 방식은 병렬 실행과 새로운 기능만큼 강력하지는 않은 예전 버전의 비동기 실행을 지원합니다. + +비동기 웹 파이썬(ASGI)에 대한 주요 명세가 웹소켓을 지원하기 위해 Django에서 개발 되었음에도 그렇습니다. + +이러한 종류의 비동기성은 (NodeJS는 병렬적이지 않음에도) NodeJS가 사랑받는 이유이고, 프로그래밍 언어로서의 Go의 강점입니다. + +그리고 **FastAPI**를 사용함으로써 동일한 성능을 낼 수 있습니다. + +또한 병렬성과 비동기성을 동시에 사용할 수 있기 때문에, 대부분의 테스트가 완료된 NodeJS 프레임워크보다 더 높은 성능을 얻고 C에 더 가까운 컴파일 언어인 Go와 동등한 성능을 얻을 수 있습니다(모두 Starlette 덕분입니다). + +### 동시성이 병렬성보다 더 나은가? + +그렇지 않습니다! 그것이 이야기의 교훈은 아닙니다. + +동시성은 병렬성과 다릅니다. 그리고 그것은 많은 대기를 필요로하는 **특정한** 시나리오에서는 더 낫습니다. 이로 인해, 웹 응용프로그램 개발에서 동시성이 병렬성보다 일반적으로 훨씬 낫습니다. 하지만 모든 경우에 그런 것은 아닙니다. + +따라서, 균형을 맞추기 위해, 다음의 짧은 이야기를 상상해보십시오: + +> 당신은 크고, 더러운 집을 청소해야합니다. + +*네, 이게 전부입니다*. + +--- + +어디에도 대기 🕙 는 없고, 집안 곳곳에서 해야하는 많은 작업들만 있습니다. + +버거 예시처럼 처음에는 거실, 그 다음은 부엌과 같은 식으로 순서를 정할 수도 있으나, 무엇도 기다리지 🕙 않고 계속해서 청소 작업만 수행하기 때문에, 순서는 아무런 영향을 미치지 않습니다. + +순서가 있든 없든 동일한 시간이 소요될 것이고(동시성) 동일한 양의 작업을 하게 될 것입니다. + +하지만 이 경우에서, 8명의 전(前)-점원/요리사이면서-현(現)-청소부 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳 를 고용할 수 있고, 그들 각자(그리고 당신)가 집의 한 부분씩 맡아 청소를 한다면, 당신은 **병렬적**으로 작업을 수행할 수 있고, 조금의 도움이 있다면, 훨씬 더 빨리 끝낼 수 있습니다. + +이 시나리오에서, (당신을 포함한) 각각의 청소부들은 프로세서가 될 것이고, 각자의 역할을 수행합니다. + +실행 시간의 대부분이 대기가 아닌 실제 작업에 소요되고, 컴퓨터에서 작업은 CPU에서 이루어지므로, 이러한 문제를 "CPU에 묶였"다고 합니다. + +--- + +CPU에 묶인 연산에 관한 흔한 예시는 복잡한 수학 처리를 필요로 하는 경우입니다. + +예를 들어: + +* **오디오** 또는 **이미지** 처리. +* **컴퓨터 비전**: 하나의 이미지는 수백개의 픽셀로 구성되어있고, 각 픽셀은 3개의 값 / 색을 갖고 있으며, 일반적으로 해당 픽셀들에 대해 동시에 무언가를 계산해야하는 처리. +* **머신러닝**: 일반적으로 많은 "행렬"과 "벡터" 곱셈이 필요합니다. 거대한 스프레드 시트에 수들이 있고 그 수들을 동시에 곱해야 한다고 생각해보십시오. +* **딥러닝**: 머신러닝의 하위영역으로, 동일한 예시가 적용됩니다. 단지 이 경우에는 하나의 스프레드 시트에 곱해야할 수들이 있는 것이 아니라, 거대한 세트의 스프레드 시트들이 있고, 많은 경우에, 이 모델들을 만들고 사용하기 위해 특수한 프로세서를 사용합니다. + +### 동시성 + 병렬성: 웹 + 머신러닝 + +**FastAPI**를 사용하면 웹 개발에서는 매우 흔한 동시성의 이점을 (NodeJS의 주된 매력만큼) 얻을 수 있습니다. + +뿐만 아니라 머신러닝 시스템과 같이 **CPU에 묶인** 작업을 위해 병렬성과 멀티프로세싱(다수의 프로세스를 병렬적으로 동작시키는 것)을 이용하는 것도 가능합니다. + +파이썬이 **데이터 사이언스**, 머신러닝과 특히 딥러닝에 의 주된 언어라는 간단한 사실에 더해서, 이것은 FastAPI를 데이터 사이언스 / 머신러닝 웹 API와 응용프로그램에 (다른 것들보다) 좋은 선택지가 되게 합니다. + +배포시 병렬을 어떻게 가능하게 하는지 알고싶다면, [배포](/ko/deployment){.internal-link target=_blank}문서를 참고하십시오. + +## `async`와 `await` + +최신 파이썬 버전에는 비동기 코드를 정의하는 매우 직관적인 방법이 있습니다. 이는 이것을 평범한 "순차적" 코드로 보이게 하고, 적절한 순간에 당신을 위해 "대기"합니다. + +연산이 결과를 전달하기 전에 대기를 해야하고 새로운 파이썬 기능들을 지원한다면, 이렇게 코드를 작성할 수 있습니다: + +```Python +burgers = await get_burgers(2) +``` + +여기서 핵심은 `await`입니다. 이것은 파이썬에게 `burgers` 결과를 저장하기 이전에 `get_burgers(2)`의 작업이 완료되기를 🕙 기다리라고 ⏸ 말합니다. 이로 인해, 파이썬은 그동안 (다른 요청을 받는 것과 같은) 다른 작업을 수행해도 된다는 것을 🔀 ⏯ 알게될 것입니다. + +`await`가 동작하기 위해, 이것은 비동기를 지원하는 함수 내부에 있어야 합니다. 이를 위해서 함수를 `async def`를 사용해 정의하기만 하면 됩니다: + +```Python hl_lines="1" +async def get_burgers(number: int): + # Do some asynchronous stuff to create the burgers + return burgers +``` + +...`def`를 사용하는 대신: + +```Python hl_lines="2" +# This is not asynchronous +def get_sequential_burgers(number: int): + # Do some sequential stuff to create the burgers + return burgers +``` + +`async def`를 사용하면, 파이썬은 해당 함수 내에서 `await` 표현에 주의해야한다는 사실과, 해당 함수의 실행을 "일시정지"⏸하고 다시 돌아오기 전까지 다른 작업을 수행🔀할 수 있다는 것을 알게됩니다. + +`async def`f 함수를 호출하고자 할 때, "대기"해야합니다. 따라서, 아래는 동작하지 않습니다. + +```Python +# This won't work, because get_burgers was defined with: async def +burgers = get_burgers(2) +``` + +--- + +따라서, `await`f를 사용해서 호출할 수 있는 라이브러리를 사용한다면, 다음과 같이 `async def`를 사용하는 *경로 작동 함수*를 생성해야 합니다: + +```Python hl_lines="2-3" +@app.get('/burgers') +async def read_burgers(): + burgers = await get_burgers(2) + return burgers +``` + +### 더 세부적인 기술적 사항 + +`await`가 `async def`를 사용하는 함수 내부에서만 사용이 가능하다는 것을 눈치채셨을 것입니다. + +하지만 동시에, `async def`로 정의된 함수들은 "대기"되어야만 합니다. 따라서, `async def`를 사용한 함수들은 역시 `async def`를 사용한 함수 내부에서만 호출될 수 있습니다. + +그렇다면 닭이 먼저냐, 달걀이 먼저냐, 첫 `async` 함수를 어떻게 호출할 수 있겠습니까? + +**FastAPI**를 사용해 작업한다면 이것을 걱정하지 않아도 됩니다. 왜냐하면 그 "첫" 함수는 당신의 *경로 작동 함수*가 될 것이고, FastAPI는 어떻게 올바르게 처리할지 알고있기 때문입니다. + +하지만 FastAPI를 사용하지 않고 `async` / `await`를 사용하고 싶다면, 이 역시 가능합니다. + +### 당신만의 비동기 코드 작성하기 + +Starlette(그리고 FastAPI)는 AnyIO를 기반으로 하고있고, 따라서 파이썬 표준 라이브러리인 asyncioTrio와 호환됩니다. + +특히, 코드에서 고급 패턴이 필요한 고급 동시성을 사용하는 경우 직접적으로 AnyIO를 사용할 수 있습니다. + +FastAPI를 사용하지 않더라도, 높은 호환성 및 AnyIO의 이점(예: *구조화된 동시성*)을 취하기 위해 AnyIO를 사용해 비동기 응용프로그램을 작성할 수 있습니다. + +### 비동기 코드의 다른 형태 + +파이썬에서 `async`와 `await`를 사용하게 된 것은 비교적 최근의 일입니다. + +하지만 이로 인해 비동기 코드 작업이 훨씬 간단해졌습니다. + +같은 (또는 거의 유사한) 문법은 최신 버전의 자바스크립트(브라우저와 NodeJS)에도 추가되었습니다. + +하지만 그 이전에, 비동기 코드를 처리하는 것은 꽤 복잡하고 어려운 일이었습니다. + +파이썬의 예전 버전이라면, 스레드 또는 Gevent를 사용할 수 있을 것입니다. 하지만 코드를 이해하고, 디버깅하고, 이에 대해 생각하는게 훨씬 복잡합니다. + +예전 버전의 NodeJS / 브라우저 자바스크립트라면, "콜백 함수"를 사용했을 것입니다. 그리고 이로 인해 콜백 지옥에 빠지게 될 수 있습니다. + +## 코루틴 + +**코루틴**은 `async def` 함수가 반환하는 것을 칭하는 매우 고급스러운 용어일 뿐입니다. 파이썬은 그것이 시작되고 어느 시점에서 완료되지만 내부에 `await`가 있을 때마다 내부적으로 일시정지⏸될 수도 있는 함수와 유사한 것이라는 사실을 알고있습니다. + +그러나 `async` 및 `await`와 함께 비동기 코드를 사용하는 이 모든 기능들은 "코루틴"으로 간단히 요약됩니다. 이것은 Go의 주된 핵심 기능인 "고루틴"에 견줄 수 있습니다. + +## 결론 + +상기 문장을 다시 한 번 봅시다: + +> 최신 파이썬 버전은 **`async` 및 `await`** 문법과 함께 **“코루틴”**이라고 하는 것을 사용하는 **“비동기 코드”**를 지원합니다. + +이제 이 말을 조금 더 이해할 수 있을 것입니다. ✨ + +이것이 (Starlette을 통해) FastAPI를 강하게 하면서 그것이 인상적인 성능을 낼 수 있게 합니다. + +## 매우 세부적인 기술적 사항 + +!!! warning "경고" + 이 부분은 넘어가도 됩니다. + + 이것들은 **FastAPI**가 내부적으로 어떻게 동작하는지에 대한 매우 세부적인 기술사항입니다. + + 만약 기술적 지식(코루틴, 스레드, 블록킹 등)이 있고 FastAPI가 어떻게 `async def` vs `def`를 다루는지 궁금하다면, 계속하십시오. + +### 경로 작동 함수 + +경로 작동 함수를 `async def` 대신 일반적인 `def`로 선언하는 경우, (서버를 차단하는 것처럼) 그것을 직접 호출하는 대신 대기중인 외부 스레드풀에서 실행됩니다. + +만약 상기에 묘사된대로 동작하지 않는 비동기 프로그램을 사용해왔고 약간의 성능 향상 (약 100 나노초)을 위해 `def`를 사용해서 계산만을 위한 사소한 *경로 작동 함수*를 정의해왔다면, **FastAPI**는 이와는 반대라는 것에 주의하십시오. 이러한 경우에, *경로 작동 함수*가 블로킹 I/O를 수행하는 코드를 사용하지 않는 한 `async def`를 사용하는 편이 더 낫습니다. + +하지만 두 경우 모두, FastAPI가 당신이 전에 사용하던 프레임워크보다 [더 빠를](/#performance){.internal-link target=_blank} (최소한 비견될) 확률이 높습니다. + +### 의존성 + +의존성에도 동일하게 적용됩니다. 의존성이 `async def`가 아닌 표준 `def` 함수라면, 외부 스레드풀에서 실행됩니다. + +### 하위-의존성 + +함수 정의시 매개변수로 서로를 필요로하는 다수의 의존성과 하위-의존성을 가질 수 있고, 그 중 일부는 `async def`로, 다른 일부는 일반적인 `def`로 생성되었을 수 있습니다. 이것은 여전히 잘 동작하고, 일반적인 `def`로 생성된 것들은 "대기"되는 대신에 (스레드풀로부터) 외부 스레드에서 호출됩니다. + +### 다른 유틸리티 함수 + +직접 호출되는 다른 모든 유틸리티 함수는 일반적인 `def`나 `async def`로 생성될 수 있고 FastAPI는 이를 호출하는 방식에 영향을 미치지 않습니다. + +이것은 FastAPI가 당신을 위해 호출하는 함수와는 반대입니다: *경로 작동 함수*와 의존성 + +만약 당신의 유틸리티 함수가 `def`를 사용한 일반적인 함수라면, 스레드풀에서가 아니라 직접 호출(당신이 코드에 작성한 대로)될 것이고, `async def`로 생성된 함수라면 코드에서 호출할 때 그 함수를 `await` 해야 합니다. + +--- + +다시 말하지만, 이것은 당신이 이것에 대해 찾고있던 경우에 한해 유용할 매우 세부적인 기술사항입니다. + +그렇지 않은 경우, 상기의 가이드라인만으로도 충분할 것입니다: [바쁘신 경우](#in-a-hurry). diff --git a/docs/ko/docs/deployment/cloud.md b/docs/ko/docs/deployment/cloud.md new file mode 100644 index 0000000000..f2b965a911 --- /dev/null +++ b/docs/ko/docs/deployment/cloud.md @@ -0,0 +1,17 @@ +# FastAPI를 클라우드 제공업체에서 배포하기 + +사실상 거의 **모든 클라우드 제공업체**를 사용하여 여러분의 FastAPI 애플리케이션을 배포할 수 있습니다. + +대부분의 경우, 주요 클라우드 제공업체에서는 FastAPI를 배포할 수 있도록 가이드를 제공합니다. + +## 클라우드 제공업체 - 후원자들 + +몇몇 클라우드 제공업체들은 [**FastAPI를 후원하며**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, 이를 통해 FastAPI와 FastAPI **생태계**가 지속적이고 건전한 **발전**을 할 수 있습니다. + +이는 FastAPI와 **커뮤니티** (여러분)에 대한 진정한 헌신을 보여줍니다. 그들은 여러분에게 **좋은 서비스**를 제공할 뿐 만이 아니라 여러분이 **훌륭하고 건강한 프레임워크인** FastAPI 를 사용하길 원하기 때문입니다. 🙇 + +아래와 같은 서비스를 사용해보고 각 서비스의 가이드를 따를 수도 있습니다: + +* Platform.sh +* Porter +* Deta diff --git a/docs/ko/docs/index.md b/docs/ko/docs/index.md index c64713705b..594b092f73 100644 --- a/docs/ko/docs/index.md +++ b/docs/ko/docs/index.md @@ -24,7 +24,7 @@ --- -FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트에 기초한 Python3.6+의 API를 빌드하기 위한 웹 프레임워크입니다. +FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트에 기초한 Python3.8+의 API를 빌드하기 위한 웹 프레임워크입니다. 주요 특징으로: @@ -107,7 +107,7 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 ## 요구사항 -Python 3.7+ +Python 3.8+ FastAPI는 거인들의 어깨 위에 서 있습니다: @@ -323,7 +323,7 @@ def update_item(item_id: int, item: Item): 새로운 문법, 특정 라이브러리의 메소드나 클래스 등을 배울 필요가 없습니다. -그저 표준 **Python 3.6+**입니다. +그저 표준 **Python 3.8+** 입니다. 예를 들어, `int`에 대해선: @@ -437,7 +437,6 @@ item: Item Pydantic이 사용하는: -* ujson - 더 빠른 JSON "파싱". * email_validator - 이메일 유효성 검사. Starlette이 사용하는: diff --git a/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md new file mode 100644 index 0000000000..bbf3a82838 --- /dev/null +++ b/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md @@ -0,0 +1,244 @@ +# 의존성으로서의 클래스 + +**의존성 주입** 시스템에 대해 자세히 살펴보기 전에 이전 예제를 업그레이드 해보겠습니다. + +## 이전 예제의 `딕셔너리` + +이전 예제에서, 우리는 의존성(의존 가능한) 함수에서 `딕셔너리`객체를 반환하고 있었습니다: + +=== "파이썬 3.6 이상" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +우리는 *경로 작동 함수*의 매개변수 `commons`에서 `딕셔너리` 객체를 얻습니다. + +그리고 우리는 에디터들이 `딕셔너리` 객체의 키나 밸류의 자료형을 알 수 없기 때문에 자동 완성과 같은 기능을 제공해 줄 수 없다는 것을 알고 있습니다. + +더 나은 방법이 있을 것 같습니다... + +## 의존성으로 사용 가능한 것 + +지금까지 함수로 선언된 의존성을 봐왔습니다. + +아마도 더 일반적이기는 하겠지만 의존성을 선언하는 유일한 방법은 아닙니다. + +핵심 요소는 의존성이 "호출 가능"해야 한다는 것입니다 + +파이썬에서의 "**호출 가능**"은 파이썬이 함수처럼 "호출"할 수 있는 모든 것입니다. + +따라서, 만약 당신이 `something`(함수가 아닐 수도 있음) 객체를 가지고 있고, + +```Python +something() +``` + +또는 + +```Python +something(some_argument, some_keyword_argument="foo") +``` + +상기와 같은 방식으로 "호출(실행)" 할 수 있다면 "호출 가능"이 됩니다. + +## 의존성으로서의 클래스 + +파이썬 클래스의 인스턴스를 생성하기 위해 사용하는 것과 동일한 문법을 사용한다는 걸 알 수 있습니다. + +예를 들어: + +```Python +class Cat: + def __init__(self, name: str): + self.name = name + + +fluffy = Cat(name="Mr Fluffy") +``` + +이 경우에 `fluffy`는 클래스 `Cat`의 인스턴스입니다. 그리고 우리는 `fluffy`를 만들기 위해서 `Cat`을 "호출"했습니다. + +따라서, 파이썬 클래스는 **호출 가능**합니다. + +그래서 **FastAPI**에서는 파이썬 클래스를 의존성으로 사용할 수 있습니다. + +FastAPI가 실질적으로 확인하는 것은 "호출 가능성"(함수, 클래스 또는 다른 모든 것)과 정의된 매개변수들입니다. + +"호출 가능"한 것을 의존성으로서 **FastAPI**에 전달하면, 그 "호출 가능"한 것의 매개변수들을 분석한 후 이를 *경로 동작 함수*를 위한 매개변수와 동일한 방식으로 처리합니다. 하위-의존성 또한 같은 방식으로 처리합니다. + +매개변수가 없는 "호출 가능"한 것 역시 매개변수가 없는 *경로 동작 함수*와 동일한 방식으로 적용됩니다. + +그래서, 우리는 위 예제에서의 `common_paramenters` 의존성을 클래스 `CommonQueryParams`로 바꿀 수 있습니다. + +=== "파이썬 3.6 이상" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="9-13" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +클래스의 인스턴스를 생성하는 데 사용되는 `__init__` 메서드에 주목하기 바랍니다: + +=== "파이썬 3.6 이상" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +...이전 `common_parameters`와 동일한 매개변수를 가집니다: + +=== "파이썬 3.6 이상" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="6" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +이 매개변수들은 **FastAPI**가 의존성을 "해결"하기 위해 사용할 것입니다 + +함수와 클래스 두 가지 방식 모두, 아래 요소를 갖습니다: + +* `문자열`이면서 선택사항인 쿼리 매개변수 `q`. +* 기본값이 `0`이면서 `정수형`인 쿼리 매개변수 `skip` +* 기본값이 `100`이면서 `정수형`인 쿼리 매개변수 `limit` + +두 가지 방식 모두, 데이터는 변환, 검증되고 OpenAPI 스키마에 문서화됩니다. + +## 사용해봅시다! + +이제 아래의 클래스를 이용해서 의존성을 정의할 수 있습니다. + +=== "파이썬 3.6 이상" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +**FastAPI**는 `CommonQueryParams` 클래스를 호출합니다. 이것은 해당 클래스의 "인스턴스"를 생성하고 그 인스턴스는 함수의 매개변수 `commons`로 전달됩니다. + +## 타입 힌팅 vs `Depends` + +위 코드에서 `CommonQueryParams`를 두 번 작성한 방식에 주목하십시오: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +마지막 `CommonQueryParams` 변수를 보면: + +```Python +... = Depends(CommonQueryParams) +``` + +... **FastAPI**가 실제로 어떤 것이 의존성인지 알기 위해서 사용하는 방법입니다. +FastAPI는 선언된 매개변수들을 추출할 것이고 실제로 이 변수들을 호출할 것입니다. + +--- + +이 경우에, 첫번째 `CommonQueryParams` 변수를 보면: + +```Python +commons: CommonQueryParams ... +``` + +... **FastAPI**는 `CommonQueryParams` 변수에 어떠한 특별한 의미도 부여하지 않습니다. FastAPI는 이 변수를 데이터 변환, 검증 등에 활용하지 않습니다. (활용하려면 `= Depends(CommonQueryParams)`를 사용해야 합니다.) + +사실 아래와 같이 작성해도 무관합니다: + +```Python +commons = Depends(CommonQueryParams) +``` + +..전체적인 코드는 아래와 같습니다: + +=== "파이썬 3.6 이상" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial003_py310.py!} + ``` + +그러나 자료형을 선언하면 에디터가 매개변수 `commons`로 전달될 것이 무엇인지 알게 되고, 이를 통해 코드 완성, 자료형 확인 등에 도움이 될 수 있으므로 권장됩니다. + + + +## 코드 단축 + +그러나 여기 `CommonQueryParams`를 두 번이나 작성하는, 코드 반복이 있다는 것을 알 수 있습니다: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +**FastAPI**는 *특히* 의존성이 **FastAPI**가 클래스 자체의 인스턴스를 생성하기 위해 "호출"하는 클래스인 경우, 조금 더 쉬운 방법을 제공합니다. + +이러한 특정한 경우에는 아래처럼 사용할 수 있습니다: + +이렇게 쓰는 것 대신: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +...이렇게 쓸 수 있습니다.: + +```Python +commons: CommonQueryParams = Depends() +``` + +의존성을 매개변수의 타입으로 선언하는 경우 `Depends(CommonQueryParams)`처럼 클래스 이름 전체를 *다시* 작성하는 대신, 매개변수를 넣지 않은 `Depends()`의 형태로 사용할 수 있습니다. + +아래에 같은 예제가 있습니다: + +=== "파이썬 3.6 이상" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial004_py310.py!} + ``` + +...이렇게 코드를 단축하여도 **FastAPI**는 무엇을 해야하는지 알고 있습니다. + +!!! tip "팁" + 만약 이것이 도움이 되기보다 더 헷갈리게 만든다면, 잊어버리십시오. 이것이 반드시 필요한 것은 아닙니다. + + 이것은 단지 손쉬운 방법일 뿐입니다. 왜냐하면 **FastAPI**는 코드 반복을 최소화할 수 있는 방법을 고민하기 때문입니다. diff --git a/docs/ko/docs/tutorial/index.md b/docs/ko/docs/tutorial/index.md index d6db525e8d..deb5ca8f27 100644 --- a/docs/ko/docs/tutorial/index.md +++ b/docs/ko/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# 자습서 - 사용자 안내서 - 도입부 +# 자습서 - 사용자 안내서 이 자습서는 **FastAPI**의 대부분의 기능을 단계별로 사용하는 방법을 보여줍니다. diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 3dfc208c8a..de18856f44 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -1,157 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/ko/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- 자습서 - 사용자 안내서: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/header-params.md - - tutorial/path-params-numeric-validations.md - - tutorial/response-status-code.md - - tutorial/request-files.md - - tutorial/request-forms-and-files.md - - tutorial/encoder.md - - tutorial/cors.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs/language_names.yml b/docs/language_names.yml new file mode 100644 index 0000000000..7c37ff2b13 --- /dev/null +++ b/docs/language_names.yml @@ -0,0 +1,183 @@ +aa: Afaraf +ab: аҧсуа бызшәа +ae: avesta +af: Afrikaans +ak: Akan +am: አማርኛ +an: aragonés +ar: اللغة العربية +as: অসমীয়া +av: авар мацӀ +ay: aymar aru +az: azərbaycan dili +ba: башҡорт теле +be: беларуская мова +bg: български език +bh: भोजपुरी +bi: Bislama +bm: bamanankan +bn: বাংলা +bo: བོད་ཡིག +br: brezhoneg +bs: bosanski jezik +ca: Català +ce: нохчийн мотт +ch: Chamoru +co: corsu +cr: ᓀᐦᐃᔭᐍᐏᐣ +cs: čeština +cu: ѩзыкъ словѣньскъ +cv: чӑваш чӗлхи +cy: Cymraeg +da: dansk +de: Deutsch +dv: Dhivehi +dz: རྫོང་ཁ +ee: Eʋegbe +el: Ελληνικά +en: English +eo: Esperanto +es: español +et: eesti +eu: euskara +fa: فارسی +ff: Fulfulde +fi: suomi +fj: Vakaviti +fo: føroyskt +fr: français +fy: Frysk +ga: Gaeilge +gd: Gàidhlig +gl: galego +gu: ગુજરાતી +gv: Gaelg +ha: هَوُسَ +he: עברית +hi: हिन्दी +ho: Hiri Motu +hr: Hrvatski +ht: Kreyòl ayisyen +hu: magyar +hy: Հայերեն +hz: Otjiherero +ia: Interlingua +id: Bahasa Indonesia +ie: Interlingue +ig: Asụsụ Igbo +ii: ꆈꌠ꒿ Nuosuhxop +ik: Iñupiaq +io: Ido +is: Íslenska +it: italiano +iu: ᐃᓄᒃᑎᑐᑦ +ja: 日本語 +jv: basa Jawa +ka: ქართული +kg: Kikongo +ki: Gĩkũyũ +kj: Kuanyama +kk: қазақ тілі +kl: kalaallisut +km: ខេមរភាសា +kn: ಕನ್ನಡ +ko: 한국어 +kr: Kanuri +ks: कश्मीरी +ku: Kurdî +kv: коми кыв +kw: Kernewek +ky: Кыргызча +la: latine +lb: Lëtzebuergesch +lg: Luganda +li: Limburgs +ln: Lingála +lo: ພາສາ +lt: lietuvių kalba +lu: Tshiluba +lv: latviešu valoda +mg: fiteny malagasy +mh: Kajin M̧ajeļ +mi: te reo Māori +mk: македонски јазик +ml: മലയാളം +mn: Монгол хэл +mr: मराठी +ms: Bahasa Malaysia +mt: Malti +my: ဗမာစာ +na: Ekakairũ Naoero +nb: Norsk bokmål +nd: isiNdebele +ne: नेपाली +ng: Owambo +nl: Nederlands +nn: Norsk nynorsk +'no': Norsk +nr: isiNdebele +nv: Diné bizaad +ny: chiCheŵa +oc: occitan +oj: ᐊᓂᔑᓈᐯᒧᐎᓐ +om: Afaan Oromoo +or: ଓଡ଼ିଆ +os: ирон æвзаг +pa: ਪੰਜਾਬੀ +pi: पाऴि +pl: Polski +ps: پښتو +pt: português +qu: Runa Simi +rm: rumantsch grischun +rn: Ikirundi +ro: Română +ru: русский язык +rw: Ikinyarwanda +sa: संस्कृतम् +sc: sardu +sd: सिन्धी +se: Davvisámegiella +sg: yângâ tî sängö +si: සිංහල +sk: slovenčina +sl: slovenščina +sn: chiShona +so: Soomaaliga +sq: shqip +sr: српски језик +ss: SiSwati +st: Sesotho +su: Basa Sunda +sv: svenska +sw: Kiswahili +ta: தமிழ் +te: తెలుగు +tg: тоҷикӣ +th: ไทย +ti: ትግርኛ +tk: Türkmen +tl: Wikang Tagalog +tn: Setswana +to: faka Tonga +tr: Türkçe +ts: Xitsonga +tt: татар теле +tw: Twi +ty: Reo Tahiti +ug: ئۇيغۇرچە‎ +uk: українська мова +ur: اردو +uz: Ўзбек +ve: Tshivenḓa +vi: Tiếng Việt +vo: Volapük +wa: walon +wo: Wollof +xh: isiXhosa +yi: ייִדיש +yo: Yorùbá +za: Saɯ cueŋƅ +zh: 汉语 +zh-hant: 繁體中文 +zu: isiZulu diff --git a/docs/nl/docs/index.md b/docs/nl/docs/index.md deleted file mode 100644 index 23143a96fb..0000000000 --- a/docs/nl/docs/index.md +++ /dev/null @@ -1,468 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**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. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## 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.7+ - -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[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**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="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -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). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on HTTPX 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: - -* httpx - Required if you want to use the `TestClient`. -* 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). -* 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/nl/mkdocs.yml b/docs/nl/mkdocs.yml deleted file mode 100644 index 6d46939f93..0000000000 --- a/docs/nl/mkdocs.yml +++ /dev/null @@ -1,145 +0,0 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/nl/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: nl -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/pl/docs/features.md b/docs/pl/docs/features.md new file mode 100644 index 0000000000..ed10af9bc7 --- /dev/null +++ b/docs/pl/docs/features.md @@ -0,0 +1,200 @@ +# Cechy + +## Cechy FastAPI + +**FastAPI** zapewnia Ci następujące korzyści: + +### Oparcie o standardy open + +* OpenAPI do tworzenia API, w tym deklaracji ścieżek operacji, parametrów, ciał zapytań, bezpieczeństwa, itp. +* Automatyczna dokumentacja modelu danych za pomocą JSON Schema (ponieważ OpenAPI bazuje na JSON Schema). +* Zaprojektowane z myślą o zgodności z powyższymi standardami zamiast dodawania ich obsługi po fakcie. +* Możliwość automatycznego **generowania kodu klienta** w wielu językach. + +### Automatyczna dokumentacja + +Interaktywna dokumentacja i webowe interfejsy do eksploracji API. Z racji tego, że framework bazuje na OpenAPI, istnieje wiele opcji, z czego 2 są domyślnie dołączone. + +* Swagger UI, z interaktywnym interfejsem - odpytuj i testuj swoje API bezpośrednio z przeglądarki. + +![Swagger UI interakcja](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Alternatywna dokumentacja API z ReDoc. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Nowoczesny Python + +Wszystko opiera się na standardowych deklaracjach typu **Python 3.8** (dzięki Pydantic). Brak nowej składni do uczenia. Po prostu standardowy, współczesny Python. + +Jeśli potrzebujesz szybkiego przypomnienia jak używać deklaracji typów w Pythonie (nawet jeśli nie używasz FastAPI), sprawdź krótki samouczek: [Python Types](python-types.md){.internal-link target=_blank}. + +Wystarczy, że napiszesz standardowe deklaracje typów Pythona: + +```Python +from datetime import date + +from pydantic import BaseModel + +# Zadeklaruj parametr jako str +# i uzyskaj wsparcie edytora wewnątrz funkcji +def main(user_id: str): + return user_id + + +# Model Pydantic +class User(BaseModel): + id: int + name: str + joined: date +``` + +A one będą mogły zostać później użyte w następujący sposób: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +!!! info + `**second_user_data` oznacza: + + Przekaż klucze i wartości słownika `second_user_data` bezpośrednio jako argumenty klucz-wartość, co jest równoznaczne z: `User(id=4, name="Mary", joined="2018-11-30")` + +### Wsparcie edytora + +Cały framework został zaprojektowany tak, aby był łatwy i intuicyjny w użyciu. Wszystkie pomysły zostały przetestowane na wielu edytorach jeszcze przed rozpoczęciem procesu tworzenia, aby zapewnić najlepsze wrażenia programistyczne. + +Ostatnia ankieta Python developer survey jasno wskazuje, że najczęściej używaną funkcjonalnością jest autouzupełnianie w edytorze. + +Cała struktura frameworku **FastAPI** jest na tym oparta. Autouzupełnianie działa wszędzie. + +Rzadko będziesz musiał wracać do dokumentacji. + +Oto, jak twój edytor może Ci pomóc: + +* Visual Studio Code: + +![wsparcie edytora](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* PyCharm: + +![wsparcie edytora](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +Otrzymasz uzupełnienie nawet w miejscach, w których normalnie uzupełnienia nie ma. Na przykład klucz "price" w treści JSON (który mógł być zagnieżdżony), który pochodzi z zapytania. + +Koniec z wpisywaniem błędnych nazw kluczy, przechodzeniem tam i z powrotem w dokumentacji lub przewijaniem w górę i w dół, aby sprawdzić, czy w końcu użyłeś nazwy `username` czy `user_name`. + +### Zwięzłość + +Wszystko posiada sensowne **domyślne wartości**. Wszędzie znajdziesz opcjonalne konfiguracje. Wszystkie parametry możesz dostroić, aby zrobić to co potrzebujesz do zdefiniowania API. + +Ale domyślnie wszystko **"po prostu działa"**. + +### Walidacja + +* Walidacja większości (lub wszystkich?) **typów danych** Pythona, w tym: + * Obiektów JSON (`dict`). + * Tablic JSON (`list`) ze zdefiniowanym typem elementów. + * Pól tekstowych (`str`) z określeniem minimalnej i maksymalnej długości. + * Liczb (`int`, `float`) z wartościami minimalnymi, maksymalnymi, itp. + +* Walidacja bardziej egzotycznych typów danych, takich jak: + * URL. + * Email. + * UUID. + * ...i inne. + +Cała walidacja jest obsługiwana przez ugruntowaną i solidną bibliotekę **Pydantic**. + +### Bezpieczeństwo i uwierzytelnianie + +Bezpieczeństwo i uwierzytelnianie jest zintegrowane. Bez żadnych kompromisów z bazami czy modelami danych. + +Wszystkie schematy bezpieczeństwa zdefiniowane w OpenAPI, w tym: + +* Podstawowy protokół HTTP. +* **OAuth2** (również z **tokenami JWT**). Sprawdź samouczek [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* Klucze API w: + * Nagłówkach. + * Parametrach zapytań. + * Ciasteczkach, itp. + +Plus wszystkie funkcje bezpieczeństwa Starlette (włączając w to **ciasteczka sesyjne**). + +Wszystko zbudowane jako narzędzia i komponenty wielokrotnego użytku, które można łatwo zintegrować z systemami, magazynami oraz bazami danych - relacyjnymi, NoSQL, itp. + +### Wstrzykiwanie Zależności + +FastAPI zawiera niezwykle łatwy w użyciu, ale niezwykle potężny system Wstrzykiwania Zależności. + +* Nawet zależności mogą mieć zależności, tworząc hierarchię lub **"graf" zależności**. +* Wszystko jest **obsługiwane automatycznie** przez framework. +* Wszystkie zależności mogą wymagać danych w żądaniach oraz rozszerzać ograniczenia i automatyczną dokumentację **operacji na ścieżce**. +* **Automatyczna walidacja** parametrów *operacji na ścieżce* zdefiniowanych w zależnościach. +* Obsługa złożonych systemów uwierzytelniania użytkowników, **połączeń z bazami danych**, itp. +* Bazy danych, front end, itp. **bez kompromisów**, ale wciąż łatwe do integracji. + +### Nieograniczone "wtyczki" + +Lub ujmując to inaczej - brak potrzeby wtyczek. Importuj i używaj kod, który potrzebujesz. + +Każda integracja została zaprojektowana tak, aby była tak prosta w użyciu (z zależnościami), że możesz utworzyć "wtyczkę" dla swojej aplikacji w 2 liniach kodu, używając tej samej struktury i składni, które są używane w *operacjach na ścieżce*. + +### Testy + +* 100% pokrycia kodu testami. +* 100% adnotacji typów. +* Używany w aplikacjach produkcyjnych. + +## Cechy Starlette + +**FastAPI** jest w pełni kompatybilny z (oraz bazuje na) Starlette. Tak więc każdy dodatkowy kod Starlette, który posiadasz, również będzie działał. + +`FastAPI` jest w rzeczywistości podklasą `Starlette`, więc jeśli już znasz lub używasz Starlette, większość funkcji będzie działać w ten sam sposób. + +Dzięki **FastAPI** otrzymujesz wszystkie funkcje **Starlette** (ponieważ FastAPI to po prostu Starlette na sterydach): + +* Bardzo imponująca wydajność. Jest to jeden z najszybszych dostępnych frameworków Pythona, na równi z **NodeJS** i **Go**. +* Wsparcie dla **WebSocket**. +* Zadania w tle. +* Eventy startup i shutdown. +* Klient testowy zbudowany na bazie biblioteki `requests`. +* **CORS**, GZip, pliki statyczne, streamy. +* Obsługa **sesji i ciasteczek**. +* 100% pokrycie testami. +* 100% adnotacji typów. + +## Cechy Pydantic + +**FastAPI** jest w pełni kompatybilny z (oraz bazuje na) Pydantic. Tak więc każdy dodatkowy kod Pydantic, który posiadasz, również będzie działał. + +Wliczając w to zewnętrzne biblioteki, również oparte o Pydantic, takie jak ORM, ODM dla baz danych. + +Oznacza to, że w wielu przypadkach możesz przekazać ten sam obiekt, który otrzymasz z żądania **bezpośrednio do bazy danych**, ponieważ wszystko jest walidowane automatycznie. + +Działa to również w drugą stronę, w wielu przypadkach możesz po prostu przekazać obiekt otrzymany z bazy danych **bezpośrednio do klienta**. + +Dzięki **FastAPI** otrzymujesz wszystkie funkcje **Pydantic** (ponieważ FastAPI bazuje na Pydantic do obsługi wszystkich danych): + +* **Bez prania mózgu**: + * Brak nowego mikrojęzyka do definiowania schematu, którego trzeba się nauczyć. + * Jeśli znasz adnotacje typów Pythona to wiesz jak używać Pydantic. +* Dobrze współpracuje z Twoim **IDE/linterem/mózgiem**: + * Ponieważ struktury danych Pydantic to po prostu instancje klas, które definiujesz; autouzupełnianie, linting, mypy i twoja intuicja powinny działać poprawnie z Twoimi zwalidowanymi danymi. +* **Szybkość**: + * w benchmarkach Pydantic jest szybszy niż wszystkie inne testowane biblioteki. +* Walidacja **złożonych struktur**: + * Wykorzystanie hierarchicznych modeli Pydantic, Pythonowego modułu `typing` zawierającego `List`, `Dict`, itp. + * Walidatory umożliwiają jasne i łatwe definiowanie, sprawdzanie złożonych struktur danych oraz dokumentowanie ich jako JSON Schema. + * Możesz mieć głęboko **zagnieżdżone obiekty JSON** i wszystkie je poddać walidacji i adnotować. +* **Rozszerzalność**: + * Pydantic umożliwia zdefiniowanie niestandardowych typów danych lub rozszerzenie walidacji o metody na modelu, na których użyty jest dekorator walidatora. +* 100% pokrycie testami. diff --git a/docs/pl/docs/help-fastapi.md b/docs/pl/docs/help-fastapi.md new file mode 100644 index 0000000000..3d02a87410 --- /dev/null +++ b/docs/pl/docs/help-fastapi.md @@ -0,0 +1,263 @@ +# Pomóż FastAPI - Uzyskaj pomoc + +Czy podoba Ci się **FastAPI**? + +Czy chciałbyś pomóc FastAPI, jego użytkownikom i autorowi? + +Może napotkałeś na trudności z **FastAPI** i potrzebujesz pomocy? + +Istnieje kilka bardzo łatwych sposobów, aby pomóc (czasami wystarczy jedno lub dwa kliknięcia). + +Istnieje również kilka sposobów uzyskania pomocy. + +## Zapisz się do newslettera + +Możesz zapisać się do rzadkiego [newslettera o **FastAPI i jego przyjaciołach**](/newsletter/){.internal-link target=_blank}, aby być na bieżąco z: + +* Aktualnościami o FastAPI i przyjaciołach 🚀 +* Przewodnikami 📝 +* Funkcjami ✨ +* Przełomowymi zmianami 🚨 +* Poradami i sztuczkami ✅ + +## Śledź FastAPI na Twitterze + +Śledź @fastapi na **Twitterze** aby być na bieżąco z najnowszymi wiadomościami o **FastAPI**. 🐦 + +## Dodaj gwiazdkę **FastAPI** na GitHubie + +Możesz "dodać gwiazdkę" FastAPI na GitHubie (klikając przycisk gwiazdki w prawym górnym rogu): https://github.com/tiangolo/fastapi. ⭐️ + +Dodając gwiazdkę, inni użytkownicy będą mogli łatwiej znaleźć projekt i zobaczyć, że był już przydatny dla innych. + +## Obserwuj repozytorium GitHub w poszukiwaniu nowych wydań + +Możesz "obserwować" FastAPI na GitHubie (klikając przycisk "obserwuj" w prawym górnym rogu): https://github.com/tiangolo/fastapi. 👀 + +Wybierz opcję "Tylko wydania". + +Dzięki temu będziesz otrzymywać powiadomienia (na swój adres e-mail) za każdym razem, gdy pojawi się nowe wydanie (nowa wersja) **FastAPI** z poprawkami błędów i nowymi funkcjami. + +## Skontaktuj się z autorem + +Możesz skontaktować się ze mną (Sebastián Ramírez / `tiangolo`), autorem. + +Możesz: + +* Śledzić mnie na **GitHubie**. + * Zobacz inne projekty open source, które stworzyłem, a mogą być dla Ciebie pomocne. + * Śledź mnie, aby dostać powiadomienie, gdy utworzę nowy projekt open source. +* Śledzić mnie na **Twitterze** lub na Mastodonie. + * Napisz mi, w jaki sposób korzystasz z FastAPI (uwielbiam o tym czytać). + * Dowiedz się, gdy ogłoszę coś nowego lub wypuszczę nowe narzędzia. + * Możesz także śledzić @fastapi na Twitterze (to oddzielne konto). +* Nawiąż ze mną kontakt na **Linkedinie**. + * Dowiedz się, gdy ogłoszę coś nowego lub wypuszczę nowe narzędzia (chociaż częściej korzystam z Twittera 🤷‍♂). +* Czytaj moje posty (lub śledź mnie) na **Dev.to** lub na **Medium**. + * Czytaj o innych pomysłach, artykułach i dowiedz się o narzędziach, które stworzyłem. + * Śledź mnie, by wiedzieć gdy opublikuję coś nowego. + +## Napisz tweeta o **FastAPI** + +Napisz tweeta o **FastAPI** i powiedz czemu Ci się podoba. 🎉 + +Uwielbiam czytać w jaki sposób **FastAPI** jest używane, co Ci się w nim podobało, w jakim projekcie/firmie go używasz itp. + +## Głosuj na FastAPI + +* Głosuj na **FastAPI** w Slant. +* Głosuj na **FastAPI** w AlternativeTo. +* Powiedz, że używasz **FastAPI** na StackShare. + +## Pomagaj innym, odpowiadając na ich pytania na GitHubie + +Możesz spróbować pomóc innym, odpowiadając w: + +* Dyskusjach na GitHubie +* Problemach na GitHubie + +W wielu przypadkach możesz już znać odpowiedź na te pytania. 🤓 + +Jeśli pomożesz wielu ludziom, możesz zostać oficjalnym [Ekspertem FastAPI](fastapi-people.md#experts){.internal-link target=_blank}. 🎉 + +Pamiętaj tylko o najważniejszym: bądź życzliwy. Ludzie przychodzą sfrustrowani i w wielu przypadkach nie zadają pytań w najlepszy sposób, ale mimo to postaraj się być dla nich jak najbardziej życzliwy. 🤗 + +Chciałbym, by społeczność **FastAPI** była życzliwa i przyjazna. Nie akceptuj prześladowania ani braku szacunku wobec innych. Dbajmy o siebie nawzajem. + +--- + +Oto, jak pomóc innym z pytaniami (w dyskusjach lub problemach): + +### Zrozum pytanie + +* Upewnij się, czy rozumiesz **cel** i przypadek użycia osoby pytającej. + +* Następnie sprawdź, czy pytanie (większość to pytania) jest **jasne**. + +* W wielu przypadkach zadane pytanie dotyczy rozwiązania wymyślonego przez użytkownika, ale może istnieć **lepsze** rozwiązanie. Jeśli dokładnie zrozumiesz problem i przypadek użycia, być może będziesz mógł zaproponować lepsze **alternatywne rozwiązanie**. + +* Jeśli nie rozumiesz pytania, poproś o więcej **szczegółów**. + +### Odtwórz problem + +W większości przypadków problem wynika z **autorskiego kodu** osoby pytającej. + +Często pytający umieszczają tylko fragment kodu, niewystarczający do **odtworzenia problemu**. + +* Możesz poprosić ich o dostarczenie minimalnego, odtwarzalnego przykładu, który możesz **skopiować i wkleić** i uruchomić lokalnie, aby zobaczyć ten sam błąd lub zachowanie, które widzą, lub lepiej zrozumieć ich przypadki użycia. + +* Jeśli jesteś wyjątkowo pomocny, możesz spróbować **stworzyć taki przykład** samodzielnie, opierając się tylko na opisie problemu. Miej na uwadze, że może to zająć dużo czasu i lepiej może być najpierw poprosić ich o wyjaśnienie problemu. + +### Proponuj rozwiązania + +* Po zrozumieniu pytania możesz podać im możliwą **odpowiedź**. + +* W wielu przypadkach lepiej zrozumieć ich **podstawowy problem lub przypadek użycia**, ponieważ może istnieć lepszy sposób rozwiązania niż to, co próbują zrobić. + +### Poproś o zamknięcie + +Jeśli odpowiedzą, jest duża szansa, że rozwiązałeś ich problem, gratulacje, **jesteś bohaterem**! 🦸 + +* Jeśli Twoja odpowiedź rozwiązała problem, możesz poprosić o: + + * W Dyskusjach na GitHubie: oznaczenie komentarza jako **odpowiedź**. + * W Problemach na GitHubie: **zamknięcie** problemu. + +## Obserwuj repozytorium na GitHubie + +Możesz "obserwować" FastAPI na GitHubie (klikając przycisk "obserwuj" w prawym górnym rogu): https://github.com/tiangolo/fastapi. 👀 + +Jeśli wybierzesz "Obserwuj" zamiast "Tylko wydania", otrzymasz powiadomienia, gdy ktoś utworzy nowy problem lub pytanie. Możesz również określić, że chcesz być powiadamiany tylko o nowych problemach, dyskusjach, PR-ach itp. + +Następnie możesz spróbować pomóc rozwiązać te problemy. + +## Zadawaj pytania + +Możesz utworzyć nowe pytanie w repozytorium na GitHubie, na przykład aby: + +* Zadać **pytanie** lub zapytać o **problem**. +* Zaproponować nową **funkcję**. + +**Uwaga**: jeśli to zrobisz, poproszę Cię również o pomoc innym. 😉 + +## Przeglądaj Pull Requesty + +Możesz pomóc mi w przeglądaniu pull requestów autorstwa innych osób. + +Jak wcześniej wspomniałem, postaraj się być jak najbardziej życzliwy. 🤗 + +--- + +Oto, co warto mieć na uwadze podczas oceny pull requestu: + +### Zrozum problem + +* Najpierw upewnij się, że **rozumiesz problem**, który próbuje rozwiązać pull request. Może być osadzony w większym kontekście w GitHubowej dyskusji lub problemie. + +* Jest też duża szansa, że pull request nie jest konieczny, ponieważ problem można rozwiązać w **inny sposób**. Wtedy możesz to zasugerować lub o to zapytać. + +### Nie martw się stylem + +* Nie przejmuj się zbytnio rzeczami takimi jak style wiadomości commitów, przy wcielaniu pull requesta łączę commity i modyfikuję opis sumarycznego commita ręcznie. + +* Nie przejmuj się również stylem kodu, automatyczne narzędzia w repozytorium sprawdzają to samodzielnie. + +A jeśli istnieje jakaś konkretna potrzeba dotycząca stylu lub spójności, sam poproszę o zmiany lub dodam commity z takimi zmianami. + +### Sprawdź kod + +* Przeczytaj kod, zastanów się czy ma sens, **uruchom go lokalnie** i potwierdź czy faktycznie rozwiązuje problem. + +* Następnie dodaj **komentarz** z informacją o tym, że sprawdziłeś kod, dzięki temu będę miał pewność, że faktycznie go sprawdziłeś. + +!!! info + Niestety, nie mogę ślepo ufać PR-om, nawet jeśli mają kilka zatwierdzeń. + + Kilka razy zdarzyło się, że PR-y miały 3, 5 lub więcej zatwierdzeń (prawdopodobnie dlatego, że opis obiecuje rozwiązanie ważnego problemu), ale gdy sam sprawdziłem danego PR-a, okazał się być zbugowany lub nie rozwiązywał problemu, który rzekomo miał rozwiązywać. 😅 + + Dlatego tak ważne jest, abyś faktycznie przeczytał i uruchomił kod oraz napisał w komentarzu, że to zrobiłeś. 🤓 + +* Jeśli PR można uprościć w jakiś sposób, możesz o to poprosić, ale nie ma potrzeby być zbyt wybrednym, może być wiele subiektywnych punktów widzenia (a ja też będę miał swój 🙈), więc lepiej żebyś skupił się na kluczowych rzeczach. + +### Testy + +* Pomóż mi sprawdzić, czy PR ma **testy**. + +* Sprawdź, czy testy **nie przechodzą** przed PR. 🚨 + +* Następnie sprawdź, czy testy **przechodzą** po PR. ✅ + +* Wiele PR-ów nie ma testów, możesz **przypomnieć** im o dodaniu testów, a nawet **zaproponować** samemu jakieś testy. To jedna z rzeczy, które pochłaniają najwięcej czasu i możesz w tym bardzo pomóc. + +* Następnie skomentuj również to, czego spróbowałeś, wtedy będę wiedział, że to sprawdziłeś. 🤓 + +## Utwórz Pull Request + +Możesz [wnieść wkład](contributing.md){.internal-link target=_blank} do kodu źródłowego za pomocą Pull Requestu, na przykład: + +* Naprawić literówkę, którą znalazłeś w dokumentacji. +* Podzielić się artykułem, filmem lub podcastem, który stworzyłeś lub znalazłeś na temat FastAPI, edytując ten plik. + * Upewnij się, że dodajesz swój link na początku odpowiedniej sekcji. +* Pomóc w [tłumaczeniu dokumentacji](contributing.md#translations){.internal-link target=_blank} na Twój język. + * Możesz również pomóc w weryfikacji tłumaczeń stworzonych przez innych. +* Zaproponować nowe sekcje dokumentacji. +* Naprawić istniejący problem/błąd. + * Upewnij się, że dodajesz testy. +* Dodać nową funkcję. + * Upewnij się, że dodajesz testy. + * Upewnij się, że dodajesz dokumentację, jeśli jest to istotne. + +## Pomóż w utrzymaniu FastAPI + +Pomóż mi utrzymać **FastAPI**! 🤓 + +Jest wiele pracy do zrobienia, a w większości przypadków **TY** możesz to zrobić. + +Główne zadania, które możesz wykonać teraz to: + +* [Pomóc innym z pytaniami na GitHubie](#help-others-with-questions-in-github){.internal-link target=_blank} (zobacz sekcję powyżej). +* [Oceniać Pull Requesty](#review-pull-requests){.internal-link target=_blank} (zobacz sekcję powyżej). + +Te dwie czynności **zajmują najwięcej czasu**. To główna praca związana z utrzymaniem FastAPI. + +Jeśli możesz mi w tym pomóc, **pomożesz mi utrzymać FastAPI** i zapewnisz że będzie **rozwijać się szybciej i lepiej**. 🚀 + +## Dołącz do czatu + +Dołącz do 👥 serwera czatu na Discordzie 👥 i spędzaj czas z innymi w społeczności FastAPI. + +!!! wskazówka + Jeśli masz pytania, zadaj je w Dyskusjach na GitHubie, jest dużo większa szansa, że otrzymasz pomoc od [Ekspertów FastAPI](fastapi-people.md#experts){.internal-link target=_blank}. + + Używaj czatu tylko do innych ogólnych rozmów. + +### Nie zadawaj pytań na czacie + +Miej na uwadze, że ponieważ czaty pozwalają na bardziej "swobodną rozmowę", łatwo jest zadawać pytania, które są zbyt ogólne i trudniejsze do odpowiedzi, więc możesz nie otrzymać odpowiedzi. + +Na GitHubie szablon poprowadzi Cię do napisania odpowiedniego pytania, dzięki czemu łatwiej uzyskasz dobrą odpowiedź, a nawet rozwiążesz problem samodzielnie, zanim zapytasz. Ponadto na GitHubie mogę się upewnić, że zawsze odpowiadam na wszystko, nawet jeśli zajmuje to trochę czasu. Osobiście nie mogę tego zrobić z systemami czatu. 😅 + +Rozmów w systemach czatu nie można tak łatwo przeszukiwać, jak na GitHubie, więc pytania i odpowiedzi mogą zaginąć w rozmowie. A tylko te na GitHubie liczą się do zostania [Ekspertem FastAPI](fastapi-people.md#experts){.internal-link target=_blank}, więc najprawdopodobniej otrzymasz więcej uwagi na GitHubie. + +Z drugiej strony w systemach czatu są tysiące użytkowników, więc jest duża szansa, że znajdziesz tam kogoś do rozmowy, prawie w każdej chwili. 😄 + +## Wspieraj autora + +Możesz również finansowo wesprzeć autora (mnie) poprzez sponsoring na GitHubie. + +Tam możesz postawić mi kawę ☕️ aby podziękować. 😄 + +Możesz także zostać srebrnym lub złotym sponsorem FastAPI. 🏅🎉 + +## Wspieraj narzędzia, które napędzają FastAPI + +Jak widziałeś w dokumentacji, FastAPI stoi na ramionach gigantów, Starlette i Pydantic. + +Możesz również wesprzeć: + +* Samuel Colvin (Pydantic) +* Encode (Starlette, Uvicorn) + +--- + +Dziękuję! 🚀 diff --git a/docs/pl/docs/index.md b/docs/pl/docs/index.md index 98e1e82fc2..49f5c2b011 100644 --- a/docs/pl/docs/index.md +++ b/docs/pl/docs/index.md @@ -24,7 +24,7 @@ --- -FastAPI to nowoczesny, wydajny framework webowy do budowania API z użyciem Pythona 3.6+ bazujący na standardowym typowaniu Pythona. +FastAPI to nowoczesny, wydajny framework webowy do budowania API z użyciem Pythona 3.8+ bazujący na standardowym typowaniu Pythona. Kluczowe cechy: @@ -106,7 +106,7 @@ Jeżeli tworzysz aplikacje CLI< ## Wymagania -Python 3.7+ +Python 3.8+ FastAPI oparty jest na: @@ -321,7 +321,7 @@ Robisz to tak samo jak ze standardowymi typami w Pythonie. Nie musisz sie uczyć żadnej nowej składni, metod lub klas ze specyficznych bibliotek itp. -Po prostu standardowy **Python 3.6+**. +Po prostu standardowy **Python 3.8+**. Na przykład, dla danych typu `int`: @@ -435,7 +435,6 @@ Aby dowiedzieć się o tym więcej, zobacz sekcję ujson - dla szybszego "parsowania" danych JSON. * email_validator - dla walidacji adresów email. Używane przez Starlette: diff --git a/docs/pl/docs/tutorial/index.md b/docs/pl/docs/tutorial/index.md index ed8752a95a..f8c5c60227 100644 --- a/docs/pl/docs/tutorial/index.md +++ b/docs/pl/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Samouczek - Wprowadzenie +# Samouczek Ten samouczek pokaże Ci, krok po kroku, jak używać większości funkcji **FastAPI**. diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 1cd1294208..de18856f44 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -1,148 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/pl/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: pl -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- Samouczek: - - tutorial/index.md - - tutorial/first-steps.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs/pt/docs/advanced/events.md b/docs/pt/docs/advanced/events.md new file mode 100644 index 0000000000..7f6cb6f5d4 --- /dev/null +++ b/docs/pt/docs/advanced/events.md @@ -0,0 +1,163 @@ +# Eventos de vida útil + +Você pode definir a lógica (código) que poderia ser executada antes da aplicação **inicializar**. Isso significa que esse código será executado **uma vez**, **antes** da aplicação **começar a receber requisições**. + +Do mesmo modo, você pode definir a lógica (código) que será executada quando a aplicação estiver sendo **encerrada**. Nesse caso, este código será executado **uma vez**, **depois** de ter possivelmente tratado **várias requisições**. + +Por conta desse código ser executado antes da aplicação **começar** a receber requisições, e logo após **terminar** de lidar com as requisições, ele cobre toda a **vida útil** (_lifespan_) da aplicação (o termo "vida útil" será importante em um segundo 😉). + +Pode ser muito útil para configurar **recursos** que você precisa usar por toda aplicação, e que são **compartilhados** entre as requisições, e/ou que você precisa **limpar** depois. Por exemplo, o pool de uma conexão com o banco de dados ou carregamento de um modelo compartilhado de aprendizado de máquina (_machine learning_). + +## Caso de uso + +Vamos iniciar com um exemplo de **caso de uso** e então ver como resolvê-lo com isso. + +Vamos imaginar que você tem alguns **modelos de _machine learning_** que deseja usar para lidar com as requisições. 🤖 + +Os mesmos modelos são compartilhados entre as requisições, então não é um modelo por requisição, ou um por usuário ou algo parecido. + +Vamos imaginar que o carregamento do modelo pode **demorar bastante tempo**, porque ele tem que ler muitos **dados do disco**. Então você não quer fazer isso a cada requisição. + +Você poderia carregá-lo no nível mais alto do módulo/arquivo, mas isso também poderia significaria **carregar o modelo** mesmo se você estiver executando um simples teste automatizado, então esse teste poderia ser **lento** porque teria que esperar o carregamento do modelo antes de ser capaz de executar uma parte independente do código. + + +Isso é que nós iremos resolver, vamos carregar o modelo antes das requisições serem manuseadas, mas apenas um pouco antes da aplicação começar a receber requisições, não enquanto o código estiver sendo carregado. + +## Vida útil (_Lifespan_) + +Você pode definir essa lógica de *inicialização* e *encerramento* usando os parâmetros de `lifespan` da aplicação `FastAPI`, e um "gerenciador de contexto" (te mostrarei o que é isso a seguir). + +Vamos iniciar com um exemplo e ver isso detalhadamente. + +Nós criamos uma função assíncrona chamada `lifespan()` com `yield` como este: + +```Python hl_lines="16 19" +{!../../../docs_src/events/tutorial003.py!} +``` + +Aqui nós estamos simulando a *inicialização* custosa do carregamento do modelo colocando a (falsa) função de modelo no dicionário com modelos de _machine learning_ antes do `yield`. Este código será executado **antes** da aplicação **começar a receber requisições**, durante a *inicialização*. + +E então, logo após o `yield`, descarregaremos o modelo. Esse código será executado **após** a aplicação **terminar de lidar com as requisições**, pouco antes do *encerramento*. Isso poderia, por exemplo, liberar recursos como memória ou GPU. + +!!! tip "Dica" + O `shutdown` aconteceria quando você estivesse **encerrando** a aplicação. + + Talvez você precise inicializar uma nova versão, ou apenas cansou de executá-la. 🤷 + +### Função _lifespan_ + +A primeira coisa a notar, é que estamos definindo uma função assíncrona com `yield`. Isso é muito semelhante à Dependências com `yield`. + +```Python hl_lines="14-19" +{!../../../docs_src/events/tutorial003.py!} +``` + +A primeira parte da função, antes do `yield`, será executada **antes** da aplicação inicializar. + +E a parte posterior do `yield` irá executar **após** a aplicação ser encerrada. + +### Gerenciador de Contexto Assíncrono + +Se você verificar, a função está decorada com um `@asynccontextmanager`. + +Que converte a função em algo chamado de "**Gerenciador de Contexto Assíncrono**". + +```Python hl_lines="1 13" +{!../../../docs_src/events/tutorial003.py!} +``` + +Um **gerenciador de contexto** em Python é algo que você pode usar em uma declaração `with`, por exemplo, `open()` pode ser usado como um gerenciador de contexto: + +```Python +with open("file.txt") as file: + file.read() +``` + +Nas versões mais recentes de Python, há também um **gerenciador de contexto assíncrono**. Você o usaria com `async with`: + +```Python +async with lifespan(app): + await do_stuff() +``` + +Quando você cria um gerenciador de contexto ou um gerenciador de contexto assíncrono como mencionado acima, o que ele faz é que, antes de entrar no bloco `with`, ele irá executar o código anterior ao `yield`, e depois de sair do bloco `with`, ele irá executar o código depois do `yield`. + +No nosso exemplo de código acima, nós não usamos ele diretamente, mas nós passamos para o FastAPI para ele usá-lo. + +O parâmetro `lifespan` da aplicação `FastAPI` usa um **Gerenciador de Contexto Assíncrono**, então nós podemos passar nosso novo gerenciador de contexto assíncrono do `lifespan` para ele. + +```Python hl_lines="22" +{!../../../docs_src/events/tutorial003.py!} +``` + +## Eventos alternativos (deprecados) + +!!! warning "Aviso" + A maneira recomendada para lidar com a *inicialização* e o *encerramento* é usando o parâmetro `lifespan` da aplicação `FastAPI` como descrito acima. + + Você provavelmente pode pular essa parte. + +Existe uma forma alternativa para definir a execução dessa lógica durante *inicialização* e durante *encerramento*. + +Você pode definir manipuladores de eventos (funções) que precisam ser executadas antes da aplicação inicializar, ou quando a aplicação estiver encerrando. + +Essas funções podem ser declaradas com `async def` ou `def` normal. + +### Evento `startup` + +Para adicionar uma função que deve rodar antes da aplicação iniciar, declare-a com o evento `"startup"`: + +```Python hl_lines="8" +{!../../../docs_src/events/tutorial001.py!} +``` + +Nesse caso, a função de manipulação de evento `startup` irá inicializar os itens do "banco de dados" (só um `dict`) com alguns valores. + +Você pode adicionar mais que uma função de manipulação de evento. + +E sua aplicação não irá começar a receber requisições até que todos os manipuladores de eventos de `startup` sejam concluídos. + +### Evento `shutdown` + +Para adicionar uma função que deve ser executada quando a aplicação estiver encerrando, declare ela com o evento `"shutdown"`: + +```Python hl_lines="6" +{!../../../docs_src/events/tutorial002.py!} +``` + +Aqui, a função de manipulação de evento `shutdown` irá escrever uma linha de texto `"Application shutdown"` no arquivo `log.txt`. + +!!! info "Informação" + Na função `open()`, o `mode="a"` significa "acrescentar", então, a linha irá ser adicionada depois de qualquer coisa que esteja naquele arquivo, sem sobrescrever o conteúdo anterior. + +!!! tip "Dica" + Perceba que nesse caso nós estamos usando a função padrão do Python `open()` que interage com um arquivo. + + Então, isso envolve I/O (input/output), que exige "esperar" que coisas sejam escritas em disco. + + Mas `open()` não usa `async` e `await`. + + Então, nós declaramos uma função de manipulação de evento com o padrão `def` ao invés de `async def`. + +### `startup` e `shutdown` juntos + +Há uma grande chance que a lógica para sua *inicialização* e *encerramento* esteja conectada, você pode querer iniciar alguma coisa e então finalizá-la, adquirir um recurso e então liberá-lo, etc. + +Fazendo isso em funções separadas que não compartilham lógica ou variáveis entre elas é mais difícil já que você precisa armazenar os valores em variáveis globais ou truques parecidos. + +Por causa disso, agora é recomendado em vez disso usar o `lifespan` como explicado acima. + +## Detalhes técnicos + +Só um detalhe técnico para nerds curiosos. 🤓 + +Por baixo, na especificação técnica ASGI, essa é a parte do Protocolo Lifespan, e define eventos chamados `startup` e `shutdown`. + +!!! info "Informação" + Você pode ler mais sobre o manipulador `lifespan` do Starlette na Documentação do Lifespan Starlette. + + Incluindo como manipular estado do lifespan que pode ser usado em outras áreas do seu código. + +## Sub Aplicações + +🚨 Tenha em mente que esses eventos de lifespan (de inicialização e desligamento) irão somente ser executados para a aplicação principal, não para [Sub Aplicações - Montagem](./sub-applications.md){.internal-link target=_blank}. diff --git a/docs/pt/docs/advanced/index.md b/docs/pt/docs/advanced/index.md index d1a57c6d17..7e276f732a 100644 --- a/docs/pt/docs/advanced/index.md +++ b/docs/pt/docs/advanced/index.md @@ -1,4 +1,4 @@ -# Guia de Usuário Avançado - Introdução +# Guia de Usuário Avançado ## Recursos Adicionais diff --git a/docs/pt/docs/contributing.md b/docs/pt/docs/contributing.md index f95b6f4ecc..02895fcfc8 100644 --- a/docs/pt/docs/contributing.md +++ b/docs/pt/docs/contributing.md @@ -98,7 +98,7 @@ Após ativar o ambiente como descrito acima:
```console -$ pip install -e ."[dev,doc,test]" +$ pip install -r requirements.txt ---> 100% ``` diff --git a/docs/pt/docs/deployment/deta.md b/docs/pt/docs/deployment/deta.md deleted file mode 100644 index 9271bba42e..0000000000 --- a/docs/pt/docs/deployment/deta.md +++ /dev/null @@ -1,258 +0,0 @@ -# Implantação FastAPI na Deta - -Nessa seção você aprenderá sobre como realizar a implantação de uma aplicação **FastAPI** na Deta utilizando o plano gratuito. 🎁 - -Isso tudo levará aproximadamente **10 minutos**. - -!!! info "Informação" - Deta é uma patrocinadora do **FastAPI**. 🎉 - -## Uma aplicação **FastAPI** simples - -* Crie e entre em um diretório para a sua aplicação, por exemplo, `./fastapideta/`. - -### Código FastAPI - -* Crie o arquivo `main.py` com: - -```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): - return {"item_id": item_id} -``` - -### Requisitos - -Agora, no mesmo diretório crie o arquivo `requirements.txt` com: - -```text -fastapi -``` - -!!! tip "Dica" - Você não precisa instalar Uvicorn para realizar a implantação na Deta, embora provavelmente queira instalá-lo para testar seu aplicativo localmente. - -### Estrutura de diretório - -Agora você terá o diretório `./fastapideta/` com dois arquivos: - -``` -. -└── main.py -└── requirements.txt -``` - -## Crie uma conta gratuita na Deta - -Agora crie uma conta gratuita na Deta, você precisará apenas de um email e senha. - -Você nem precisa de um cartão de crédito. - -## Instale a CLI - -Depois de ter sua conta criada, instale Deta CLI: - -=== "Linux, macOS" - -
- - ```console - $ curl -fsSL https://get.deta.dev/cli.sh | sh - ``` - -
- -=== "Windows PowerShell" - -
- - ```console - $ iwr https://get.deta.dev/cli.ps1 -useb | iex - ``` - -
- -Após a instalação, abra um novo terminal para que a CLI seja detectada. - -Em um novo terminal, confirme se foi instalado corretamente com: - -
- -```console -$ deta --help - -Deta command line interface for managing deta micros. -Complete documentation available at https://docs.deta.sh - -Usage: - deta [flags] - deta [command] - -Available Commands: - auth Change auth settings for a deta micro - -... -``` - -
- -!!! tip "Dica" - Se você tiver problemas ao instalar a CLI, verifique a documentação oficial da Deta. - -## Login pela CLI - -Agora faça login na Deta pela CLI com: - -
- -```console -$ deta login - -Please, log in from the web page. Waiting.. -Logged in successfully. -``` - -
- -Isso abrirá um navegador da Web e autenticará automaticamente. - -## Implantação com Deta - -Em seguida, implante seu aplicativo com a Deta CLI: - -
- -```console -$ deta new - -Successfully created a new micro - -// Notice the "endpoint" 🔍 - -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} - -Adding dependencies... - - ----> 100% - - -Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6 -``` - -
- -Você verá uma mensagem JSON semelhante a: - -```JSON hl_lines="4" -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} -``` - -!!! tip "Dica" - Sua implantação terá um URL `"endpoint"` diferente. - -## Confira - -Agora, abra seu navegador na URL do `endpoint`. No exemplo acima foi `https://qltnci.deta.dev`, mas o seu será diferente. - -Você verá a resposta JSON do seu aplicativo FastAPI: - -```JSON -{ - "Hello": "World" -} -``` - -Agora vá para o `/docs` da sua API, no exemplo acima seria `https://qltnci.deta.dev/docs`. - -Ele mostrará sua documentação como: - - - -## Permitir acesso público - -Por padrão, a Deta lidará com a autenticação usando cookies para sua conta. - -Mas quando estiver pronto, você pode torná-lo público com: - -
- -```console -$ deta auth disable - -Successfully disabled http auth -``` - -
- -Agora você pode compartilhar essa URL com qualquer pessoa e elas conseguirão acessar sua API. 🚀 - -## HTTPS - -Parabéns! Você realizou a implantação do seu app FastAPI na Deta! 🎉 🍰 - -Além disso, observe que a Deta lida corretamente com HTTPS para você, para que você não precise cuidar disso e tenha a certeza de que seus clientes terão uma conexão criptografada segura. ✅ 🔒 - -## Verifique o Visor - -Na UI da sua documentação (você estará em um URL como `https://qltnci.deta.dev/docs`) envie um request para *operação de rota* `/items/{item_id}`. - -Por exemplo com ID `5`. - -Agora vá para https://web.deta.sh. - -Você verá que há uma seção à esquerda chamada "Micros" com cada um dos seus apps. - -Você verá uma aba com "Detalhes", e também a aba "Visor", vá para "Visor". - -Lá você pode inspecionar as solicitações recentes enviadas ao seu aplicativo. - -Você também pode editá-los e reproduzi-los novamente. - - - -## Saiba mais - -Em algum momento, você provavelmente desejará armazenar alguns dados para seu aplicativo de uma forma que persista ao longo do tempo. Para isso você pode usar Deta Base, que também tem um generoso **nível gratuito**. - -Você também pode ler mais na documentação da Deta. - -## Conceitos de implantação - -Voltando aos conceitos que discutimos em [Deployments Concepts](./concepts.md){.internal-link target=_blank}, veja como cada um deles seria tratado com a Deta: - -* **HTTPS**: Realizado pela Deta, eles fornecerão um subdomínio e lidarão com HTTPS automaticamente. -* **Executando na inicialização**: Realizado pela Deta, como parte de seu serviço. -* **Reinicialização**: Realizado pela Deta, como parte de seu serviço. -* **Replicação**: Realizado pela Deta, como parte de seu serviço. -* **Memória**: Limite predefinido pela Deta, você pode contatá-los para aumentá-lo. -* **Etapas anteriores a inicialização**: Não suportado diretamente, você pode fazê-lo funcionar com o sistema Cron ou scripts adicionais. - -!!! note "Nota" - O Deta foi projetado para facilitar (e gratuitamente) a implantação rápida de aplicativos simples. - - Ele pode simplificar vários casos de uso, mas, ao mesmo tempo, não suporta outros, como o uso de bancos de dados externos (além do próprio sistema de banco de dados NoSQL da Deta), máquinas virtuais personalizadas, etc. - - Você pode ler mais detalhes na documentação da Deta para ver se é a escolha certa para você. diff --git a/docs/pt/docs/deployment/index.md b/docs/pt/docs/deployment/index.md index 1ff0e44a09..6b4290d1d7 100644 --- a/docs/pt/docs/deployment/index.md +++ b/docs/pt/docs/deployment/index.md @@ -1,4 +1,4 @@ -# Implantação - Introdução +# Implantação A implantação de uma aplicação **FastAPI** é relativamente simples. diff --git a/docs/pt/docs/external-links.md b/docs/pt/docs/external-links.md index 6ec6c3a27c..77ec323511 100644 --- a/docs/pt/docs/external-links.md +++ b/docs/pt/docs/external-links.md @@ -9,70 +9,21 @@ Aqui tem uma lista, incompleta, de algumas delas. !!! tip "Dica" Se você tem um artigo, projeto, ferramenta ou qualquer coisa relacionada ao **FastAPI** que ainda não está listada aqui, crie um _Pull Request_ adicionando ele. -## Artigos +{% for section_name, section_content in external_links.items() %} -### Inglês +## {{ section_name }} -{% if external_links %} -{% for article in external_links.articles.english %} +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* {{ item.title }} by {{ item.author }}. -* {{ article.title }} by {{ article.author }}. {% endfor %} -{% endif %} - -### Japonês - -{% if external_links %} -{% for article in external_links.articles.japanese %} - -* {{ article.title }} by {{ article.author }}. {% endfor %} -{% endif %} - -### Vietnamita - -{% if external_links %} -{% for article in external_links.articles.vietnamese %} - -* {{ article.title }} by {{ article.author }}. {% endfor %} -{% endif %} - -### Russo - -{% if external_links %} -{% for article in external_links.articles.russian %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} - -### Alemão - -{% if external_links %} -{% for article in external_links.articles.german %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} - -## Podcasts - -{% if external_links %} -{% for article in external_links.podcasts.english %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} - -## Palestras - -{% if external_links %} -{% for article in external_links.talks.english %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} ## Projetos diff --git a/docs/pt/docs/features.md b/docs/pt/docs/features.md index bd0db8e762..822992c5b9 100644 --- a/docs/pt/docs/features.md +++ b/docs/pt/docs/features.md @@ -25,7 +25,7 @@ Documentação interativa da API e navegação _web_ da interface de usuário. C ### Apenas Python moderno -Tudo é baseado no padrão das declarações de **tipos do Python 3.6** (graças ao Pydantic). Nenhuma sintaxe nova para aprender. Apenas o padrão moderno do Python. +Tudo é baseado no padrão das declarações de **tipos do Python 3.8** (graças ao Pydantic). Nenhuma sintaxe nova para aprender. Apenas o padrão moderno do Python. Se você precisa refrescar a memória rapidamente sobre como usar tipos do Python (mesmo que você não use o FastAPI), confira esse rápido tutorial: [Tipos do Python](python-types.md){.internal-link target=_blank}. diff --git a/docs/pt/docs/help-fastapi.md b/docs/pt/docs/help-fastapi.md index d82ce3414a..d04905197d 100644 --- a/docs/pt/docs/help-fastapi.md +++ b/docs/pt/docs/help-fastapi.md @@ -114,8 +114,6 @@ do FastAPI. Use o chat apenas para outro tipo de assunto. -Também existe o chat do Gitter, porém ele não possuí canais e recursos avançados, conversas são mais engessadas, por isso o Discord é mais recomendado. - ### Não faça perguntas no chat Tenha em mente que os chats permitem uma "conversa mais livre", dessa forma é muito fácil fazer perguntas que são muito genéricas e dificeís de responder, assim você pode acabar não sendo respondido. diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index afc101edea..d1e64b3b90 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -24,7 +24,7 @@ --- -FastAPI é um moderno e rápido (alta performance) _framework web_ para construção de APIs com Python 3.6 ou superior, baseado nos _type hints_ padrões do Python. +FastAPI é um moderno e rápido (alta performance) _framework web_ para construção de APIs com Python 3.8 ou superior, baseado nos _type hints_ padrões do Python. Os recursos chave são: @@ -100,7 +100,7 @@ Se você estiver construindo uma aplicação ujson - para JSON mais rápido "parsing". * email_validator - para validação de email. Usados por Starlette: diff --git a/docs/pt/docs/tutorial/body-multiple-params.md b/docs/pt/docs/tutorial/body-multiple-params.md index ac67aa47ff..0eaa9664c0 100644 --- a/docs/pt/docs/tutorial/body-multiple-params.md +++ b/docs/pt/docs/tutorial/body-multiple-params.md @@ -8,18 +8,18 @@ Primeiro, é claro, você pode misturar `Path`, `Query` e declarações de parâ E você também pode declarar parâmetros de corpo como opcionais, definindo o valor padrão com `None`: -=== "Python 3.6 e superiores" - - ```Python hl_lines="19-21" - {!> ../../../docs_src/body_multiple_params/tutorial001.py!} - ``` - -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="17-19" {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + !!! nota Repare que, neste caso, o `item` que seria capturado a partir do corpo é opcional. Visto que ele possui `None` como valor padrão. @@ -38,18 +38,18 @@ No exemplo anterior, as *operações de rota* esperariam um JSON no corpo conten Mas você pode também declarar múltiplos parâmetros de corpo, por exemplo, `item` e `user`: -=== "Python 3.6 e superiores" - - ```Python hl_lines="22" - {!> ../../../docs_src/body_multiple_params/tutorial002.py!} - ``` - -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="20" {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` + Neste caso, o **FastAPI** perceberá que existe mais de um parâmetro de corpo na função (dois parâmetros que são modelos Pydantic). Então, ele usará o nome dos parâmetros como chaves (nome dos campos) no corpo, e espera um corpo como: @@ -87,13 +87,13 @@ Se você declará-lo como é, porque é um valor singular, o **FastAPI** assumir Mas você pode instruir o **FastAPI** para tratá-lo como outra chave do corpo usando `Body`: -=== "Python 3.6 e superiores" +=== "Python 3.8+" ```Python hl_lines="22" {!> ../../../docs_src/body_multiple_params/tutorial003.py!} ``` -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="20" {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} @@ -137,18 +137,18 @@ q: str | None = None Por exemplo: -=== "Python 3.6 e superiores" - - ```Python hl_lines="27" - {!> ../../../docs_src/body_multiple_params/tutorial004.py!} - ``` - -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="26" {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + !!! info "Informação" `Body` também possui todas as validações adicionais e metadados de parâmetros como em `Query`,`Path` e outras que você verá depois. @@ -166,18 +166,18 @@ item: Item = Body(embed=True) como em: -=== "Python 3.6 e superiores" - - ```Python hl_lines="17" - {!> ../../../docs_src/body_multiple_params/tutorial005.py!} - ``` - -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="15" {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + Neste caso o **FastAPI** esperará um corpo como: ```JSON hl_lines="2" diff --git a/docs/pt/docs/tutorial/body-nested-models.md b/docs/pt/docs/tutorial/body-nested-models.md new file mode 100644 index 0000000000..8ab77173e9 --- /dev/null +++ b/docs/pt/docs/tutorial/body-nested-models.md @@ -0,0 +1,248 @@ +# Corpo - Modelos aninhados + +Com o **FastAPI**, você pode definir, validar, documentar e usar modelos profundamente aninhados de forma arbitrária (graças ao Pydantic). + +## Campos do tipo Lista + +Você pode definir um atributo como um subtipo. Por exemplo, uma `list` do Python: + +```Python hl_lines="14" +{!../../../docs_src/body_nested_models/tutorial001.py!} +``` + +Isso fará com que tags seja uma lista de itens mesmo sem declarar o tipo dos elementos desta lista. + +## Campos do tipo Lista com um parâmetro de tipo + +Mas o Python tem uma maneira específica de declarar listas com tipos internos ou "parâmetros de tipo": + +### Importe `List` do typing + +Primeiramente, importe `List` do módulo `typing` que já vem por padrão no Python: + +```Python hl_lines="1" +{!../../../docs_src/body_nested_models/tutorial002.py!} +``` + +### Declare a `List` com um parâmetro de tipo + +Para declarar tipos que têm parâmetros de tipo(tipos internos), como `list`, `dict`, `tuple`: + +* Importe os do modulo `typing` +* Passe o(s) tipo(s) interno(s) como "parâmetros de tipo" usando colchetes: `[` e `]` + +```Python +from typing import List + +my_list: List[str] +``` + +Essa é a sintaxe padrão do Python para declarações de tipo. + +Use a mesma sintaxe padrão para atributos de modelo com tipos internos. + +Portanto, em nosso exemplo, podemos fazer com que `tags` sejam especificamente uma "lista de strings": + + +```Python hl_lines="14" +{!../../../docs_src/body_nested_models/tutorial002.py!} +``` + +## Tipo "set" + + +Mas então, quando nós pensamos mais, percebemos que as tags não devem se repetir, elas provavelmente devem ser strings únicas. + +E que o Python tem um tipo de dados especial para conjuntos de itens únicos, o `set`. + +Então podemos importar `Set` e declarar `tags` como um `set` de `str`s: + + +```Python hl_lines="1 14" +{!../../../docs_src/body_nested_models/tutorial003.py!} +``` + +Com isso, mesmo que você receba uma requisição contendo dados duplicados, ela será convertida em um conjunto de itens exclusivos. + +E sempre que você enviar esses dados como resposta, mesmo se a fonte tiver duplicatas, eles serão gerados como um conjunto de itens exclusivos. + +E também teremos anotações/documentação em conformidade. + +## Modelos aninhados + +Cada atributo de um modelo Pydantic tem um tipo. + +Mas esse tipo pode ser outro modelo Pydantic. + +Portanto, você pode declarar "objects" JSON profundamente aninhados com nomes, tipos e validações de atributos específicos. + +Tudo isso, aninhado arbitrariamente. + +### Defina um sub-modelo + +Por exemplo, nós podemos definir um modelo `Image`: + +```Python hl_lines="9-11" +{!../../../docs_src/body_nested_models/tutorial004.py!} +``` + +### Use o sub-modelo como um tipo + +E então podemos usa-lo como o tipo de um atributo: + +```Python hl_lines="20" +{!../../../docs_src/body_nested_models/tutorial004.py!} +``` + +Isso significa que o **FastAPI** vai esperar um corpo similar à: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": ["rock", "metal", "bar"], + "image": { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + } +} +``` + +Novamente, apenas fazendo essa declaração, com o **FastAPI**, você ganha: + +* Suporte do editor de texto (compleção, etc), inclusive para modelos aninhados +* Conversão de dados +* Validação de dados +* Documentação automatica + +## Tipos especiais e validação + +Além dos tipos singulares normais como `str`, `int`, `float`, etc. Você também pode usar tipos singulares mais complexos que herdam de `str`. + +Para ver todas as opções possíveis, cheque a documentação para ostipos exoticos do Pydantic. Você verá alguns exemplos no próximo capitulo. + +Por exemplo, no modelo `Image` nós temos um campo `url`, nós podemos declara-lo como um `HttpUrl` do Pydantic invés de como uma `str`: + +```Python hl_lines="4 10" +{!../../../docs_src/body_nested_models/tutorial005.py!} +``` + +A string será verificada para se tornar uma URL válida e documentada no esquema JSON/1OpenAPI como tal. + +## Atributos como listas de submodelos + +Você também pode usar modelos Pydantic como subtipos de `list`, `set`, etc: + +```Python hl_lines="20" +{!../../../docs_src/body_nested_models/tutorial006.py!} +``` + +Isso vai esperar(converter, validar, documentar, etc) um corpo JSON tal qual: + +```JSON hl_lines="11" +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": [ + "rock", + "metal", + "bar" + ], + "images": [ + { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + }, + { + "url": "http://example.com/dave.jpg", + "name": "The Baz" + } + ] +} +``` + +!!! Informação + Note como o campo `images` agora tem uma lista de objetos de image. + +## Modelos profundamente aninhados + +Você pode definir modelos profundamente aninhados de forma arbitrária: + +```Python hl_lines="9 14 20 23 27" +{!../../../docs_src/body_nested_models/tutorial007.py!} +``` + +!!! Informação + Note como `Offer` tem uma lista de `Item`s, que por sua vez possui opcionalmente uma lista `Image`s + +## Corpos de listas puras + +Se o valor de primeiro nível do corpo JSON que você espera for um `array` do JSON (uma` lista` do Python), você pode declarar o tipo no parâmetro da função, da mesma forma que nos modelos do Pydantic: + + +```Python +images: List[Image] +``` + +como em: + +```Python hl_lines="15" +{!../../../docs_src/body_nested_models/tutorial008.py!} +``` + +## Suporte de editor em todo canto + +E você obtém suporte do editor em todos os lugares. + +Mesmo para itens dentro de listas: + + + +Você não conseguiria este tipo de suporte de editor se estivesse trabalhando diretamente com `dict` em vez de modelos Pydantic. + +Mas você também não precisa se preocupar com eles, os dicts de entrada são convertidos automaticamente e sua saída é convertida automaticamente para JSON também. + +## Corpos de `dict`s arbitrários + +Você também pode declarar um corpo como um `dict` com chaves de algum tipo e valores de outro tipo. + +Sem ter que saber de antemão quais são os nomes de campos/atributos válidos (como seria o caso dos modelos Pydantic). + +Isso seria útil se você deseja receber chaves que ainda não conhece. + +--- + +Outro caso útil é quando você deseja ter chaves de outro tipo, por exemplo, `int`. + +É isso que vamos ver aqui. + +Neste caso, você aceitaria qualquer `dict`, desde que tenha chaves` int` com valores `float`: + +```Python hl_lines="9" +{!../../../docs_src/body_nested_models/tutorial009.py!} +``` + +!!! Dica + Leve em condideração que o JSON só suporta `str` como chaves. + + Mas o Pydantic tem conversão automática de dados. + + Isso significa que, embora os clientes da API só possam enviar strings como chaves, desde que essas strings contenham inteiros puros, o Pydantic irá convertê-los e validá-los. + + E o `dict` que você recebe como `weights` terá, na verdade, chaves `int` e valores` float`. + +## Recapitulação + +Com **FastAPI** você tem a flexibilidade máxima fornecida pelos modelos Pydantic, enquanto seu código é mantido simples, curto e elegante. + +Mas com todos os benefícios: + +* Suporte do editor (compleção em todo canto!) +* Conversão de dados (leia-se parsing/serialização) +* Validação de dados +* Documentação dos esquemas +* Documentação automática diff --git a/docs/pt/docs/tutorial/encoder.md b/docs/pt/docs/tutorial/encoder.md new file mode 100644 index 0000000000..b9bfbf63bf --- /dev/null +++ b/docs/pt/docs/tutorial/encoder.md @@ -0,0 +1,42 @@ +# Codificador Compatível com JSON + +Existem alguns casos em que você pode precisar converter um tipo de dados (como um modelo Pydantic) para algo compatível com JSON (como um `dict`, `list`, etc). + +Por exemplo, se você precisar armazená-lo em um banco de dados. + +Para isso, **FastAPI** fornece uma função `jsonable_encoder()`. + +## Usando a função `jsonable_encoder` + +Vamos imaginar que você tenha um banco de dados `fake_db` que recebe apenas dados compatíveis com JSON. + +Por exemplo, ele não recebe objetos `datetime`, pois estes objetos não são compatíveis com JSON. + +Então, um objeto `datetime` teria que ser convertido em um `str` contendo os dados no formato ISO. + +Da mesma forma, este banco de dados não receberia um modelo Pydantic (um objeto com atributos), apenas um `dict`. + +Você pode usar a função `jsonable_encoder` para resolver isso. + +A função recebe um objeto, como um modelo Pydantic e retorna uma versão compatível com JSON: + +=== "Python 3.10+" + + ```Python hl_lines="4 21" + {!> ../../../docs_src/encoder/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + +Neste exemplo, ele converteria o modelo Pydantic em um `dict`, e o `datetime` em um `str`. + +O resultado de chamar a função é algo que pode ser codificado com o padrão do Python `json.dumps()`. + +A função não retorna um grande `str` contendo os dados no formato JSON (como uma string). Mas sim, retorna uma estrutura de dados padrão do Python (por exemplo, um `dict`) com valores e subvalores compatíveis com JSON. + +!!! nota + `jsonable_encoder` é realmente usado pelo **FastAPI** internamente para converter dados. Mas também é útil em muitos outros cenários. diff --git a/docs/pt/docs/tutorial/extra-models.md b/docs/pt/docs/tutorial/extra-models.md new file mode 100644 index 0000000000..1343a3ae48 --- /dev/null +++ b/docs/pt/docs/tutorial/extra-models.md @@ -0,0 +1,252 @@ +# Modelos Adicionais + +Continuando com o exemplo anterior, será comum ter mais de um modelo relacionado. + +Isso é especialmente o caso para modelos de usuários, porque: + +* O **modelo de entrada** precisa ser capaz de ter uma senha. +* O **modelo de saída** não deve ter uma senha. +* O **modelo de banco de dados** provavelmente precisaria ter uma senha criptografada. + +!!! danger + Nunca armazene senhas em texto simples dos usuários. Sempre armazene uma "hash segura" que você pode verificar depois. + + Se não souber, você aprenderá o que é uma "senha hash" nos [capítulos de segurança](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}. + +## Múltiplos modelos + +Aqui está uma ideia geral de como os modelos poderiam parecer com seus campos de senha e os lugares onde são usados: + +=== "Python 3.8 and above" + + ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" + {!> ../../../docs_src/extra_models/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" + {!> ../../../docs_src/extra_models/tutorial001_py310.py!} + ``` + +### Sobre `**user_in.dict()` + +#### O `.dict()` do Pydantic + +`user_in` é um modelo Pydantic da classe `UserIn`. + +Os modelos Pydantic possuem um método `.dict()` que retorna um `dict` com os dados do modelo. + +Então, se criarmos um objeto Pydantic `user_in` como: + +```Python +user_in = UserIn(username="john", password="secret", email="john.doe@example.com") +``` + +e depois chamarmos: + +```Python +user_dict = user_in.dict() +``` + +agora temos um `dict` com os dados na variável `user_dict` (é um `dict` em vez de um objeto de modelo Pydantic). + +E se chamarmos: + +```Python +print(user_dict) +``` + +teríamos um `dict` Python com: + +```Python +{ + 'username': 'john', + 'password': 'secret', + 'email': 'john.doe@example.com', + 'full_name': None, +} +``` + +#### Desembrulhando um `dict` + +Se tomarmos um `dict` como `user_dict` e passarmos para uma função (ou classe) com `**user_dict`, o Python irá "desembrulhá-lo". Ele passará as chaves e valores do `user_dict` diretamente como argumentos chave-valor. + +Então, continuando com o `user_dict` acima, escrevendo: + +```Python +UserInDB(**user_dict) +``` + +Resultaria em algo equivalente a: + +```Python +UserInDB( + username="john", + password="secret", + email="john.doe@example.com", + full_name=None, +) +``` + +Ou mais exatamente, usando `user_dict` diretamente, com qualquer conteúdo que ele possa ter no futuro: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], +) +``` + +#### Um modelo Pydantic a partir do conteúdo de outro + +Como no exemplo acima, obtivemos o `user_dict` a partir do `user_in.dict()`, este código: + +```Python +user_dict = user_in.dict() +UserInDB(**user_dict) +``` + +seria equivalente a: + +```Python +UserInDB(**user_in.dict()) +``` + +...porque `user_in.dict()` é um `dict`, e depois fazemos o Python "desembrulhá-lo" passando-o para UserInDB precedido por `**`. + +Então, obtemos um modelo Pydantic a partir dos dados em outro modelo Pydantic. + +#### Desembrulhando um `dict` e palavras-chave extras + +E, então, adicionando o argumento de palavra-chave extra `hashed_password=hashed_password`, como em: + +```Python +UserInDB(**user_in.dict(), hashed_password=hashed_password) +``` + +...acaba sendo como: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], + hashed_password = hashed_password, +) +``` + +!!! warning + As funções adicionais de suporte são apenas para demonstração de um fluxo possível dos dados, mas é claro que elas não fornecem segurança real. + +## Reduzir duplicação + +Reduzir a duplicação de código é uma das ideias principais no **FastAPI**. + +A duplicação de código aumenta as chances de bugs, problemas de segurança, problemas de desincronização de código (quando você atualiza em um lugar, mas não em outros), etc. + +E esses modelos estão compartilhando muitos dos dados e duplicando nomes e tipos de atributos. + +Nós poderíamos fazer melhor. + +Podemos declarar um modelo `UserBase` que serve como base para nossos outros modelos. E então podemos fazer subclasses desse modelo que herdam seus atributos (declarações de tipo, validação, etc.). + +Toda conversão de dados, validação, documentação, etc. ainda funcionará normalmente. + +Dessa forma, podemos declarar apenas as diferenças entre os modelos (com `password` em texto claro, com `hashed_password` e sem senha): + +=== "Python 3.8 and above" + + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 13-14 17-18 21-22" + {!> ../../../docs_src/extra_models/tutorial002_py310.py!} + ``` + +## `Union` ou `anyOf` + +Você pode declarar uma resposta como o `Union` de dois tipos, o que significa que a resposta seria qualquer um dos dois. + +Isso será definido no OpenAPI com `anyOf`. + +Para fazer isso, use a dica de tipo padrão do Python `typing.Union`: + +!!! note + Ao definir um `Union`, inclua o tipo mais específico primeiro, seguido pelo tipo menos específico. No exemplo abaixo, o tipo mais específico `PlaneItem` vem antes de `CarItem` em `Union[PlaneItem, CarItem]`. + +=== "Python 3.8 and above" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003_py310.py!} + ``` + +### `Union` no Python 3.10 + +Neste exemplo, passamos `Union[PlaneItem, CarItem]` como o valor do argumento `response_model`. + +Dado que estamos passando-o como um **valor para um argumento** em vez de colocá-lo em uma **anotação de tipo**, precisamos usar `Union` mesmo no Python 3.10. + +Se estivesse em uma anotação de tipo, poderíamos ter usado a barra vertical, como: + +```Python +some_variable: PlaneItem | CarItem +``` + +Mas se colocarmos isso em `response_model=PlaneItem | CarItem` teríamos um erro, pois o Python tentaria executar uma **operação inválida** entre `PlaneItem` e `CarItem` em vez de interpretar isso como uma anotação de tipo. + +## Lista de modelos + +Da mesma forma, você pode declarar respostas de listas de objetos. + +Para isso, use o padrão Python `typing.List` (ou simplesmente `list` no Python 3.9 e superior): + +=== "Python 3.8 and above" + + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/extra_models/tutorial004_py39.py!} + ``` + +## Resposta com `dict` arbitrário + +Você também pode declarar uma resposta usando um simples `dict` arbitrário, declarando apenas o tipo das chaves e valores, sem usar um modelo Pydantic. + +Isso é útil se você não souber os nomes de campo / atributo válidos (que seriam necessários para um modelo Pydantic) antecipadamente. + +Neste caso, você pode usar `typing.Dict` (ou simplesmente dict no Python 3.9 e superior): + +=== "Python 3.8 and above" + + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="6" + {!> ../../../docs_src/extra_models/tutorial005_py39.py!} + ``` + +## Em resumo + +Use vários modelos Pydantic e herde livremente para cada caso. + +Não é necessário ter um único modelo de dados por entidade se essa entidade precisar ter diferentes "estados". No caso da "entidade" de usuário com um estado que inclui `password`, `password_hash` e sem senha. diff --git a/docs/pt/docs/tutorial/header-params.md b/docs/pt/docs/tutorial/header-params.md index 94ee784cd2..4bdfb7e9cd 100644 --- a/docs/pt/docs/tutorial/header-params.md +++ b/docs/pt/docs/tutorial/header-params.md @@ -6,36 +6,36 @@ Você pode definir parâmetros de Cabeçalho da mesma maneira que define paramê Primeiro importe `Header`: -=== "Python 3.6 and above" - - ```Python hl_lines="3" - {!> ../../../docs_src/header_params/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="1" {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + ## Declare parâmetros de `Header` Então declare os paramêtros de cabeçalho usando a mesma estrutura que em `Path`, `Query` e `Cookie`. O primeiro valor é o valor padrão, você pode passar todas as validações adicionais ou parâmetros de anotação: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + !!! note "Detalhes Técnicos" `Header` é uma classe "irmã" de `Path`, `Query` e `Cookie`. Ela também herda da mesma classe em comum `Param`. @@ -60,18 +60,18 @@ Portanto, você pode usar `user_agent` como faria normalmente no código Python, Se por algum motivo você precisar desabilitar a conversão automática de sublinhados para hífens, defina o parâmetro `convert_underscores` de `Header` para `False`: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/header_params/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="8" {!> ../../../docs_src/header_params/tutorial002_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} + ``` + !!! warning "Aviso" Antes de definir `convert_underscores` como `False`, lembre-se de que alguns proxies e servidores HTTP não permitem o uso de cabeçalhos com sublinhados. @@ -85,22 +85,22 @@ Você receberá todos os valores do cabeçalho duplicado como uma `list` Python. Por exemplo, para declarar um cabeçalho de `X-Token` que pode aparecer mais de uma vez, você pode escrever: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial003.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial003_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial003_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.8+" - ```Python hl_lines="7" - {!> ../../../docs_src/header_params/tutorial003_py310.py!} + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} ``` Se você se comunicar com essa *operação de caminho* enviando dois cabeçalhos HTTP como: diff --git a/docs/pt/docs/tutorial/index.md b/docs/pt/docs/tutorial/index.md index b1abd32bc1..5fc0485a07 100644 --- a/docs/pt/docs/tutorial/index.md +++ b/docs/pt/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Tutorial - Guia de Usuário - Introdução +# Tutorial - Guia de Usuário Esse tutorial mostra como usar o **FastAPI** com a maior parte de seus recursos, passo a passo. diff --git a/docs/pt/docs/tutorial/path-operation-configuration.md b/docs/pt/docs/tutorial/path-operation-configuration.md new file mode 100644 index 0000000000..13a87240f1 --- /dev/null +++ b/docs/pt/docs/tutorial/path-operation-configuration.md @@ -0,0 +1,180 @@ +# Configuração da Operação de Rota + +Existem vários parâmetros que você pode passar para o seu *decorador de operação de rota* para configurá-lo. + +!!! warning "Aviso" + Observe que esses parâmetros são passados diretamente para o *decorador de operação de rota*, não para a sua *função de operação de rota*. + +## Código de Status da Resposta + +Você pode definir o `status_code` (HTTP) para ser usado na resposta da sua *operação de rota*. + +Você pode passar diretamente o código `int`, como `404`. + +Mas se você não se lembrar o que cada código numérico significa, pode usar as constantes de atalho em `status`: + +=== "Python 3.8 and above" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 15" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} + ``` + +Esse código de status será usado na resposta e será adicionado ao esquema OpenAPI. + +!!! note "Detalhes Técnicos" + Você também poderia usar `from starlette import status`. + + **FastAPI** fornece o mesmo `starlette.status` como `fastapi.status` apenas como uma conveniência para você, o desenvolvedor. Mas vem diretamente do Starlette. + +## Tags + +Você pode adicionar tags para sua *operação de rota*, passe o parâmetro `tags` com uma `list` de `str` (comumente apenas um `str`): + +=== "Python 3.8 and above" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="15 20 25" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} + ``` + +Eles serão adicionados ao esquema OpenAPI e usados pelas interfaces de documentação automática: + + + +### Tags com Enums + +Se você tem uma grande aplicação, você pode acabar acumulando **várias tags**, e você gostaria de ter certeza de que você sempre usa a **mesma tag** para *operações de rota* relacionadas. + +Nestes casos, pode fazer sentido armazenar as tags em um `Enum`. + +**FastAPI** suporta isso da mesma maneira que com strings simples: + +```Python hl_lines="1 8-10 13 18" +{!../../../docs_src/path_operation_configuration/tutorial002b.py!} +``` + +## Resumo e descrição + +Você pode adicionar um `summary` e uma `description`: + +=== "Python 3.8 and above" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} + ``` + +## Descrição do docstring + +Como as descrições tendem a ser longas e cobrir várias linhas, você pode declarar a descrição da *operação de rota* na docstring da função e o **FastAPI** irá lê-la de lá. + +Você pode escrever Markdown na docstring, ele será interpretado e exibido corretamente (levando em conta a indentação da docstring). + +=== "Python 3.8 and above" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17-25" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} + ``` + +Ela será usada nas documentações interativas: + + + + +## Descrição da resposta + +Você pode especificar a descrição da resposta com o parâmetro `response_description`: + +=== "Python 3.8 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} + ``` + +!!! info "Informação" + Note que `response_description` se refere especificamente à resposta, a `description` se refere à *operação de rota* em geral. + +!!! check + OpenAPI especifica que cada *operação de rota* requer uma descrição de resposta. + + Então, se você não fornecer uma, o **FastAPI** irá gerar automaticamente uma de "Resposta bem-sucedida". + + + +## Depreciar uma *operação de rota* + +Se você precisar marcar uma *operação de rota* como descontinuada, mas sem removê-la, passe o parâmetro `deprecated`: + +```Python hl_lines="16" +{!../../../docs_src/path_operation_configuration/tutorial006.py!} +``` + +Ela será claramente marcada como descontinuada nas documentações interativas: + + + +Verifique como *operações de rota* descontinuadas e não descontinuadas se parecem: + + + +## Resumindo + +Você pode configurar e adicionar metadados para suas *operações de rota* facilmente passando parâmetros para os *decoradores de operação de rota*. diff --git a/docs/pt/docs/tutorial/path-params-numeric-validations.md b/docs/pt/docs/tutorial/path-params-numeric-validations.md index f478fd190d..eb0d31dc34 100644 --- a/docs/pt/docs/tutorial/path-params-numeric-validations.md +++ b/docs/pt/docs/tutorial/path-params-numeric-validations.md @@ -6,36 +6,36 @@ Do mesmo modo que você pode declarar mais validações e metadados para parâme Primeiro, importe `Path` de `fastapi`: -=== "Python 3.6 e superiores" - - ```Python hl_lines="3" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} - ``` - -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="1" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + ## Declare metadados Você pode declarar todos os parâmetros da mesma maneira que na `Query`. Por exemplo para declarar um valor de metadado `title` para o parâmetro de rota `item_id` você pode digitar: -=== "Python 3.6 e superiores" - - ```Python hl_lines="10" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} - ``` - -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="8" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + !!! note "Nota" Um parâmetro de rota é sempre obrigatório, como se fizesse parte da rota. diff --git a/docs/pt/docs/tutorial/path-params.md b/docs/pt/docs/tutorial/path-params.md index 5de3756ed6..cd8c188584 100644 --- a/docs/pt/docs/tutorial/path-params.md +++ b/docs/pt/docs/tutorial/path-params.md @@ -236,7 +236,6 @@ Então, você poderia usar ele com: Com o **FastAPI**, usando as declarações de tipo do Python, você obtém: * Suporte no editor: verificação de erros, e opção de autocompletar, etc. -* Parsing de dados * "Parsing" de dados * Validação de dados * Anotação da API e documentação automática diff --git a/docs/pt/docs/tutorial/query-params.md b/docs/pt/docs/tutorial/query-params.md index 1897243961..08bb99dbc8 100644 --- a/docs/pt/docs/tutorial/query-params.md +++ b/docs/pt/docs/tutorial/query-params.md @@ -63,18 +63,18 @@ Os valores dos parâmetros na sua função serão: Da mesma forma, você pode declarar parâmetros de consulta opcionais, definindo o valor padrão para `None`: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/query_params/tutorial002_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial002.py!} + ``` + Nesse caso, o parâmetro da função `q` será opcional, e `None` será o padrão. !!! check "Verificar" @@ -85,18 +85,18 @@ Nesse caso, o parâmetro da função `q` será opcional, e `None` será o padrã Você também pode declarar tipos `bool`, e eles serão convertidos: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/query_params/tutorial003_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} + ``` + Nesse caso, se você for para: ``` @@ -137,18 +137,18 @@ E você não precisa declarar eles em nenhuma ordem específica. Eles serão detectados pelo nome: -=== "Python 3.6 and above" - - ```Python hl_lines="8 10" - {!> ../../../docs_src/query_params/tutorial004.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="6 8" {!> ../../../docs_src/query_params/tutorial004_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} + ``` + ## Parâmetros de consulta obrigatórios Quando você declara um valor padrão para parâmetros que não são de rota (até agora, nós vimos apenas parâmetros de consulta), então eles não são obrigatórios. @@ -203,17 +203,18 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy E claro, você pode definir alguns parâmetros como obrigatórios, alguns possuindo um valor padrão, e outros sendo totalmente opcionais: -=== "Python 3.6 and above" +=== "Python 3.10+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params/tutorial006_py310.py!} + ``` + +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params/tutorial006.py!} ``` -=== "Python 3.10 and above" - - ```Python hl_lines="8" - {!> ../../../docs_src/query_params/tutorial006_py310.py!} - ``` Nesse caso, existem 3 parâmetros de consulta: * `needy`, um `str` obrigatório. diff --git a/docs/pt/docs/tutorial/security/index.md b/docs/pt/docs/tutorial/security/index.md index 70f864040a..f94a8ab626 100644 --- a/docs/pt/docs/tutorial/security/index.md +++ b/docs/pt/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# Introdução à segurança +# Segurança Há várias formas de lidar segurança, autenticação e autorização. diff --git a/docs/pt/docs/tutorial/static-files.md b/docs/pt/docs/tutorial/static-files.md new file mode 100644 index 0000000000..009158fc63 --- /dev/null +++ b/docs/pt/docs/tutorial/static-files.md @@ -0,0 +1,39 @@ +# Arquivos Estáticos + +Você pode servir arquivos estáticos automaticamente de um diretório usando `StaticFiles`. + +## Use `StaticFiles` + +* Importe `StaticFiles`. +* "Monte" uma instância de `StaticFiles()` em um caminho específico. + +```Python hl_lines="2 6" +{!../../../docs_src/static_files/tutorial001.py!} +``` + +!!! note "Detalhes técnicos" + Você também pode usar `from starlette.staticfiles import StaticFiles`. + + O **FastAPI** fornece o mesmo que `starlette.staticfiles` como `fastapi.staticfiles` apenas como uma conveniência para você, o desenvolvedor. Mas na verdade vem diretamente da Starlette. + +### O que é "Montagem" + +"Montagem" significa adicionar um aplicativo completamente "independente" em uma rota específica, que então cuida de todas as subrotas. + +Isso é diferente de usar um `APIRouter`, pois um aplicativo montado é completamente independente. A OpenAPI e a documentação do seu aplicativo principal não incluirão nada do aplicativo montado, etc. + +Você pode ler mais sobre isso no **Guia Avançado do Usuário**. + +## Detalhes + +O primeiro `"/static"` refere-se à subrota em que este "subaplicativo" será "montado". Portanto, qualquer caminho que comece com `"/static"` será tratado por ele. + +O `directory="static"` refere-se ao nome do diretório que contém seus arquivos estáticos. + +O `name="static"` dá a ela um nome que pode ser usado internamente pelo FastAPI. + +Todos esses parâmetros podem ser diferentes de "`static`", ajuste-os de acordo com as necessidades e detalhes específicos de sua própria aplicação. + +## Mais informações + +Para mais detalhes e opções, verifique Starlette's docs about Static Files. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 0858de0624..de18856f44 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -1,180 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/pt/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: pt -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- Tutorial - Guia de Usuário: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/body-multiple-params.md - - tutorial/body-fields.md - - tutorial/extra-data-types.md - - tutorial/query-params-str-validations.md - - tutorial/path-params-numeric-validations.md - - tutorial/cookie-params.md - - tutorial/header-params.md - - tutorial/response-status-code.md - - tutorial/request-forms.md - - tutorial/request-forms-and-files.md - - tutorial/handling-errors.md - - Segurança: - - tutorial/security/index.md - - tutorial/background-tasks.md - - Guia de Usuário Avançado: - - advanced/index.md -- Implantação: - - deployment/index.md - - deployment/versions.md - - deployment/https.md - - deployment/deta.md - - deployment/docker.md -- alternatives.md -- history-design-future.md -- external-links.md -- benchmarks.md -- help-fastapi.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs/ru/docs/alternatives.md b/docs/ru/docs/alternatives.md new file mode 100644 index 0000000000..9e3c497d10 --- /dev/null +++ b/docs/ru/docs/alternatives.md @@ -0,0 +1,460 @@ +# Альтернативы, источники вдохновения и сравнения + +Что вдохновило на создание **FastAPI**, сравнение его с альтернативами и чему он научился у них. + +## Введение + +**FastAPI** не существовал бы, если б не было более ранних работ других людей. + +Они создали большое количество инструментов, которые вдохновили меня на создание **FastAPI**. + +Я всячески избегал создания нового фреймворка в течение нескольких лет. +Сначала я пытался собрать все нужные функции, которые ныне есть в **FastAPI**, используя множество различных фреймворков, плагинов и инструментов. + +Но в какой-то момент не осталось другого выбора, кроме как создать что-то, что предоставляло бы все эти функции сразу. +Взять самые лучшие идеи из предыдущих инструментов и, используя новые возможности Python (которых не было до версии 3.6, то есть подсказки типов), объединить их. + +## Предшествующие инструменты + +### Django + +Это самый популярный Python-фреймворк, и он пользуется доверием. +Он используется для создания проектов типа Instagram. + +Django довольно тесно связан с реляционными базами данных (такими как MySQL или PostgreSQL), потому использовать NoSQL базы данных (например, Couchbase, MongoDB, Cassandra и т.п.) в качестве основного хранилища данных - непросто. + +Он был создан для генерации HTML-страниц на сервере, а не для создания API, используемых современными веб-интерфейсами (React, Vue.js, Angular и т.п.) или другими системами (например, IoT) взаимодействующими с сервером. + +### Django REST Framework + +Фреймворк Django REST был создан, как гибкий инструментарий для создания веб-API на основе Django. + +DRF использовался многими компаниями, включая Mozilla, Red Hat и Eventbrite. + +Это был один из первых примеров **автоматического документирования API** и это была одна из первых идей, вдохновивших на создание **FastAPI**. + +!!! note "Заметка" + Django REST Framework был создан Tom Christie. + Он же создал Starlette и Uvicorn, на которых основан **FastAPI**. + +!!! check "Идея для **FastAPI**" + Должно быть автоматическое создание документации API с пользовательским веб-интерфейсом. + +### Flask + +Flask - это "микрофреймворк", в нём нет интеграции с базами данных и многих других вещей, которые предустановлены в Django. + +Его простота и гибкость дают широкие возможности, такие как использование баз данных NoSQL в качестве основной системы хранения данных. + +Он очень прост, его изучение интуитивно понятно, хотя в некоторых местах документация довольно техническая. + +Flask часто используется и для приложений, которым не нужна база данных, настройки прав доступа для пользователей и прочие из множества функций, предварительно встроенных в Django. +Хотя многие из этих функций могут быть добавлены с помощью плагинов. + +Такое разделение на части и то, что это "микрофреймворк", который можно расширить, добавляя необходимые возможности, было ключевой особенностью, которую я хотел сохранить. + +Простота Flask, показалась мне подходящей для создания API. +Но ещё нужно было найти "Django REST Framework" для Flask. + +!!! check "Идеи для **FastAPI**" + Это будет микрофреймворк. К нему легко будет добавить необходимые инструменты и части. + + Должна быть простая и лёгкая в использовании система маршрутизации запросов. + + +### Requests + +На самом деле **FastAPI** не является альтернативой **Requests**. +Их область применения очень разная. + +В принципе, можно использовать Requests *внутри* приложения FastAPI. + +Но всё же я использовал в FastAPI некоторые идеи из Requests. + +**Requests** - это библиотека для взаимодействия с API в качестве клиента, +в то время как **FastAPI** - это библиотека для *создания* API (то есть сервера). + +Они, так или иначе, диаметрально противоположны и дополняют друг друга. + +Requests имеет очень простой и понятный дизайн, очень прост в использовании и имеет разумные значения по умолчанию. +И в то же время он очень мощный и настраиваемый. + +Вот почему на официальном сайте написано: + +> Requests - один из самых загружаемых пакетов Python всех времен + + +Использовать его очень просто. Например, чтобы выполнить запрос `GET`, Вы бы написали: + +```Python +response = requests.get("http://example.com/some/url") +``` + +Противоположная *операция пути* в FastAPI может выглядеть следующим образом: + +```Python hl_lines="1" +@app.get("/some/url") +def read_url(): + return {"message": "Hello World"} +``` + +Глядите, как похоже `requests.get(...)` и `@app.get(...)`. + +!!! check "Идеи для **FastAPI**" + * Должен быть простой и понятный API. + * Нужно использовать названия HTTP-методов (операций) для упрощения понимания происходящего. + * Должны быть разумные настройки по умолчанию и широкие возможности их кастомизации. + + +### Swagger / OpenAPI + +Главной функцией, которую я хотел унаследовать от Django REST Framework, была автоматическая документация API. + +Но потом я обнаружил, что существует стандарт документирования API, использующий JSON (или YAML, расширение JSON) под названием Swagger. + +И к нему уже был создан пользовательский веб-интерфейс. +Таким образом, возможность генерировать документацию Swagger для API позволила бы использовать этот интерфейс. + +В какой-то момент Swagger был передан Linux Foundation и переименован в OpenAPI. + +Вот почему, когда говорят о версии 2.0, обычно говорят "Swagger", а для версии 3+ "OpenAPI". + +!!! check "Идеи для **FastAPI**" + Использовать открытые стандарты для спецификаций API вместо самодельных схем. + + Совместимость с основанными на стандартах пользовательскими интерфейсами: + + * Swagger UI + * ReDoc + + Они были выбраны за популярность и стабильность. + Но сделав беглый поиск, Вы можете найти десятки альтернативных пользовательских интерфейсов для OpenAPI, которые Вы можете использовать с **FastAPI**. + +### REST фреймворки для Flask + +Существует несколько REST фреймворков для Flask, но потратив время и усилия на их изучение, я обнаружил, что многие из них не обновляются или заброшены и имеют нерешённые проблемы из-за которых они непригодны к использованию. + +### Marshmallow + +Одной из основных функций, необходимых системам API, является "сериализация" данных, то есть преобразование данных из кода (Python) во что-то, что может быть отправлено по сети. +Например, превращение объекта содержащего данные из базы данных в объект JSON, конвертация объекта `datetime` в строку и т.п. + +Еще одна важная функция, необходимая API — проверка данных, позволяющая убедиться, что данные действительны и соответствуют заданным параметрам. +Как пример, можно указать, что ожидаются данные типа `int`, а не какая-то произвольная строка. +Это особенно полезно для входящих данных. + +Без системы проверки данных Вам пришлось бы прописывать все проверки вручную. + +Именно для обеспечения этих функций и была создана Marshmallow. +Это отличная библиотека и я много раз пользовался ею раньше. + +Но она была создана до того, как появились подсказки типов Python. +Итак, чтобы определить каждую схему, +Вам нужно использовать определенные утилиты и классы, предоставляемые Marshmallow. + +!!! check "Идея для **FastAPI**" + Использовать код программы для автоматического создания "схем", определяющих типы данных и их проверку. + +### Webargs + +Другая немаловажная функция API - парсинг данных из входящих запросов. + +Webargs - это инструмент, который был создан для этого и поддерживает несколько фреймворков, включая Flask. + +Для проверки данных он использует Marshmallow и создан теми же авторами. + +Это превосходный инструмент и я тоже часто пользовался им до **FastAPI**. + +!!! info "Информация" + Webargs бы создан разработчиками Marshmallow. + +!!! check "Идея для **FastAPI**" + Должна быть автоматическая проверка входных данных. + +### APISpec + +Marshmallow и Webargs осуществляют проверку, анализ и сериализацию данных как плагины. + +Но документации API всё ещё не было. Тогда был создан APISpec. + +Это плагин для множества фреймворков, в том числе и для Starlette. + +Он работает так - Вы записываете определение схем, используя формат YAML, внутри докстринга каждой функции, обрабатывающей маршрут. + +Используя эти докстринги, он генерирует схему OpenAPI. + +Так это работает для Flask, Starlette, Responder и т.п. + +Но теперь у нас возникает новая проблема - наличие постороннего микро-синтаксиса внутри кода Python (большие YAML). + +Редактор кода не особо может помочь в такой парадигме. +А изменив какие-то параметры или схемы для Marshmallow можно забыть отредактировать докстринг с YAML и сгенерированная схема становится недействительной. + +!!! info "Информация" + APISpec тоже был создан авторами Marshmallow. + +!!! check "Идея для **FastAPI**" + Необходима поддержка открытого стандарта для API - OpenAPI. + +### Flask-apispec + +Это плагин для Flask, который связан с Webargs, Marshmallow и APISpec. + +Он получает информацию от Webargs и Marshmallow, а затем использует APISpec для автоматического создания схемы OpenAPI. + +Это отличный, но крайне недооценённый инструмент. +Он должен быть более популярен, чем многие плагины для Flask. +Возможно, это связано с тем, что его документация слишком скудна и абстрактна. + +Он избавил от необходимости писать чужеродный синтаксис YAML внутри докстрингов. + +Такое сочетание Flask, Flask-apispec, Marshmallow и Webargs было моим любимым стеком при построении бэкенда до появления **FastAPI**. + +Использование этого стека привело к созданию нескольких генераторов проектов. Я и некоторые другие команды до сих пор используем их: + +* https://github.com/tiangolo/full-stack +* https://github.com/tiangolo/full-stack-flask-couchbase +* https://github.com/tiangolo/full-stack-flask-couchdb + +Эти генераторы проектов также стали основой для [Генераторов проектов с **FastAPI**](project-generation.md){.internal-link target=_blank}. + +!!! info "Информация" + Как ни странно, но Flask-apispec тоже создан авторами Marshmallow. + +!!! check "Идея для **FastAPI**" + Схема OpenAPI должна создаваться автоматически и использовать тот же код, который осуществляет сериализацию и проверку данных. + +### NestJSAngular) + +Здесь даже не используется Python. NestJS - этот фреймворк написанный на JavaScript (TypeScript), основанный на NodeJS и вдохновлённый Angular. + +Он позволяет получить нечто похожее на то, что можно сделать с помощью Flask-apispec. + +В него встроена система внедрения зависимостей, ещё одна идея взятая от Angular. +Однако требуется предварительная регистрация "внедрений" (как и во всех других известных мне системах внедрения зависимостей), что увеличивает количество и повторяемость кода. + +Так как параметры в нём описываются с помощью типов TypeScript (аналогично подсказкам типов в Python), поддержка редактора работает довольно хорошо. + +Но поскольку данные из TypeScript не сохраняются после компиляции в JavaScript, он не может полагаться на подсказки типов для определения проверки данных, сериализации и документации. +Из-за этого и некоторых дизайнерских решений, для валидации, сериализации и автоматической генерации схем, приходится во многих местах добавлять декораторы. +Таким образом, это становится довольно многословным. + +Кроме того, он не очень хорошо справляется с вложенными моделями. +Если в запросе имеется объект JSON, внутренние поля которого, в свою очередь, являются вложенными объектами JSON, это не может быть должным образом задокументировано и проверено. + +!!! check "Идеи для **FastAPI** " + Нужно использовать подсказки типов, чтоб воспользоваться поддержкой редактора кода. + + Нужна мощная система внедрения зависимостей. Необходим способ для уменьшения повторов кода. + +### Sanic + +Sanic был одним из первых чрезвычайно быстрых Python-фреймворков основанных на `asyncio`. +Он был сделан очень похожим на Flask. + +!!! note "Технические детали" + В нём использован `uvloop` вместо стандартного цикла событий `asyncio`, что и сделало его таким быстрым. + + Он явно вдохновил создателей Uvicorn и Starlette, которые в настоящее время быстрее Sanic в открытых бенчмарках. + +!!! check "Идеи для **FastAPI**" + Должна быть сумасшедшая производительность. + + Для этого **FastAPI** основан на Starlette, самом быстром из доступных фреймворков (по замерам незаинтересованных лиц). + +### Falcon + +Falcon - ещё один высокопроизводительный Python-фреймворк. +В нём минимум функций и он создан, чтоб быть основой для других фреймворков, например, Hug. + +Функции в нём получают два параметра - "запрос к серверу" и "ответ сервера". +Затем Вы "читаете" часть запроса и "пишите" часть ответа. +Из-за такой конструкции невозможно объявить параметры запроса и тела сообщения со стандартными подсказками типов Python в качестве параметров функции. + +Таким образом, и валидацию данных, и их сериализацию, и документацию нужно прописывать вручную. +Либо эти функции должны быть встроены во фреймворк, сконструированный поверх Falcon, как в Hug. +Такая же особенность присутствует и в других фреймворках, вдохновлённых идеей Falcon, использовать только один объект запроса и один объект ответа. + +!!! check "Идея для **FastAPI**" + Найдите способы добиться отличной производительности. + + Объявлять параметры `ответа сервера` в функциях, как в Hug. + + Хотя в FastAPI это необязательно и используется в основном для установки заголовков, куки и альтернативных кодов состояния. + +### Molten + +Molten мне попался на начальной стадии написания **FastAPI**. В нём были похожие идеи: + +* Использование подсказок типов. +* Валидация и документация исходя из этих подсказок. +* Система внедрения зависимостей. + +В нём не используются сторонние библиотеки (такие, как Pydantic) для валидации, сериализации и документации. +Поэтому переиспользовать эти определения типов непросто. + +Также требуется более подробная конфигурация и используется стандарт WSGI, который не предназначен для использования с высокопроизводительными инструментами, такими как Uvicorn, Starlette и Sanic, в отличие от ASGI. + +Его система внедрения зависимостей требует предварительной регистрации, и зависимости определяются, как объявления типов. +Из-за этого невозможно объявить более одного "компонента" (зависимости), который предоставляет определенный тип. + +Маршруты объявляются в единственном месте с использованием функций, объявленных в других местах (вместо использования декораторов, в которые могут быть обёрнуты функции, обрабатывающие конкретные ресурсы). +Это больше похоже на Django, чем на Flask и Starlette. +Он разделяет в коде вещи, которые довольно тесно связаны. + +!!! check "Идея для **FastAPI**" + Определить дополнительные проверки типов данных, используя значения атрибутов модели "по умолчанию". + Это улучшает помощь редактора и раньше это не было доступно в Pydantic. + + Фактически это подтолкнуло на обновление Pydantic для поддержки одинакового стиля проверок (теперь этот функционал уже доступен в Pydantic). + +### Hug + +Hug был одним из первых фреймворков, реализовавших объявление параметров API с использованием подсказок типов Python. +Эта отличная идея была использована и другими инструментами. + +При объявлении параметров вместо стандартных типов Python использовались собственные типы, но всё же это был огромный шаг вперед. + +Это также был один из первых фреймворков, генерировавших полную API-схему в формате JSON. + +Данная схема не придерживалась стандартов вроде OpenAPI и JSON Schema. +Поэтому было бы непросто совместить её с другими инструментами, такими как Swagger UI. +Но опять же, это была очень инновационная идея. + +Ещё у него есть интересная и необычная функция: используя один и тот же фреймворк можно создавать и API, и CLI. + +Поскольку он основан на WSGI, старом стандарте для синхронных веб-фреймворков, он не может работать с веб-сокетами и другими модными штуками, но всё равно обладает высокой производительностью. + +!!! info "Информация" + Hug создан Timothy Crosley, автором `isort`, отличного инструмента для автоматической сортировки импортов в Python-файлах. + +!!! check "Идеи для **FastAPI**" + Hug повлиял на создание некоторых частей APIStar и был одним из инструментов, которые я счел наиболее многообещающими, наряду с APIStar. + + Hug натолкнул на мысли использовать в **FastAPI** подсказки типов Python для автоматического создания схемы, определяющей API и его параметры. + + Hug вдохновил **FastAPI** объявить параметр `ответа` в функциях для установки заголовков и куки. + +### APIStar (<= 0.5) + +Непосредственно перед тем, как принять решение о создании **FastAPI**, я обнаружил **APIStar**. +В нем было почти все, что я искал и у него был отличный дизайн. + +Это была одна из первых реализаций фреймворка, использующего подсказки типов для объявления параметров и запросов, которые я когда-либо видел (до NestJS и Molten). +Я нашёл его примерно в то же время, что и Hug, но APIStar использовал стандарт OpenAPI. + +В нём были автоматические проверка и сериализация данных и генерация схемы OpenAPI основанные на подсказках типов в нескольких местах. + +При определении схемы тела сообщения не использовались подсказки типов, как в Pydantic, это больше похоже на Marshmallow, поэтому помощь редактора была недостаточно хорошей, но всё же APIStar был лучшим доступным вариантом. + +На тот момент у него были лучшие показатели производительности (проигрывающие только Starlette). + +Изначально у него не было автоматической документации API для веб-интерфейса, но я знал, что могу добавить к нему Swagger UI. + +В APIStar была система внедрения зависимостей, которая тоже требовала предварительную регистрацию компонентов, как и ранее описанные инструменты. +Но, тем не менее, это была отличная штука. + +Я не смог использовать его в полноценном проекте, так как были проблемы со встраиванием функций безопасности в схему OpenAPI, из-за которых невозможно было встроить все функции, применяемые в генераторах проектов на основе Flask-apispec. +Я добавил в свой список задач создание пул-реквеста, добавляющего эту функциональность. + +В дальнейшем фокус проекта сместился. + +Это больше не был API-фреймворк, так как автор сосредоточился на Starlette. + +Ныне APIStar - это набор инструментов для проверки спецификаций OpenAPI. + +!!! info "Информация" + APIStar был создан Tom Christie. Тот самый парень, который создал: + + * Django REST Framework + * Starlette (на котором основан **FastAPI**) + * Uvicorn (используемый в Starlette и **FastAPI**) + +!!! check "Идеи для **FastAPI**" + Воплощение. + + Мне казалось блестящей идеей объявлять множество функций (проверка данных, сериализация, документация) с помощью одних и тех же типов Python, которые при этом обеспечивают ещё и помощь редактора кода. + + После долгих поисков среди похожих друг на друга фреймворков и сравнения их различий, APIStar стал самым лучшим выбором. + + Но APIStar перестал быть фреймворком для создания веб-сервера, зато появился Starlette, новая и лучшая основа для построения подобных систем. + Это была последняя капля, сподвигнувшая на создание **FastAPI**. + + Я считаю **FastAPI** "духовным преемником" APIStar, улучившим его возможности благодаря урокам, извлечённым из всех упомянутых выше инструментов. + +## Что используется в **FastAPI** + +### Pydantic + +Pydantic - это библиотека для валидации данных, сериализации и документирования (используя JSON Schema), основываясь на подсказках типов Python, что делает его чрезвычайно интуитивным. + +Его можно сравнить с Marshmallow, хотя в бенчмарках Pydantic быстрее, чем Marshmallow. +И он основан на тех же подсказках типов, которые отлично поддерживаются редакторами кода. + +!!! check "**FastAPI** использует Pydantic" + Для проверки данных, сериализации данных и автоматической документации моделей (на основе JSON Schema). + + Затем **FastAPI** берёт эти схемы JSON и помещает их в схему OpenAPI, не касаясь других вещей, которые он делает. + +### Starlette + +Starlette - это легковесный ASGI фреймворк/набор инструментов, который идеален для построения высокопроизводительных асинхронных сервисов. + +Starlette очень простой и интуитивный. +Он разработан таким образом, чтобы быть легко расширяемым и иметь модульные компоненты. + +В нём есть: + +* Впечатляющая производительность. +* Поддержка веб-сокетов. +* Фоновые задачи. +* Обработка событий при старте и финише приложения. +* Тестовый клиент на основе HTTPX. +* Поддержка CORS, сжатие GZip, статические файлы, потоковая передача данных. +* Поддержка сессий и куки. +* 100% покрытие тестами. +* 100% аннотированный код. +* Несколько жёстких зависимостей. + +В настоящее время Starlette показывает самую высокую скорость среди Python-фреймворков в тестовых замерах. +Быстрее только Uvicorn, который является сервером, а не фреймворком. + +Starlette обеспечивает весь функционал микрофреймворка, но не предоставляет автоматическую валидацию данных, сериализацию и документацию. + +**FastAPI** добавляет эти функции используя подсказки типов Python и Pydantic. +Ещё **FastAPI** добавляет систему внедрения зависимостей, утилиты безопасности, генерацию схемы OpenAPI и т.д. + +!!! note "Технические детали" + ASGI - это новый "стандарт" разработанный участниками команды Django. + Он пока что не является "стандартом в Python" (то есть принятым PEP), но процесс принятия запущен. + + Тем не менее он уже используется в качестве "стандарта" несколькими инструментами. + Это значительно улучшает совместимость, поскольку Вы можете переключиться с Uvicorn на любой другой ASGI-сервер (например, Daphne или Hypercorn) или Вы можете добавить ASGI-совместимые инструменты, такие как `python-socketio`. + +!!! check "**FastAPI** использует Starlette" + В качестве ядра веб-сервиса для обработки запросов, добавив некоторые функции сверху. + + Класс `FastAPI` наследуется напрямую от класса `Starlette`. + + Таким образом, всё что Вы могли делать со Starlette, Вы можете делать с **FastAPI**, по сути это прокачанный Starlette. + +### Uvicorn + +Uvicorn - это молниеносный ASGI-сервер, построенный на uvloop и httptools. + +Uvicorn является сервером, а не фреймворком. +Например, он не предоставляет инструментов для маршрутизации запросов по ресурсам. +Для этого нужна надстройка, такая как Starlette (или **FastAPI**). + +Он рекомендуется в качестве сервера для Starlette и **FastAPI**. + +!!! check "**FastAPI** рекомендует его" + Как основной сервер для запуска приложения **FastAPI**. + + Вы можете объединить его с Gunicorn, чтобы иметь асинхронный многопроцессный сервер. + + Узнать больше деталей можно в разделе [Развёртывание](deployment/index.md){.internal-link target=_blank}. + +## Тестовые замеры и скорость + +Чтобы понять, сравнить и увидеть разницу между Uvicorn, Starlette и FastAPI, ознакомьтесь с разделом [Тестовые замеры](benchmarks.md){.internal-link target=_blank}. diff --git a/docs/ru/docs/benchmarks.md b/docs/ru/docs/benchmarks.md new file mode 100644 index 0000000000..259dca8e67 --- /dev/null +++ b/docs/ru/docs/benchmarks.md @@ -0,0 +1,37 @@ +# Замеры производительности + +Независимые тесты производительности приложений от TechEmpower показывают, что **FastAPI** под управлением Uvicorn один из самых быстрых Python-фреймворков и уступает только Starlette и Uvicorn (которые используются в FastAPI). (*) + +Но при просмотре и сравнении замеров производительности следует иметь в виду нижеописанное. + +## Замеры производительности и скорости + +В подобных тестах часто можно увидеть, что инструменты разного типа сравнивают друг с другом, как аналогичные. + +В частности, сравнивают вместе Uvicorn, Starlette и FastAPI (среди многих других инструментов). + +Чем проще проблема, которую решает инструмент, тем выше его производительность. И большинство тестов не проверяют дополнительные функции, предоставляемые инструментом. + +Иерархия инструментов имеет следующий вид: + +* **Uvicorn**: ASGI-сервер + * **Starlette** (использует Uvicorn): веб-микрофреймворк + * **FastAPI** (использует Starlette): API-микрофреймворк с дополнительными функциями для создания API, с валидацией данных и т.д. + +* **Uvicorn**: + * Будет иметь наилучшую производительность, так как не имеет большого количества дополнительного кода, кроме самого сервера. + * Вы не будете писать приложение на Uvicorn напрямую. Это означало бы, что Ваш код должен включать как минимум весь + код, предоставляемый Starlette (или **FastAPI**). И если Вы так сделаете, то в конечном итоге Ваше приложение будет иметь те же накладные расходы, что и при использовании фреймворка, минимизирующего код Вашего приложения и Ваши ошибки. + * Uvicorn подлежит сравнению с Daphne, Hypercorn, uWSGI и другими веб-серверами. + +* **Starlette**: + * Будет уступать Uvicorn по производительности. Фактически Starlette управляется Uvicorn и из-за выполнения большего количества кода он не может быть быстрее, чем Uvicorn. + * Зато он предоставляет Вам инструменты для создания простых веб-приложений с обработкой маршрутов URL и т.д. + * Starlette следует сравнивать с Sanic, Flask, Django и другими веб-фреймворками (или микрофреймворками). + +* **FastAPI**: + * Так же как Starlette использует Uvicorn и не может быть быстрее него, **FastAPI** использует Starlette, то есть он не может быть быстрее Starlette. + * FastAPI предоставляет больше возможностей поверх Starlette, которые наверняка Вам понадобятся при создании API, такие как проверка данных и сериализация. В довесок Вы ещё и получаете автоматическую документацию (автоматическая документация даже не увеличивает накладные расходы при работе приложения, так как она создается при запуске). + * Если Вы не используете FastAPI, а используете Starlette напрямую (или другой инструмент вроде Sanic, Flask, Responder и т.д.), Вам пришлось бы самостоятельно реализовать валидацию и сериализацию данных. То есть, в итоге, Ваше приложение имело бы такие же накладные расходы, как если бы оно было создано с использованием FastAPI. И во многих случаях валидация и сериализация данных представляют собой самый большой объём кода, написанного в приложениях. + * Таким образом, используя FastAPI Вы потратите меньше времени на разработку, уменьшите количество ошибок, строк кода и, вероятно, получите ту же производительность (или лучше), как и если бы не использовали его (поскольку Вам пришлось бы реализовать все его возможности в своем коде). + * FastAPI должно сравнивать с фреймворками веб-приложений (или наборами инструментов), которые обеспечивают валидацию и сериализацию данных, а также предоставляют автоматическую документацию, такими как Flask-apispec, NestJS, Molten и им подобные. diff --git a/docs/ru/docs/contributing.md b/docs/ru/docs/contributing.md new file mode 100644 index 0000000000..f9b8912e55 --- /dev/null +++ b/docs/ru/docs/contributing.md @@ -0,0 +1,469 @@ +# Участие в разработке фреймворка + +Возможно, для начала Вам стоит ознакомиться с основными способами [помочь FastAPI или получить помощь](help-fastapi.md){.internal-link target=_blank}. + +## Разработка + +Если Вы уже склонировали репозиторий и знаете, что Вам нужно более глубокое погружение в код фреймворка, то здесь представлены некоторые инструкции по настройке виртуального окружения. + +### Виртуальное окружение с помощью `venv` + +Находясь в нужной директории, Вы можете создать виртуальное окружение при помощи Python модуля `venv`. + +
+ +```console +$ python -m venv env +``` + +
+ +Эта команда создаст директорию `./env/` с бинарными (двоичными) файлами Python, а затем Вы сможете скачивать и устанавливать необходимые библиотеки в изолированное виртуальное окружение. + +### Активация виртуального окружения + +Активируйте виртуально окружение командой: + +=== "Linux, macOS" + +
+ + ```console + $ source ./env/bin/activate + ``` + +
+ +=== "Windows PowerShell" + +
+ + ```console + $ .\env\Scripts\Activate.ps1 + ``` + +
+ +=== "Windows Bash" + + Если Вы пользуетесь Bash для Windows (например: 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`, значит всё в порядке. 🎉 + +Во избежание ошибок в дальнейших шагах, удостоверьтесь, что в Вашем виртуальном окружении установлена последняя версия `pip`: + +
+ +```console +$ python -m pip install --upgrade pip + +---> 100% +``` + +
+ +!!! tip "Подсказка" + Каждый раз, перед установкой новой библиотеки в виртуальное окружение при помощи `pip`, не забудьте активировать это виртуальное окружение. + + Это гарантирует, что если Вы используете библиотеку, установленную этим пакетом, то Вы используете библиотеку из Вашего локального окружения, а не любую другую, которая может быть установлена глобально. + +### pip + +После активации виртуального окружения, как было указано ранее, введите следующую команду: + +
+ +```console +$ pip install -r requirements.txt + +---> 100% +``` + +
+ +Это установит все необходимые зависимости в локальное окружение для Вашего локального FastAPI. + +#### Использование локального FastAPI + +Если Вы создаёте Python файл, который импортирует и использует FastAPI,а затем запускаете его интерпретатором Python из Вашего локального окружения, то он будет использовать код из локального FastAPI. + +И, так как при вводе вышеупомянутой команды был указан флаг `-e`, если Вы измените код локального FastAPI, то при следующем запуске этого файла, он будет использовать свежую версию локального FastAPI, который Вы только что изменили. + +Таким образом, Вам не нужно "переустанавливать" Вашу локальную версию, чтобы протестировать каждое изменение. + +### Форматировние + +Скачанный репозиторий содержит скрипт, который может отформатировать и подчистить Ваш код: + +
+ +```console +$ bash scripts/format.sh +``` + +
+ +Заодно он упорядочит Ваши импорты. + +Чтобы он сортировал их правильно, необходимо, чтобы FastAPI был установлен локально в Вашей среде, с помощью команды из раздела выше, использующей флаг `-e`. + +## Документация + +Прежде всего, убедитесь, что Вы настроили своё окружение, как описано выше, для установки всех зависимостей. + +Документация использует MkDocs. + +Также существуют дополнительные инструменты/скрипты для работы с переводами в `./scripts/docs.py`. + +!!! tip "Подсказка" + + Нет необходимости заглядывать в `./scripts/docs.py`, просто используйте это в командной строке. + +Вся документация имеет формат Markdown и расположена в директории `./docs/en/`. + +Многие руководства содержат блоки кода. + +В большинстве случаев эти блоки кода представляют собой вполне законченные приложения, которые можно запускать как есть. + +На самом деле, эти блоки кода не написаны внутри Markdown, это Python файлы в директории `./docs_src/`. + +И эти 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 (опционально) + + +Приведенная ранее инструкция показала Вам, как запускать скрипт `./scripts/docs.py` непосредственно через интерпретатор `python` . + +Но также можно использовать 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`. + +### Переводы на другие языки + +Помощь с переводами ценится КРАЙНЕ ВЫСОКО! И переводы не могут быть сделаны без помощи сообщества. 🌎 🚀 + +Ниже приведены шаги, как помочь с переводами. + +#### Подсказки и инструкции + +* Проверьте существующие пул-реквесты для Вашего языка. Добавьте отзывы с просьбой внести изменения, если они необходимы, или одобрите их. + +!!! tip "Подсказка" + Вы можете добавлять комментарии с предложениями по изменению в существующие пул-реквесты. + + Ознакомьтесь с документацией о добавлении отзыва к пул-реквесту, чтобы утвердить его или запросить изменения. + +* Проверьте проблемы и вопросы, чтобы узнать, есть ли кто-то, координирующий переводы для Вашего языка. + +* Добавляйте один пул-реквест для каждой отдельной переведённой страницы. Это значительно облегчит другим его просмотр. + +Для языков, которые я не знаю, прежде чем добавить перевод в основную ветку, я подожду пока несколько других участников сообщества проверят его. + +* Вы также можете проверить, есть ли переводы для Вашего языка и добавить к ним отзыв, который поможет мне убедиться в правильности перевода. Тогда я смогу объединить его с основной веткой. + +* Используйте те же самые примеры кода Python. Переводите только текст документации. Вам не нужно ничего менять, чтобы эти примеры работали. + +* Используйте те же самые изображения, имена файлов и ссылки. Вы не должны менять ничего для сохранения работоспособности. + +* Чтобы узнать 2-буквенный код языка, на который Вы хотите сделать перевод, Вы можете воспользоваться таблицей Список кодов языков ISO 639-1. + +#### Существующий язык + +Допустим, Вы хотите перевести страницу на язык, на котором уже есть какие-то переводы, например, на испанский. + +Кодом испанского языка является `es`. А значит директория для переводов на испанский язык: `docs/es/`. + +!!! tip "Подсказка" + Главный ("официальный") язык - английский, директория для него `docs/en/`. + +Вы можете запустить сервер документации на испанском: + +
+ +```console +// Используйте команду "live" и передайте код языка в качестве аргумента командной строки +$ 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.md){.internal-link target=_blank}. + +* Скопируйте файл: + +``` +docs/en/docs/features.md +``` + +* Вставьте его точно в то же место, но в директорию языка, на который Вы хотите сделать перевод, например: + +``` +docs/es/docs/features.md +``` + +!!! tip "Подсказка" + Заметьте, что в пути файла мы изменили только код языка с `en` на `es`. + +* Теперь откройте файл конфигурации MkDocs для английского языка, расположенный тут: + +``` +docs/en/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/mkdocs.yml +``` + +* Добавьте строку `docs/features.md` точно в то же место, как и в случае для английского, как-то так: + +```YAML hl_lines="8" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +``` + +Убедитесь, что при наличии других записей, новая запись с Вашим переводом находится точно в том же порядке, что и в английской версии. + +Если Вы зайдёте в свой браузер, то увидите, что в документации стал отображаться Ваш новый раздел.🎉 + +Теперь Вы можете переводить эту страницу и смотреть, как она выглядит при сохранении файла. + +#### Новый язык + +Допустим, Вы хотите добавить перевод для языка, на который пока что не переведена ни одна страница. + +Скажем, Вы решили сделать перевод для креольского языка, но его еще нет в документации. + +Перейдите в таблицу кодов языков по ссылке указанной выше, где найдёте, что кодом креольского языка является `ht`. + +Затем запустите скрипт, генерирующий директорию для переводов на новые языки: + +
+ +```console +// Используйте команду new-lang и передайте код языка в качестве аргумента командной строки +$ python ./scripts/docs.py new-lang ht + +Successfully initialized: docs/ht +Updating ht +Updating en +``` + +
+ +После чего Вы можете проверить в своем редакторе кода, что появился новый каталог `docs/ht/`. + +!!! tip "Подсказка" + Создайте первый пул-реквест, который будет содержать только пустую директорию для нового языка, прежде чем добавлять переводы. + + Таким образом, другие участники могут переводить другие страницы, пока Вы работаете над одной. 🚀 + +Начните перевод с главной страницы `docs/ht/index.md`. + +В дальнейшем можно действовать, как указано в предыдущих инструкциях для "существующего языка". + +##### Новый язык не поддерживается + +Если при запуске скрипта `./scripts/docs.py live` Вы получаете сообщение об ошибке, что язык не поддерживается, что-то вроде: + +``` + raise TemplateNotFound(template) +jinja2.exceptions.TemplateNotFound: partials/language/xx.html +``` + +Сие означает, что тема не поддерживает этот язык (в данном случае с поддельным 2-буквенным кодом `xx`). + +Но не стоит переживать. Вы можете установить языком темы английский, а затем перевести текст документации. + +Если возникла такая необходимость, отредактируйте `mkdocs.yml` для Вашего нового языка. Это будет выглядеть как-то так: + +```YAML hl_lines="5" +site_name: FastAPI +# More stuff +theme: + # More stuff + language: xx +``` + +Измените `xx` (код Вашего языка) на `en` и перезапустите сервер. + +#### Предпросмотр результата + +Когда Вы запускаете скрипт `./scripts/docs.py` с командой `live`, то будут показаны файлы и переводы для указанного языка. + +Но когда Вы закончите, то можете посмотреть, как это будет выглядеть по-настоящему. + +Для этого сначала создайте всю документацию: + +
+ +```console +// Используйте команду "build-all", это займёт немного времени +$ 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 +// Используйте команду "serve" после того, как отработает команда "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`. Открыв его в Вашем браузере, Вы можете в интерактивном режиме изучить, все ли части кода охвачены тестами. diff --git a/docs/ru/docs/deployment/concepts.md b/docs/ru/docs/deployment/concepts.md new file mode 100644 index 0000000000..681acf15ea --- /dev/null +++ b/docs/ru/docs/deployment/concepts.md @@ -0,0 +1,311 @@ +# Концепции развёртывания + +Существует несколько концепций, применяемых для развёртывания приложений **FastAPI**, равно как и для любых других типов веб-приложений, среди которых Вы можете выбрать **наиболее подходящий** способ. + +Самые важные из них: + +* Использование более безопасного протокола HTTPS +* Настройки запуска приложения +* Перезагрузка приложения +* Запуск нескольких экземпляров приложения +* Управление памятью +* Использование перечисленных функций перед запуском приложения. + +Рассмотрим ниже влияние каждого из них на процесс **развёртывания**. + +Наша конечная цель - **обслуживать клиентов Вашего API безопасно** и **бесперебойно**, с максимально эффективным использованием **вычислительных ресурсов** (например, удалённых серверов/виртуальных машин). 🚀 + +Здесь я немного расскажу Вам об этих **концепциях** и надеюсь, что у Вас сложится **интуитивное понимание**, какой способ выбрать при развертывании Вашего API в различных окружениях, возможно, даже **ещё не существующих**. + +Ознакомившись с этими концепциями, Вы сможете **оценить и выбрать** лучший способ развёртывании **Вашего API**. + +В последующих главах я предоставлю Вам **конкретные рецепты** развёртывания приложения FastAPI. + +А сейчас давайте остановимся на важных **идеях этих концепций**. Эти идеи можно также применить и к другим типам веб-приложений. 💡 + +## Использование более безопасного протокола HTTPS + +В [предыдущей главе об HTTPS](./https.md){.internal-link target=_blank} мы рассмотрели, как HTTPS обеспечивает шифрование для Вашего API. + +Также мы заметили, что обычно для работы с HTTPS Вашему приложению нужен **дополнительный** компонент - **прокси-сервер завершения работы TLS**. + +И если прокси-сервер не умеет сам **обновлять сертификаты HTTPS**, то нужен ещё один компонент для этого действия. + +### Примеры инструментов для работы с HTTPS + +Вот некоторые инструменты, которые Вы можете применять как прокси-серверы: + +* Traefik + * С автоматическим обновлением сертификатов ✨ +* Caddy + * С автоматическим обновлением сертификатов ✨ +* Nginx + * С дополнительным компонентом типа Certbot для обновления сертификатов +* HAProxy + * С дополнительным компонентом типа Certbot для обновления сертификатов +* Kubernetes с Ingress Controller похожим на Nginx + * С дополнительным компонентом типа cert-manager для обновления сертификатов +* Использование услуг облачного провайдера (читайте ниже 👇) + +В последнем варианте Вы можете воспользоваться услугами **облачного сервиса**, который сделает большую часть работы, включая настройку HTTPS. Это может наложить дополнительные ограничения или потребовать дополнительную плату и т.п. Зато Вам не понадобится самостоятельно заниматься настройками прокси-сервера. + +В дальнейшем я покажу Вам некоторые конкретные примеры их применения. + +--- + +Следующие концепции рассматривают применение программы, запускающей Ваш API (такой как Uvicorn). + +## Программа и процесс + +Мы часто будем встречать слова **процесс** и **программа**, потому следует уяснить отличия между ними. + +### Что такое программа + +Термином **программа** обычно описывают множество вещей: + +* **Код**, который Вы написали, в нашем случае **Python-файлы**. +* **Файл**, который может быть **исполнен** операционной системой, например `python`, `python.exe` или `uvicorn`. +* Конкретная программа, **запущенная** операционной системой и использующая центральный процессор и память. В таком случае это также называется **процесс**. + +### Что такое процесс + +Термин **процесс** имеет более узкое толкование, подразумевая что-то, запущенное операционной системой (как в последнем пункте из вышестоящего абзаца): + +* Конкретная программа, **запущенная** операционной системой. + * Это не имеет отношения к какому-либо файлу или коду, но нечто **определённое**, управляемое и **выполняемое** операционной системой. +* Любая программа, любой код, **могут делать что-то** только когда они **выполняются**. То есть, когда являются **работающим процессом**. +* Процесс может быть **прерван** (или "убит") Вами или Вашей операционной системой. В результате чего он перестанет исполняться и **не будет продолжать делать что-либо**. +* Каждое приложение, которое Вы запустили на своём компьютере, каждая программа, каждое "окно" запускает какой-то процесс. И обычно на включенном компьютере **одновременно** запущено множество процессов. +* И **одна программа** может запустить **несколько параллельных процессов**. + +Если Вы заглянете в "диспетчер задач" или "системный монитор" (или аналогичные инструменты) Вашей операционной системы, то увидите множество работающих процессов. + +Вполне вероятно, что Вы увидите несколько процессов с одним и тем же названием браузерной программы (Firefox, Chrome, Edge и т. Д.). Обычно браузеры запускают один процесс на вкладку и вдобавок некоторые дополнительные процессы. + + + +--- + +Теперь, когда нам известна разница между **процессом** и **программой**, давайте продолжим обсуждение развёртывания. + +## Настройки запуска приложения + +В большинстве случаев когда Вы создаёте веб-приложение, то желаете, чтоб оно **работало постоянно** и непрерывно, предоставляя клиентам доступ в любое время. Хотя иногда у Вас могут быть причины, чтоб оно запускалось только при определённых условиях. + +### Удалённый сервер + +Когда Вы настраиваете удалённый сервер (облачный сервер, виртуальную машину и т.п.), самое простое, что можно сделать, запустить Uvicorn (или его аналог) вручную, как Вы делаете при локальной разработке. + +Это рабочий способ и он полезен **во время разработки**. + +Но если Вы потеряете соединение с сервером, то не сможете отслеживать - работает ли всё ещё **запущенный Вами процесс**. + +И если сервер перезагрузится (например, после обновления или каких-то действий облачного провайдера), Вы скорее всего **этого не заметите**, чтобы снова запустить процесс вручную. Вследствие этого Ваш API останется мёртвым. 😱 + +### Автоматический запуск программ + +Вероятно Вы пожелаете, чтоб Ваша серверная программа (такая как Uvicorn) стартовала автоматически при включении сервера, без **человеческого вмешательства** и всегда могла управлять Вашим API (так как Uvicorn запускает приложение FastAPI). + +### Отдельная программа + +Для этого у обычно используют отдельную программу, которая следит за тем, чтобы Ваши приложения запускались при включении сервера. Такой подход гарантирует, что другие компоненты или приложения также будут запущены, например, база данных + +### Примеры инструментов, управляющих запуском программ + +Вот несколько примеров, которые могут справиться с такой задачей: + +* Docker +* Kubernetes +* Docker Compose +* Docker в режиме Swarm +* Systemd +* Supervisor +* Использование услуг облачного провайдера +* Прочие... + +Я покажу Вам некоторые примеры их использования в следующих главах. + +## Перезапуск + +Вы, вероятно, также пожелаете, чтоб Ваше приложение **перезапускалось**, если в нём произошёл сбой. + +### Мы ошибаемся + +Все люди совершают **ошибки**. Программное обеспечение почти *всегда* содержит **баги** спрятавшиеся в разных местах. 🐛 + +И мы, будучи разработчиками, продолжаем улучшать код, когда обнаруживаем в нём баги или добавляем новый функционал (возможно, добавляя при этом баги 😅). + +### Небольшие ошибки обрабатываются автоматически + +Когда Вы создаёте свои API на основе FastAPI и допускаете в коде ошибку, то FastAPI обычно остановит её распространение внутри одного запроса, при обработке которого она возникла. 🛡 + +Клиент получит ошибку **500 Internal Server Error** в ответ на свой запрос, но приложение не сломается и будет продолжать работать с последующими запросами. + +### Большие ошибки - Падение приложений + +Тем не менее, может случиться так, что ошибка вызовет **сбой всего приложения** или даже сбой в Uvicorn, а то и в самом Python. 💥 + +Но мы всё ещё хотим, чтобы приложение **продолжало работать** несмотря на эту единственную ошибку, обрабатывая, как минимум, запросы к *операциям пути* не имеющим ошибок. + +### Перезапуск после падения + +Для случаев, когда ошибки приводят к сбою в запущенном **процессе**, Вам понадобится добавить компонент, который **перезапустит** процесс хотя бы пару раз... + +!!! tip "Заметка" + ... Если приложение падает сразу же после запуска, вероятно бесполезно его бесконечно перезапускать. Но полагаю, Вы заметите такое поведение во время разработки или, по крайней мере, сразу после развёртывания. + + Так что давайте сосредоточимся на конкретных случаях, когда приложение может полностью выйти из строя, но всё ещё есть смысл его запустить заново. + +Возможно Вы захотите, чтоб был некий **внешний компонент**, ответственный за перезапуск Вашего приложения даже если уже не работает Uvicorn или Python. То есть ничего из того, что написано в Вашем коде внутри приложения, не может быть выполнено в принципе. + +### Примеры инструментов для автоматического перезапуска + +В большинстве случаев инструменты **запускающие программы при старте сервера** умеют **перезапускать** эти программы. + +В качестве примера можно взять те же: + +* Docker +* Kubernetes +* Docker Compose +* Docker в режиме Swarm +* Systemd +* Supervisor +* Использование услуг облачного провайдера +* Прочие... + +## Запуск нескольких экземпляров приложения (Репликация) - Процессы и память + +Приложение FastAPI, управляемое серверной программой (такой как Uvicorn), запускается как **один процесс** и может обслуживать множество клиентов одновременно. + +Но часто Вам может понадобиться несколько одновременно работающих одинаковых процессов. + +### Множество процессов - Воркеры (Workers) + +Если количество Ваших клиентов больше, чем может обслужить один процесс (допустим, что виртуальная машина не слишком мощная), но при этом Вам доступно **несколько ядер процессора**, то Вы можете запустить **несколько процессов** одного и того же приложения параллельно и распределить запросы между этими процессами. + +**Несколько запущенных процессов** одной и той же API-программы часто называют **воркерами**. + +### Процессы и порты́ + +Помните ли Вы, как на странице [Об HTTPS](./https.md){.internal-link target=_blank} мы обсуждали, что на сервере только один процесс может слушать одну комбинацию IP-адреса и порта? + +С тех пор ничего не изменилось. + +Соответственно, чтобы иметь возможность работать с **несколькими процессами** одновременно, должен быть **один процесс, прослушивающий порт** и затем каким-либо образом передающий данные каждому рабочему процессу. + +### У каждого процесса своя память + +Работающая программа загружает в память данные, необходимые для её работы, например, переменные содержащие модели машинного обучения или большие файлы. Каждая переменная **потребляет некоторое количество оперативной памяти (RAM)** сервера. + +Обычно процессы **не делятся памятью друг с другом**. Сие означает, что каждый работающий процесс имеет свои данные, переменные и свой кусок памяти. И если для выполнения Вашего кода процессу нужно много памяти, то **каждый такой же процесс** запущенный дополнительно, потребует такого же количества памяти. + +### Память сервера + +Допустим, что Ваш код загружает модель машинного обучения **размером 1 ГБ**. Когда Вы запустите своё API как один процесс, он займёт в оперативной памяти не менее 1 ГБ. А если Вы запустите **4 таких же процесса** (4 воркера), то каждый из них займёт 1 ГБ оперативной памяти. В результате Вашему API потребуется **4 ГБ оперативной памяти (RAM)**. + +И если Ваш удалённый сервер или виртуальная машина располагает только 3 ГБ памяти, то попытка загрузить в неё 4 ГБ данных вызовет проблемы. 🚨 + +### Множество процессов - Пример + +В этом примере **менеджер процессов** запустит и будет управлять двумя **воркерами**. + +Менеджер процессов будет слушать определённый **сокет** (IP:порт) и передавать данные работающим процессам. + +Каждый из этих процессов будет запускать Ваше приложение для обработки полученного **запроса** и возвращения вычисленного **ответа** и они будут использовать оперативную память. + + + +Безусловно, на этом же сервере будут работать и **другие процессы**, которые не относятся к Вашему приложению. + +Интересная деталь - обычно в течение времени процент **использования центрального процессора (CPU)** каждым процессом может очень сильно **изменяться**, но объём занимаемой **оперативной памяти (RAM)** остаётся относительно **стабильным**. + +Если у Вас есть API, который каждый раз выполняет сопоставимый объем вычислений, и у Вас много клиентов, то **загрузка процессора**, вероятно, *также будет стабильной* (вместо того, чтобы постоянно быстро увеличиваться и уменьшаться). + +### Примеры стратегий и инструментов для запуска нескольких экземпляров приложения + +Существует несколько подходов для достижения целей репликации и я расскажу Вам больше о конкретных стратегиях в следующих главах, например, когда речь пойдет о Docker и контейнерах. + +Основное ограничение при этом - только **один** компонент может работать с определённым **портом публичного IP**. И должен быть способ **передачи** данных между этим компонентом и копиями **процессов/воркеров**. + +Вот некоторые возможные комбинации и стратегии: + +* **Gunicorn** управляющий **воркерами Uvicorn** + * Gunicorn будет выступать как **менеджер процессов**, прослушивая **IP:port**. Необходимое количество запущенных экземпляров приложения будет осуществляться посредством запуска **множества работающих процессов Uvicorn**. +* **Uvicorn** управляющий **воркерами Uvicorn** + * Один процесс Uvicorn будет выступать как **менеджер процессов**, прослушивая **IP:port**. Он будет запускать **множество работающих процессов Uvicorn**. +* **Kubernetes** и аналогичные **контейнерные системы** + * Какой-то компонент в **Kubernetes** будет слушать **IP:port**. Необходимое количество запущенных экземпляров приложения будет осуществляться посредством запуска **нескольких контейнеров**, в каждом из которых работает **один процесс Uvicorn**. +* **Облачные сервисы**, которые позаботятся обо всём за Вас + * Возможно, что облачный сервис умеет **управлять запуском дополнительных экземпляров приложения**. Вероятно, он потребует, чтоб Вы указали - какой **процесс** или **образ** следует клонировать. Скорее всего, Вы укажете **один процесс Uvicorn** и облачный сервис будет запускать его копии при необходимости. + +!!! tip "Заметка" + Если Вы не знаете, что такое **контейнеры**, Docker или Kubernetes, не переживайте. + + Я поведаю Вам о контейнерах, образах, Docker, Kubernetes и т.п. в главе: [FastAPI внутри контейнеров - Docker](./docker.md){.internal-link target=_blank}. + +## Шаги, предшествующие запуску + +Часто бывает, что Вам необходимо произвести какие-то подготовительные шаги **перед запуском** своего приложения. + +Например, запустить **миграции базы данных**. + +Но в большинстве случаев такие действия достаточно произвести **однократно**. + +Поэтому Вам нужен будет **один процесс**, выполняющий эти **подготовительные шаги** до запуска приложения. + +Также Вам нужно будет убедиться, что этот процесс выполнил подготовительные шаги *даже* если впоследствии Вы запустите **несколько процессов** (несколько воркеров) самого приложения. Если бы эти шаги выполнялись в каждом **клонированном процессе**, они бы **дублировали** работу, пытаясь выполнить её **параллельно**. И если бы эта работа была бы чем-то деликатным, вроде миграции базы данных, то это может вызвать конфликты между ними. + +Безусловно, возможны случаи, когда нет проблем при выполнении предварительной подготовки параллельно или несколько раз. Тогда Вам повезло, работать с ними намного проще. + +!!! tip "Заметка" + Имейте в виду, что в некоторых случаях запуск Вашего приложения **может не требовать каких-либо предварительных шагов вовсе**. + + Что ж, тогда Вам не нужно беспокоиться об этом. 🤷 + +### Примеры стратегий запуска предварительных шагов + +Существует **сильная зависимость** от того, как Вы **развёртываете свою систему**, запускаете программы, обрабатываете перезапуски и т.д. + +Вот некоторые возможные идеи: + +* При использовании Kubernetes нужно предусмотреть "инициализирующий контейнер", запускаемый до контейнера с приложением. +* Bash-скрипт, выполняющий предварительные шаги, а затем запускающий приложение. + * При этом Вам всё ещё нужно найти способ - как запускать/перезапускать *такой* bash-скрипт, обнаруживать ошибки и т.п. + +!!! tip "Заметка" + Я приведу Вам больше конкретных примеров работы с контейнерами в главе: [FastAPI внутри контейнеров - Docker](./docker.md){.internal-link target=_blank}. + +## Утилизация ресурсов + +Ваш сервер располагает ресурсами, которые Ваши программы могут потреблять или **утилизировать**, а именно - время работы центрального процессора и объём оперативной памяти. + +Как много системных ресурсов Вы предполагаете потребить/утилизировать? Если не задумываться, то можно ответить - "немного", но на самом деле Вы, вероятно, пожелаете использовать **максимально возможное количество**. + +Если Вы платите за содержание трёх серверов, но используете лишь малую часть системных ресурсов каждого из них, то Вы **выбрасываете деньги на ветер**, а также **впустую тратите электроэнергию** и т.п. + +В таком случае было бы лучше обойтись двумя серверами, но более полно утилизировать их ресурсы (центральный процессор, оперативную память, жёсткий диск, сети передачи данных и т.д). + +С другой стороны, если Вы располагаете только двумя серверами и используете **на 100% их процессоры и память**, но какой-либо процесс запросит дополнительную память, то операционная система сервера будет использовать жёсткий диск для расширения оперативной памяти (а диск работает в тысячи раз медленнее), а то вовсе **упадёт**. Или если какому-то процессу понадобится произвести вычисления, то ему придётся подождать, пока процессор освободится. + +В такой ситуации лучше подключить **ещё один сервер** и перераспределить процессы между серверами, чтоб всем **хватало памяти и процессорного времени**. + +Также есть вероятность, что по какой-то причине возник **всплеск** запросов к Вашему API. Возможно, это был вирус, боты или другие сервисы начали пользоваться им. И для таких происшествий Вы можете захотеть иметь дополнительные ресурсы. + +При настройке логики развёртываний, Вы можете указать **целевое значение** утилизации ресурсов, допустим, **от 50% до 90%**. Обычно эти метрики и используют. + +Вы можете использовать простые инструменты, такие как `htop`, для отслеживания загрузки центрального процессора и оперативной памяти сервера, в том числе каждым процессом. Или более сложные системы мониторинга нескольких серверов. + +## Резюме + +Вы прочитали некоторые из основных концепций, которые необходимо иметь в виду при принятии решения о развертывании приложений: + +* Использование более безопасного протокола HTTPS +* Настройки запуска приложения +* Перезагрузка приложения +* Запуск нескольких экземпляров приложения +* Управление памятью +* Использование перечисленных функций перед запуском приложения. + +Осознание этих идей и того, как их применять, должно дать Вам интуитивное понимание, необходимое для принятия решений при настройке развертываний. 🤓 + +В следующих разделах я приведу более конкретные примеры возможных стратегий, которым Вы можете следовать. 🚀 diff --git a/docs/ru/docs/deployment/docker.md b/docs/ru/docs/deployment/docker.md new file mode 100644 index 0000000000..f045ca9448 --- /dev/null +++ b/docs/ru/docs/deployment/docker.md @@ -0,0 +1,700 @@ +# FastAPI и Docker-контейнеры + +При развёртывании приложений FastAPI, часто начинают с создания **образа контейнера на основе Linux**. Обычно для этого используют **Docker**. Затем можно развернуть такой контейнер на сервере одним из нескольких способов. + +Использование контейнеров на основе Linux имеет ряд преимуществ, включая **безопасность**, **воспроизводимость**, **простоту** и прочие. + +!!! tip "Подсказка" + Торопитесь или уже знакомы с этой технологией? Перепрыгните на раздел [Создать Docker-образ для FastAPI 👇](#docker-fastapi) + +
+Развернуть Dockerfile 👀 + +```Dockerfile +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] + +# Если используете прокси-сервер, такой как Nginx или Traefik, добавьте --proxy-headers +# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"] +``` + +
+ +## Что такое "контейнер" + +Контейнеризация - это **легковесный** способ упаковать приложение, включая все его зависимости и необходимые файлы, чтобы изолировать его от других контейнеров (других приложений и компонентов) работающих на этой же системе. + +Контейнеры, основанные на Linux, запускаются используя ядро Linux хоста (машины, виртуальной машины, облачного сервера и т.п.). Это значит, что они очень легковесные (по сравнению с полноценными виртуальными машинами, полностью эмулирующими работу операционной системы). + +Благодаря этому, контейнеры потребляют **малое количество ресурсов**, сравнимое с процессом запущенным напрямую (виртуальная машина потребует гораздо больше ресурсов). + +Контейнеры также имеют собственные запущенные **изолированные** процессы (но часто только один процесс), файловую систему и сеть, что упрощает развёртывание, разработку, управление доступом и т.п. + +## Что такое "образ контейнера" + +Для запуска **контейнера** нужен **образ контейнера**. + +Образ контейнера - это **замороженная** версия всех файлов, переменных окружения, программ и команд по умолчанию, необходимых для работы приложения. **Замороженный** - означает, что **образ** не запущен и не выполняется, это всего лишь упакованные вместе файлы и метаданные. + +В отличие от **образа контейнера**, хранящего неизменное содержимое, под термином **контейнер** подразумевают запущенный образ, то есть объёкт, который **исполняется**. + +Когда **контейнер** запущен (на основании **образа**), он может создавать и изменять файлы, переменные окружения и т.д. Эти изменения будут существовать только внутри контейнера, но не будут сохраняться в образе контейнера (не будут сохранены на диск). + +Образ контейнера можно сравнить с файлом, содержащем **программу**, например, как файл `main.py`. + +И **контейнер** (в отличие от **образа**) - это на самом деле выполняемый экземпляр образа, примерно как **процесс**. По факту, контейнер запущен только когда запущены его процессы (чаще, всего один процесс) и остановлен, когда запущенных процессов нет. + +## Образы контейнеров + +Docker является одним оз основных инструментов для создания **образов** и **контейнеров** и управления ими. + +Существует общедоступный Docker Hub с подготовленными **официальными образами** многих инструментов, окружений, баз данных и приложений. + +К примеру, есть официальный образ Python. + +Также там представлены и другие полезные образы, такие как базы данных: + +* PostgreSQL +* MySQL +* MongoDB +* Redis + +и т.п. + +Использование подготовленных образов значительно упрощает **комбинирование** и использование разных инструментов. Например, Вы можете попытаться использовать новую базу данных. В большинстве случаев можно использовать **официальный образ** и всего лишь указать переменные окружения. + +Таким образом, Вы можете изучить, что такое контейнеризация и Docker, и использовать полученные знания с разными инструментами и компонентами. + +Так, Вы можете запустить одновременно **множество контейнеров** с базой данных, Python-приложением, веб-сервером, React-приложением и соединить их вместе через внутреннюю сеть. + +Все системы управления контейнерами (такие, как Docker или Kubernetes) имеют встроенные возможности для организации такого сетевого взаимодействия. + +## Контейнеры и процессы + +Обычно **образ контейнера** содержит метаданные предустановленной программы или команду, которую следует выполнить при запуске **контейнера**. Также он может содержать параметры, передаваемые предустановленной программе. Похоже на то, как если бы Вы запускали такую программу через терминал. + +Когда **контейнер** запущен, он будет выполнять прописанные в нём команды и программы. Но Вы можете изменить его так, чтоб он выполнял другие команды и программы. + +Контейнер буде работать до тех пор, пока выполняется его **главный процесс** (команда или программа). + +В контейнере обычно выполняется **только один процесс**, но от его имени можно запустить другие процессы, тогда в этом же в контейнере будет выполняться **множество процессов**. + +Контейнер не считается запущенным, если в нём **не выполняется хотя бы один процесс**. Если главный процесс остановлен, значит и контейнер остановлен. + +## Создать Docker-образ для FastAPI + +Что ж, давайте ужё создадим что-нибудь! 🚀 + +Я покажу Вам, как собирать **Docker-образ** для FastAPI **с нуля**, основываясь на **официальном образе Python**. + +Такой подход сгодится для **большинства случаев**, например: + +* Использование с **Kubernetes** или аналогичным инструментом +* Запуск в **Raspberry Pi** +* Использование в облачных сервисах, запускающих образы контейнеров для Вас и т.п. + +### Установить зависимости + +Обычно Вашему приложению необходимы **дополнительные библиотеки**, список которых находится в отдельном файле. + +На название и содержание такого файла влияет выбранный Вами инструмент **установки** этих библиотек (зависимостей). + +Чаще всего это простой файл `requirements.txt` с построчным перечислением библиотек и их версий. + +При этом Вы, для выбора версий, будете использовать те же идеи, что упомянуты на странице [О версиях FastAPI](./versions.md){.internal-link target=_blank}. + +Ваш файл `requirements.txt` может выглядеть как-то так: + +``` +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +``` + +Устанавливать зависимости проще всего с помощью `pip`: + +
+ +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic uvicorn +``` + +
+ +!!! info "Информация" + Существуют и другие инструменты управления зависимостями. + + В этом же разделе, но позже, я покажу Вам пример использования Poetry. 👇 + +### Создать приложение **FastAPI** + +* Создайте директорию `app` и перейдите в неё. +* Создайте пустой файл `__init__.py`. +* Создайте файл `main.py` и заполните его: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +### Dockerfile + +В этой же директории создайте файл `Dockerfile` и заполните его: + +```{ .dockerfile .annotate } +# (1) +FROM python:3.9 + +# (2) +WORKDIR /code + +# (3) +COPY ./requirements.txt /code/requirements.txt + +# (4) +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (5) +COPY ./app /code/app + +# (6) +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +``` + +1. Начните с официального образа Python, который будет основой для образа приложения. + +2. Укажите, что в дальнейшем команды запускаемые в контейнере, будут выполняться в директории `/code`. + + Инструкция создаст эту директорию внутри контейнера и мы поместим в неё файл `requirements.txt` и директорию `app`. + +3. Скопируете файл с зависимостями из текущей директории в `/code`. + + Сначала копируйте **только** файл с зависимостями. + + Этот файл **изменяется довольно редко**, Docker ищет изменения при постройке образа и если не находит, то использует **кэш**, в котором хранятся предыдущии версии сборки образа. + +4. Установите библиотеки перечисленные в файле с зависимостями. + + Опция `--no-cache-dir` указывает `pip` не сохранять загружаемые библиотеки на локальной машине для использования их в случае повторной загрузки. В контейнере, в случае пересборки этого шага, они всё равно будут удалены. + + !!! note "Заметка" + Опция `--no-cache-dir` нужна только для `pip`, она никак не влияет на Docker или контейнеры. + + Опция `--upgrade` указывает `pip` обновить библиотеки, емли они уже установлены. + + Ка и в предыдущем шаге с копированием файла, этот шаг также будет использовать **кэш Docker** в случае отсутствия изменений. + + Использрвание кэша, особенно на этом шаге, позволит Вам **сэкономить** кучу времени при повторной сборке образа, так как зависимости будут сохранены в кеше, а не **загружаться и устанавливаться каждый раз**. + +5. Скопируйте директорию `./app` внутрь директории `/code` (в контейнере). + + Так как в этой директории расположен код, который **часто изменяется**, то использование **кэша** на этом шаге будет наименее эффективно, а значит лучше поместить этот шаг **ближе к концу** `Dockerfile`, дабы не терять выгоду от оптимизации предыдущих шагов. + +6. Укажите **команду**, запускающую сервер `uvicorn`. + + `CMD` принимает список строк, разделённых запятыми, но при выполнении объединит их через пробел, собрав из них одну команду, которую Вы могли бы написать в терминале. + + Эта команда будет выполнена в **текущей рабочей директории**, а именно в директории `/code`, котоая указана командой `WORKDIR /code`. + + Так как команда выполняется внутрии директории `/code`, в которую мы поместили папку `./app` с приложением, то **Uvicorn** сможет найти и **импортировать** объект `app` из файла `app.main`. + +!!! tip "Подсказка" + Если ткнёте на кружок с плюсом, то увидите пояснения. 👆 + +На данном этапе структура проекта должны выглядеть так: + +``` +. +├── app +│   ├── __init__.py +│ └── main.py +├── Dockerfile +└── requirements.txt +``` + +#### Использование прокси-сервера + +Если Вы запускаете контейнер за прокси-сервером завершения TLS (балансирующего нагрузку), таким как Nginx или Traefik, добавьте опцию `--proxy-headers`, которая укажет Uvicorn, что он работает позади прокси-сервера и может доверять заголовкам отправляемым им. + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +#### Кэш Docker'а + +В нашем `Dockerfile` использована полезная хитрость, когда сначала копируется **только файл с зависимостями**, а не вся папка с кодом приложения. + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +Docker и подобные ему инструменты **создают** образы контейнеров **пошагово**, добавляя **один слой над другим**, начиная с первой строки `Dockerfile` и добавляя файлы, создаваемые при выполнении каждой инструкции из `Dockerfile`. + +При создании образа используется **внутренний кэш** и если в файлах нет изменений с момента последней сборки образа, то будет **переиспользован** ранее созданный слой образа, а не повторное копирование файлов и создание слоя с нуля. +Заметьте, что так как слой следующего шага зависит от слоя предыдущего, то изменения внесённые в промежуточный слой, также повлияют на последующие. + +Избегание копирования файлов не обязательно улучшит ситуацию, но использование кэша на одном шаге, позволит **использовать кэш и на следующих шагах**. Например, можно использовать кэш при установке зависимостей: + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +Файл со списком зависимостей **изменяется довольно редко**. Так что выполнив команду копирования только этого файла, Docker сможет **использовать кэш** на этом шаге. + +А затем **использовать кэш и на следующем шаге**, загружающем и устанавливающем зависимости. И вот тут-то мы и **сэкономим много времени**. ✨ ...а не будем томиться в тягостном ожидании. 😪😆 + +Для загрузки и установки необходимых библиотек **может понадобиться несколько минут**, но использование **кэша** занимает несколько **секунд** максимум. + +И так как во время разработки Вы будете часто пересобирать контейнер для проверки работоспособности внесённых изменений, то сэкономленные минуты сложатся в часы, а то и дни. + +Так как папка с кодом приложения **изменяется чаще всего**, то мы расположили её в конце `Dockerfile`, ведь после внесённых в код изменений кэш не будет использован на этом и следующих шагах. + +```Dockerfile +COPY ./app /code/app +``` + +### Создать Docker-образ + +Теперь, когда все файлы на своих местах, давайте создадим образ контейнера. + +* Перейдите в директорию проекта (в ту, где расположены `Dockerfile` и папка `app` с приложением). +* Создай образ приложения FastAPI: + +
+ +```console +$ docker build -t myimage . + +---> 100% +``` + +
+ +!!! tip "Подсказка" + Обратите внимание, что в конце написана точка - `.`, это то же самое что и `./`, тем самым мы указываем Docker директорию, из которой нужно выполнять сборку образа контейнера. + + В данном случае это та же самая директория (`.`). + +### Запуск Docker-контейнера + +* Запустите контейнер, основанный на Вашем образе: + +
+ +```console +$ docker run -d --name mycontainer -p 80:80 myimage +``` + +
+ +## Проверка + +Вы можете проверить, что Ваш Docker-контейнер работает перейдя по ссылке: http://192.168.99.100/items/5?q=somequery или http://127.0.0.1/items/5?q=somequery (или похожей, которую использует Ваш Docker-хост). + +Там Вы увидите: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +## Интерактивная документация API + +Теперь перейдите по ссылке http://192.168.99.100/docs или http://127.0.0.1/docs (или похожей, которую использует Ваш Docker-хост). + +Здесь Вы увидите автоматическую интерактивную документацию API (предоставляемую Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +## Альтернативная документация API + +Также Вы можете перейти по ссылке http://192.168.99.100/redoc or http://127.0.0.1/redoc (или похожей, которую использует Ваш Docker-хост). + +Здесь Вы увидите альтернативную автоматическую документацию API (предоставляемую ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Создание Docker-образа на основе однофайлового приложения FastAPI + +Если Ваше приложение FastAPI помещено в один файл, например, `main.py` и структура Ваших файлов похожа на эту: + +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` + +Вам нужно изменить в `Dockerfile` соответствующие пути копирования файлов: + +```{ .dockerfile .annotate hl_lines="10 13" } +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (1) +COPY ./main.py /code/ + +# (2) +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] +``` + +1. Скопируйте непосредственно файл `main.py` в директорию `/code` (не указывайте `./app`). + +2. При запуске Uvicorn укажите ему, что объект `app` нужно импортировать из файла `main` (вместо импортирования из `app.main`). + +Настройте Uvicorn на использование `main` вместо `app.main` для импорта объекта `app`. + +## Концепции развёртывания + +Давайте вспомним о [Концепциях развёртывания](./concepts.md){.internal-link target=_blank} и применим их к контейнерам. + +Контейнеры - это, в основном, инструмент упрощающий **сборку и развёртывание** приложения и они не обязыают к применению какой-то определённой **концепции развёртывания**, а значит мы можем выбирать нужную стратегию. + +**Хорошая новость** в том, что независимо от выбранной стратегии, мы всё равно можем покрыть все концепции развёртывания. 🎉 + +Рассмотрим эти **концепции развёртывания** применительно к контейнерам: + +* Использование более безопасного протокола HTTPS +* Настройки запуска приложения +* Перезагрузка приложения +* Запуск нескольких экземпляров приложения +* Управление памятью +* Использование перечисленных функций перед запуском приложения + +## Использование более безопасного протокола HTTPS + +Если мы определимся, что **образ контейнера** будет содержать только приложение FastAPI, то работу с HTTPS можно организовать **снаружи** контейнера при помощи другого инструмента. + +Это может быть другой контейнер, в котором есть, например, Traefik, работающий с **HTTPS** и **самостоятельно** обновляющий **сертификаты**. + +!!! tip "Подсказка" + Traefik совместим с Docker, Kubernetes и им подобными инструментами. Он очень прост в установке и настройке использования HTTPS для Ваших контейнеров. + +В качестве альтернативы, работу с HTTPS можно доверить облачному провайдеру, если он предоставляет такую услугу. + +## Настройки запуска и перезагрузки приложения + +Обычно **запуском контейнера с приложением** занимается какой-то отдельный инструмент. + +Это может быть сам **Docker**, **Docker Compose**, **Kubernetes**, **облачный провайдер** и т.п. + +В большинстве случаев это простейшие настройки запуска и перезагрузки приложения (при падении). Например, команде запуска Docker-контейнера можно добавить опцию `--restart`. + +Управление запуском и перезагрузкой приложений без использования контейнеров - весьма затруднительно. Но при **работе с контейнерами** - это всего лишь функционал доступный по умолчанию. ✨ + +## Запуск нескольких экземпляров приложения - Указание количества процессов + +Если у Вас есть кластер машин под управлением **Kubernetes**, Docker Swarm Mode, Nomad или аналогичной сложной системой оркестрации контейнеров, скорее всего, вместо использования менеджера процессов (типа Gunicorn и его воркеры) в каждом контейнере, Вы захотите **управлять количеством запущенных экземпляров приложения** на **уровне кластера**. + +В любую из этих систем управления контейнерами обычно встроен способ управления **количеством запущенных контейнеров** для распределения **нагрузки** от входящих запросов на **уровне кластера**. + +В такой ситуации Вы, вероятно, захотите создать **образ Docker**, как [описано выше](#dockerfile), с установленными зависимостями и запускающий **один процесс Uvicorn** вместо того, чтобы запускать Gunicorn управляющий несколькими воркерами Uvicorn. + +### Балансировщик нагрузки + +Обычно при использовании контейнеров один компонент **прослушивает главный порт**. Это может быть контейнер содержащий **прокси-сервер завершения работы TLS** для работы с **HTTPS** или что-то подобное. + +Поскольку этот компонент **принимает запросы** и равномерно **распределяет** их между компонентами, его также называют **балансировщиком нагрузки**. + +!!! tip "Подсказка" + **Прокси-сервер завершения работы TLS** одновременно может быть **балансировщиком нагрузки**. + +Система оркестрации, которую Вы используете для запуска и управления контейнерами, имеет встроенный инструмент **сетевого взаимодействия** (например, для передачи HTTP-запросов) между контейнерами с Вашими приложениями и **балансировщиком нагрузки** (который также может быть **прокси-сервером**). + +### Один балансировщик - Множество контейнеров + +При работе с **Kubernetes** или аналогичными системами оркестрации использование их внутреннней сети позволяет иметь один **балансировщик нагрузки**, который прослушивает **главный** порт и передаёт запросы **множеству запущенных контейнеров** с Вашими приложениями. + +В каждом из контейнеров обычно работает **только один процесс** (например, процесс Uvicorn управляющий Вашим приложением FastAPI). Контейнеры могут быть **идентичными**, запущенными на основе одного и того же образа, но у каждого будут свои отдельные процесс, память и т.п. Таким образом мы получаем преимущества **распараллеливания** работы по **разным ядрам** процессора или даже **разным машинам**. + +Система управления контейнерами с **балансировщиком нагрузки** будет **распределять запросы** к контейнерам с приложениями **по очереди**. То есть каждый запрос будет обработан одним из множества **одинаковых контейнеров** с одним и тем же приложением. + +**Балансировщик нагрузки** может обрабатывать запросы к *разным* приложениям, расположенным в Вашем кластере (например, если у них разные домены или префиксы пути) и передавать запросы нужному контейнеру с требуемым приложением. + +### Один процесс на контейнер + +В этом варианте **в одном контейнере будет запущен только один процесс (Uvicorn)**, а управление изменением количества запущенных копий приложения происходит на уровне кластера. + +Здесь **не нужен** менеджер процессов типа Gunicorn, управляющий процессами Uvicorn, или же Uvicorn, управляющий другими процессами Uvicorn. Достаточно **только одного процесса Uvicorn** на контейнер (но запуск нескольких процессов не запрещён). + +Использование менеджера процессов (Gunicorn или Uvicorn) внутри контейнера только добавляет **излишнее усложнение**, так как управление следует осуществлять системой оркестрации. + +### Множество процессов внутри контейнера для особых случаев + +Безусловно, бывают **особые случаи**, когда может понадобится внутри контейнера запускать **менеджер процессов Gunicorn**, управляющий несколькими **процессами Uvicorn**. + +Для таких случаев Вы можете использовать **официальный Docker-образ** (прим. пер: - *здесь и далее на этой странице, если Вы встретите сочетание "официальный Docker-образ" без уточнений, то автор имеет в виду именно предоставляемый им образ*), где в качестве менеджера процессов используется **Gunicorn**, запускающий несколько **процессов Uvicorn** и некоторые настройки по умолчанию, автоматически устанавливающие количество запущенных процессов в зависимости от количества ядер Вашего процессора. Я расскажу Вам об этом подробнее тут: [Официальный Docker-образ со встроенными Gunicorn и Uvicorn](#docker-gunicorn-uvicorn). + +Некоторые примеры подобных случаев: + +#### Простое приложение + +Вы можете использовать менеджер процессов внутри контейнера, если Ваше приложение **настолько простое**, что у Вас нет необходимости (по крайней мере, пока нет) в тщательных настройках количества процессов и Вам достаточно имеющихся настроек по умолчанию (если используется официальный Docker-образ) для запуска приложения **только на одном сервере**, а не в кластере. + +#### Docker Compose + +С помощью **Docker Compose** можно разворачивать несколько контейнеров на **одном сервере** (не кластере), но при это у Вас не будет простого способа управления количеством запущенных контейнеров с одновременным сохранением общей сети и **балансировки нагрузки**. + +В этом случае можно использовать **менеджер процессов**, управляющий **несколькими процессами**, внутри **одного контейнера**. + +#### Prometheus и прочие причины + +У Вас могут быть и **другие причины**, когда использование **множества процессов** внутри **одного контейнера** будет проще, нежели запуск **нескольких контейнеров** с **единственным процессом** в каждом из них. + +Например (в зависимости от конфигурации), у Вас могут быть инструменты подобные экспортёру Prometheus, которые должны иметь доступ к **каждому запросу** приходящему в контейнер. + +Если у Вас будет **несколько контейнеров**, то Prometheus, по умолчанию, **при сборе метрик** получит их **только с одного контейнера**, который обрабатывает конкретный запрос, вместо **сбора метрик** со всех работающих контейнеров. + +В таком случае может быть проще иметь **один контейнер** со **множеством процессов**, с нужным инструментом (таким как экспортёр Prometheus) в этом же контейнере и собирающем метрики со всех внутренних процессов этого контейнера. + +--- + +Самое главное - **ни одно** из перечисленных правил не является **высеченным в камне** и Вы не обязаны слепо их повторять. Вы можете использовать эти идеи при **рассмотрении Вашего конкретного случая** и самостоятельно решать, какая из концепции подходит лучше: + +* Использование более безопасного протокола HTTPS +* Настройки запуска приложения +* Перезагрузка приложения +* Запуск нескольких экземпляров приложения +* Управление памятью +* Использование перечисленных функций перед запуском приложения + +## Управление памятью + +При **запуске одного процесса на контейнер** Вы получаете относительно понятный, стабильный и ограниченный объём памяти, потребляемый одним контейнером. + +Вы можете установить аналогичные ограничения по памяти при конфигурировании своей системы управления контейнерами (например, **Kubernetes**). Таким образом система сможет **изменять количество контейнеров** на **доступных ей машинах** приводя в соответствие количество памяти нужной контейнерам с количеством памяти доступной в кластере (наборе доступных машин). + +Если у Вас **простенькое** приложение, вероятно у Вас не будет **необходимости** устанавливать жёсткие ограничения на выделяемую ему память. Но если приложение **использует много памяти** (например, оно использует модели **машинного обучения**), Вам следует проверить, как много памяти ему требуется и отрегулировать **количество контейнеров** запущенных на **каждой машине** (может быть даже добавить машин в кластер). + +Если Вы запускаете **несколько процессов в контейнере**, то должны быть уверены, что эти процессы не **займут памяти больше**, чем доступно для контейнера. + +## Подготовительные шаги при запуске контейнеров + +Есть два основных подхода, которые Вы можете использовать при запуске контейнеров (Docker, Kubernetes и т.п.). + +### Множество контейнеров + +Когда Вы запускаете **множество контейнеров**, в каждом из которых работает **только один процесс** (например, в кластере **Kubernetes**), может возникнуть необходимость иметь **отдельный контейнер**, который осуществит **предварительные шаги перед запуском** остальных контейнеров (например, применяет миграции к базе данных). + +!!! info "Информация" + При использовании Kubernetes, это может быть Инициализирующий контейнер. + +При отсутствии такой необходимости (допустим, не нужно применять миграции к базе данных, а только проверить, что она готова принимать соединения), Вы можете проводить такую проверку в каждом контейнере перед запуском его основного процесса и запускать все контейнеры **одновременно**. + +### Только один контейнер + +Если у Вас несложное приложение для работы которого достаточно **одного контейнера**, но в котором работает **несколько процессов** (или один процесс), то прохождение предварительных шагов можно осуществить в этом же контейнере до запуска основного процесса. Официальный Docker-образ поддерживает такие действия. + +## Официальный Docker-образ с Gunicorn и Uvicorn + +Я подготовил для Вас Docker-образ, в который включён Gunicorn управляющий процессами (воркерами) Uvicorn, в соответствии с концепциями рассмотренными в предыдущей главе: [Рабочие процессы сервера (воркеры) - Gunicorn совместно с Uvicorn](./server-workers.md){.internal-link target=_blank}. + +Этот образ может быть полезен для ситуаций описанных тут: [Множество процессов внутри контейнера для особых случаев](#special-cases). + +* tiangolo/uvicorn-gunicorn-fastapi. + +!!! warning "Предупреждение" + Скорее всего у Вас **нет необходимости** в использовании этого образа или подобного ему и лучше создать свой образ с нуля как описано тут: [Создать Docker-образ для FastAPI](#docker-fastapi). + +В этом образе есть **автоматический** механизм подстройки для запуска **необходимого количества процессов** в соответствии с доступным количеством ядер процессора. + +В нём установлены **разумные значения по умолчанию**, но можно изменять и обновлять конфигурацию с помощью **переменных окружения** или конфигурационных файлов. + +Он также поддерживает прохождение **Подготовительных шагов при запуске контейнеров** при помощи скрипта. + +!!! tip "Подсказка" + Для просмотра всех возможных настроек перейдите на страницу этого Docker-образа: tiangolo/uvicorn-gunicorn-fastapi. + +### Количество процессов в официальном Docker-образе + +**Количество процессов** в этом образе **вычисляется автоматически** и зависит от доступного количества **ядер** центрального процессора. + +Это означает, что он будет пытаться **выжать** из процессора как можно больше **производительности**. + +Но Вы можете изменять и обновлять конфигурацию с помощью **переменных окружения** и т.п. + +Поскольку количество процессов зависит от процессора, на котором работает контейнер, **объём потребляемой памяти** также будет зависеть от этого. + +А значит, если Вашему приложению требуется много оперативной памяти (например, оно использует модели машинного обучения) и Ваш сервер имеет центральный процессор с большим количеством ядер, но **не слишком большим объёмом оперативной памяти**, то может дойти до того, что контейнер попытается занять памяти больше, чем доступно, из-за чего будет падение производительности (или сервер вовсе упадёт). 🚨 + + +### Написание `Dockerfile` + +Итак, теперь мы можем написать `Dockerfile` основанный на этом официальном Docker-образе: + +```Dockerfile +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 + +COPY ./requirements.txt /app/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +COPY ./app /app +``` + +### Большие приложения + +Если Вы успели ознакомиться с разделом [Приложения содержащие много файлов](../tutorial/bigger-applications.md){.internal-link target=_blank}, состоящие из множества файлов, Ваш Dockerfile может выглядеть так: + +```Dockerfile hl_lines="7" +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 + +COPY ./requirements.txt /app/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +COPY ./app /app/app +``` + +### Как им пользоваться + +Если Вы используете **Kubernetes** (или что-то вроде того), скорее всего Вам **не нужно** использовать официальный Docker-образ (или другой похожий) в качестве основы, так как управление **количеством запущенных контейнеров** должно быть настроено на уровне кластера. В таком случае лучше **создать образ с нуля**, как описано в разделе Создать [Docker-образ для FastAPI](#docker-fastapi). + +Официальный образ может быть полезен в отдельных случаях, описанных выше в разделе [Множество процессов внутри контейнера для особых случаев](#special-cases). Например, если Ваше приложение **достаточно простое**, не требует запуска в кластере и способно уместиться в один контейнер, то его настройки по умолчанию будут работать довольно хорошо. Или же Вы развертываете его с помощью **Docker Compose**, работаете на одном сервере и т. д + +## Развёртывание образа контейнера + +После создания образа контейнера существует несколько способов его развёртывания. + +Например: + +* С использованием **Docker Compose** при развёртывании на одном сервере +* С использованием **Kubernetes** в кластере +* С использованием режима Docker Swarm в кластере +* С использованием других инструментов, таких как Nomad +* С использованием облачного сервиса, который будет управлять разворачиванием Вашего контейнера + +## Docker-образ и Poetry + +Если Вы пользуетесь Poetry для управления зависимостями Вашего проекта, то можете использовать многоэтапную сборку образа: + +```{ .dockerfile .annotate } +# (1) +FROM python:3.9 as requirements-stage + +# (2) +WORKDIR /tmp + +# (3) +RUN pip install poetry + +# (4) +COPY ./pyproject.toml ./poetry.lock* /tmp/ + +# (5) +RUN poetry export -f requirements.txt --output requirements.txt --without-hashes + +# (6) +FROM python:3.9 + +# (7) +WORKDIR /code + +# (8) +COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt + +# (9) +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (10) +COPY ./app /code/app + +# (11) +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +``` + +1. Это первый этап, которому мы дадим имя `requirements-stage`. + +2. Установите директорию `/tmp` в качестве рабочей директории. + + В ней будет создан файл `requirements.txt` + +3. На этом шаге установите Poetry. + +4. Скопируйте файлы `pyproject.toml` и `poetry.lock` в директорию `/tmp`. + + Поскольку название файла написано как `./poetry.lock*` (с `*` в конце), то ничего не сломается, если такой файл не будет найден. + +5. Создайте файл `requirements.txt`. + +6. Это второй (и последний) этап сборки, который и создаст окончательный образ контейнера. + +7. Установите директорию `/code` в качестве рабочей. + +8. Скопируйте файл `requirements.txt` в директорию `/code`. + + Этот файл находится в образе, созданном на предыдущем этапе, которому мы дали имя requirements-stage, потому при копировании нужно написать `--from-requirements-stage`. + +9. Установите зависимости, указанные в файле `requirements.txt`. + +10. Скопируйте папку `app` в папку `/code`. + +11. Запустите `uvicorn`, указав ему использовать объект `app`, расположенный в `app.main`. + +!!! tip "Подсказка" + Если ткнёте на кружок с плюсом, то увидите объяснения, что происходит в этой строке. + +**Этапы сборки Docker-образа** являются частью `Dockerfile` и работают как **временные образы контейнеров**. Они нужны только для создания файлов, используемых в дальнейших этапах. + +Первый этап был нужен только для **установки Poetry** и **создания файла `requirements.txt`**, в которым прописаны зависимости Вашего проекта, взятые из файла `pyproject.toml`. + +На **следующем этапе** `pip` будет использовать файл `requirements.txt`. + +В итоговом образе будет содержаться **только последний этап сборки**, предыдущие этапы будут отброшены. + +При использовании Poetry, имеет смысл использовать **многоэтапную сборку Docker-образа**, потому что на самом деле Вам не нужен Poetry и его зависимости в окончательном образе контейнера, Вам **нужен только** сгенерированный файл `requirements.txt` для установки зависимостей Вашего проекта. + +А на последнем этапе, придерживаясь описанных ранее правил, создаётся итоговый образ + +### Использование прокси-сервера завершения TLS и Poetry + +И снова повторюсь, если используете прокси-сервер (балансировщик нагрузки), такой, как Nginx или Traefik, добавьте в команду запуска опцию `--proxy-headers`: + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +## Резюме + +При помощи систем контейнеризации (таких, как **Docker** и **Kubernetes**), становится довольно просто обрабатывать все **концепции развертывания**: + +* Использование более безопасного протокола HTTPS +* Настройки запуска приложения +* Перезагрузка приложения +* Запуск нескольких экземпляров приложения +* Управление памятью +* Использование перечисленных функций перед запуском приложения + +В большинстве случаев Вам, вероятно, не нужно использовать какой-либо базовый образ, **лучше создать образ контейнера с нуля** на основе официального Docker-образа Python. + +Позаботившись о **порядке написания** инструкций в `Dockerfile`, Вы сможете использовать **кэш Docker'а**, **минимизировав время сборки**, максимально повысив свою производительность (и избежать скуки). 😎 + +В некоторых особых случаях вы можете использовать официальный образ Docker для FastAPI. 🤓 diff --git a/docs/ru/docs/deployment/https.md b/docs/ru/docs/deployment/https.md new file mode 100644 index 0000000000..a53ab69272 --- /dev/null +++ b/docs/ru/docs/deployment/https.md @@ -0,0 +1,198 @@ +# Об HTTPS + +Обычно представляется, что HTTPS это некая опция, которая либо "включена", либо нет. + +Но всё несколько сложнее. + +!!! tip "Заметка" + Если Вы торопитесь или Вам не интересно, можете перейти на следующую страницу этого пошагового руководства по размещению приложений на серверах с использованием различных технологий. + +Чтобы **изучить основы HTTPS** для клиента, перейдите по ссылке https://howhttps.works/. + +Здесь же представлены некоторые концепции, которые **разработчик** должен иметь в виду при размышлениях об HTTPS: + +* Протокол HTTPS предполагает, что **серверу** нужно **располагать "сертификатами"** сгенерированными **третьей стороной**. + * На самом деле эти сертификаты **приобретены** у третьей стороны, а не "сгенерированы". +* У сертификатов есть **срок годности**. + * Срок годности **истекает**. + * По истечении срока годности их нужно **обновить**, то есть **снова получить** у третьей стороны. +* Шифрование соединения происходит **на уровне протокола TCP**. + * Протокол TCP находится на один уровень **ниже протокола HTTP**. + * Поэтому **проверка сертификатов и шифрование** происходит **до HTTP**. +* **TCP не знает о "доменах"**, но знает об IP-адресах. + * Информация о **запрашиваемом домене** извлекается из запроса **на уровне HTTP**. +* **Сертификаты HTTPS** "сертифицируют" **конкретный домен**, но проверка сертификатов и шифрование данных происходит на уровне протокола TCP, то есть **до того**, как станет известен домен-получатель данных. +* **По умолчанию** это означает, что у Вас может быть **только один сертификат HTTPS на один IP-адрес**. + * Не важно, насколько большой у Вас сервер и насколько маленькие приложения на нём могут быть. + * Однако, у этой проблемы есть **решение**. +* Существует **расширение** протокола **TLS** (который работает на уровне TCP, то есть до HTTP) называемое **SNI**. + * Расширение SNI позволяет одному серверу (с **одним IP-адресом**) иметь **несколько сертификатов HTTPS** и обслуживать **множество HTTPS-доменов/приложений**. + * Чтобы эта конструкция работала, **один** её компонент (программа) запущенный на сервере и слушающий **публичный IP-адрес**, должен иметь **все сертификаты HTTPS** для этого сервера. +* **После** установления защищённого соединения, протоколом передачи данных **остаётся HTTP**. + * Но данные теперь **зашифрованы**, несмотря на то, что они передаются по **протоколу HTTP**. + +Обычной практикой является иметь **одну программу/HTTP-сервер** запущенную на сервере (машине, хосте и т.д.) и **ответственную за всю работу с HTTPS**: + +* получение **зашифрованных HTTPS-запросов** +* отправка **расшифрованных HTTP запросов** в соответствующее HTTP-приложение, работающее на том же сервере (в нашем случае, это приложение **FastAPI**) +* получние **HTTP-ответа** от приложения +* **шифрование ответа** используя подходящий **сертификат HTTPS** +* отправка зашифрованного **HTTPS-ответа клиенту**. +Такой сервер часто называют **Прокси-сервер завершения работы TLS** или просто "прокси-сервер". + +Вот некоторые варианты, которые Вы можете использовать в качестве такого прокси-сервера: + +* Traefik (может обновлять сертификаты) +* Caddy (может обновлять сертификаты) +* Nginx +* HAProxy + +## Let's Encrypt (центр сертификации) + +До появления Let's Encrypt **сертификаты HTTPS** приходилось покупать у третьих сторон. + +Процесс получения такого сертификата был трудоёмким, требовал предоставления подтверждающих документов и сертификаты стоили дорого. + +Но затем консорциумом Linux Foundation был создан проект **Let's Encrypt**. + +Он автоматически предоставляет **бесплатные сертификаты HTTPS**. Эти сертификаты используют все стандартные криптографические способы шифрования. Они имеют небольшой срок годности (около 3 месяцев), благодаря чему они даже **более безопасны**. + +При запросе на получение сертификата, он автоматически генерируется и домен проверяется на безопасность. Это позволяет обновлять сертификаты автоматически. + +Суть идеи в автоматическом получении и обновлении этих сертификатов, чтобы все могли пользоваться **безопасным HTTPS. Бесплатно. В любое время.** + +## HTTPS для разработчиков + +Ниже, шаг за шагом, с заострением внимания на идеях, важных для разработчика, описано, как может выглядеть HTTPS API. + +### Имя домена + +Чаще всего, всё начинается с **приобретения имени домена**. Затем нужно настроить DNS-сервер (вероятно у того же провайдера, который выдал Вам домен). + +Далее, возможно, Вы получаете "облачный" сервер (виртуальную машину) или что-то типа этого, у которого есть постоянный **публичный IP-адрес**. + +На DNS-сервере (серверах) Вам следует настроить соответствующую ресурсную запись ("`запись A`"), указав, что **Ваш домен** связан с публичным **IP-адресом Вашего сервера**. + +Обычно эту запись достаточно указать один раз, при первоначальной настройке всего сервера. + +!!! tip "Заметка" + Уровни протоколов, работающих с именами доменов, намного ниже HTTPS, но об этом следует упомянуть здесь, так как всё зависит от доменов и IP-адресов. + +### DNS + +Теперь давайте сфокусируемся на работе с HTTPS. + +Всё начинается с того, что браузер спрашивает у **DNS-серверов**, какой **IP-адрес связан с доменом**, для примера возьмём домен `someapp.example.com`. + +DNS-сервера присылают браузеру определённый **IP-адрес**, тот самый публичный IP-адрес Вашего сервера, который Вы указали в ресурсной "записи А" при настройке. + + + +### Рукопожатие TLS + +В дальнейшем браузер будет взаимодействовать с этим IP-адресом через **port 443** (общепринятый номер порта для HTTPS). + +Первым шагом будет установление соединения между клиентом (браузером) и сервером и выбор криптографического ключа (для шифрования). + + + +Эта часть клиент-серверного взаимодействия устанавливает TLS-соединение и называется **TLS-рукопожатием**. + +### TLS с расширением SNI + +На сервере **только один процесс** может прослушивать определённый **порт** определённого **IP-адреса**. На сервере могут быть и другие процессы, слушающие другие порты этого же IP-адреса, но никакой процесс не может быть привязан к уже занятой комбинации IP-адрес:порт. Эта комбинация называется "сокет". + +По умолчанию TLS (HTTPS) использует порт `443`. Потому этот же порт будем использовать и мы. + +И раз уж только один процесс может слушать этот порт, то это будет процесс **прокси-сервера завершения работы TLS**. + +Прокси-сервер завершения работы TLS будет иметь доступ к одному или нескольким **TLS-сертификатам** (сертификаты HTTPS). + +Используя **расширение SNI** упомянутое выше, прокси-сервер из имеющихся сертификатов TLS (HTTPS) выберет тот, который соответствует имени домена, указанному в запросе от клиента. + +То есть будет выбран сертификат для домена `someapp.example.com`. + + + +Клиент уже **доверяет** тому, кто выдал этот TLS-сертификат (в нашем случае - Let's Encrypt, но мы ещё обсудим это), потому может **проверить**, действителен ли полученный от сервера сертификат. + +Затем, используя этот сертификат, клиент и прокси-сервер **выбирают способ шифрования** данных для устанавливаемого **TCP-соединения**. На этом операция **TLS-рукопожатия** завершена. + +В дальнейшем клиент и сервер будут взаимодействовать по **зашифрованному TCP-соединению**, как предлагается в протоколе TLS. И на основе этого TCP-соедениния будет создано **HTTP-соединение**. + +Таким образом, **HTTPS** это тот же **HTTP**, но внутри **безопасного TLS-соединения** вместо чистого (незашифрованного) TCP-соединения. + +!!! tip "Заметка" + Обратите внимание, что шифрование происходит на **уровне TCP**, а не на более высоком уровне HTTP. + +### HTTPS-запрос + +Теперь, когда между клиентом и сервером (в нашем случае, браузером и прокси-сервером) создано **зашифрованное TCP-соединение**, они могут начать **обмен данными по протоколу HTTP**. + +Так клиент отправляет **HTTPS-запрос**. То есть обычный HTTP-запрос, но через зашифрованное TLS-содинение. + + + +### Расшифровка запроса + +Прокси-сервер, используя согласованный с клиентом ключ, расшифрует полученный **зашифрованный запрос** и передаст **обычный (незашифрованный) HTTP-запрос** процессу, запускающему приложение (например, процессу Uvicorn запускающему приложение FastAPI). + + + +### HTTP-ответ + +Приложение обработает запрос и вернёт **обычный (незашифрованный) HTTP-ответ** прокси-серверу. + + + +### HTTPS-ответ + +Пркоси-сервер **зашифрует ответ** используя ранее согласованный с клиентом способ шифрования (которые содержатся в сертификате для домена `someapp.example.com`) и отправит его браузеру. + +Наконец, браузер проверит ответ, в том числе, что тот зашифрован с нужным ключом, **расшифрует его** и обработает. + + + +Клиент (браузер) знает, что ответ пришёл от правильного сервера, так как использует методы шифрования, согласованные ими раннее через **HTTPS-сертификат**. + +### Множество приложений + +На одном и том же сервере (или серверах) можно разместить **множество приложений**, например, другие программы с API или базы данных. + +Напомню, что только один процесс (например, прокси-сервер) может прослушивать определённый порт определённого IP-адреса. +Но другие процессы и приложения тоже могут работать на этом же сервере (серверах), если они не пытаются использовать уже занятую **комбинацию IP-адреса и порта** (сокет). + + + +Таким образом, сервер завершения TLS может обрабатывать HTTPS-запросы и использовать сертификаты для **множества доменов** или приложений и передавать запросы правильным адресатам (другим приложениям). + +### Обновление сертификата + +В недалёком будущем любой сертификат станет **просроченным** (примерно через три месяца после получения). + +Когда это произойдёт, можно запустить другую программу, которая подключится к Let's Encrypt и обновит сертификат(ы). Существуют прокси-серверы, которые могут сделать это действие самостоятельно. + + + +**TLS-сертификаты** не привязаны к IP-адресу, но **связаны с именем домена**. + +Так что при обновлении сертификатов программа должна **подтвердить** центру сертификации (Let's Encrypt), что обновление запросил **"владелец", который контролирует этот домен**. + +Есть несколько путей осуществления этого. Самые популярные из них: + +* **Изменение записей DNS**. + * Для такого варианта Ваша программа обновления должна уметь работать с API DNS-провайдера, обслуживающего Ваши DNS-записи. Не у всех провайдеров есть такой API, так что этот способ не всегда применим. +* **Запуск в качестве программы-сервера** (как минимум, на время обновления сертификатов) на публичном IP-адресе домена. + * Как уже не раз упоминалось, только один процесс может прослушивать определённый порт определённого IP-адреса. + * Это одна из причин использования прокси-сервера ещё и в качестве программы обновления сертификатов. + * В случае, если обновлением сертификатов занимается другая программа, Вам понадобится остановить прокси-сервер, запустить программу обновления сертификатов на сокете, предназначенном для прокси-сервера, настроить прокси-сервер на работу с новыми сертификатами и перезапустить его. Эта схема далека от идеальной, так как Ваши приложения будут недоступны на время отключения прокси-сервера. + +Весь этот процесс обновления, одновременный с обслуживанием запросов, является одной из основных причин, по которой желательно иметь **отдельную систему для работы с HTTPS** в виде прокси-сервера завершения TLS, а не просто использовать сертификаты TLS непосредственно с сервером приложений (например, Uvicorn). + +## Резюме + +Наличие **HTTPS** очень важно и довольно **критично** в большинстве случаев. Однако, Вам, как разработчику, не нужно тратить много сил на это, достаточно **понимать эти концепции** и принципы их работы. + +Но узнав базовые основы **HTTPS** Вы можете легко совмещать разные инструменты, которые помогут Вам в дальнейшей разработке. + +В следующих главах я покажу Вам несколько примеров, как настраивать **HTTPS** для приложений **FastAPI**. 🔒 diff --git a/docs/ru/docs/deployment/index.md b/docs/ru/docs/deployment/index.md index 4dc4e482ed..d214a9d62e 100644 --- a/docs/ru/docs/deployment/index.md +++ b/docs/ru/docs/deployment/index.md @@ -1,4 +1,4 @@ -# Развёртывание - Введение +# Развёртывание Развернуть приложение **FastAPI** довольно просто. diff --git a/docs/ru/docs/deployment/manually.md b/docs/ru/docs/deployment/manually.md new file mode 100644 index 0000000000..1d00b30860 --- /dev/null +++ b/docs/ru/docs/deployment/manually.md @@ -0,0 +1,150 @@ +# Запуск сервера вручную - Uvicorn + +Для запуска приложения **FastAPI** на удалённой серверной машине Вам необходим программный сервер, поддерживающий протокол ASGI, такой как **Uvicorn**. + +Существует три наиболее распространённые альтернативы: + +* Uvicorn: высокопроизводительный ASGI сервер. +* Hypercorn: ASGI сервер, помимо прочего поддерживающий HTTP/2 и Trio. +* Daphne: ASGI сервер, созданный для Django Channels. + +## Сервер как машина и сервер как программа + +В этих терминах есть некоторые различия и Вам следует запомнить их. 💡 + +Слово "**сервер**" чаще всего используется в двух контекстах: + +- удалённый или расположенный в "облаке" компьютер (физическая или виртуальная машина). +- программа, запущенная на таком компьютере (например, Uvicorn). + +Просто запомните, если Вам встретился термин "сервер", то обычно он подразумевает что-то из этих двух смыслов. + +Когда имеют в виду именно удалённый компьютер, часто говорят просто **сервер**, но ещё его называют **машина**, **ВМ** (виртуальная машина), **нода**. Все эти термины обозначают одно и то же - удалённый компьютер, обычно под управлением Linux, на котором Вы запускаете программы. + +## Установка программного сервера + +Вы можете установить сервер, совместимый с протоколом ASGI, так: + +=== "Uvicorn" + + * Uvicorn, молниесный ASGI сервер, основанный на библиотеках uvloop и httptools. + +
+ + ```console + $ pip install "uvicorn[standard]" + + ---> 100% + ``` + +
+ + !!! tip "Подсказка" + С опцией `standard`, Uvicorn будет установливаться и использоваться с некоторыми дополнительными рекомендованными зависимостями. + + В них входит `uvloop`, высокопроизводительная замена `asyncio`, которая значительно ускоряет работу асинхронных программ. + +=== "Hypercorn" + + * Hypercorn, ASGI сервер, поддерживающий протокол HTTP/2. + +
+ + ```console + $ pip install hypercorn + + ---> 100% + ``` + +
+ + ...или какой-либо другой ASGI сервер. + +## Запуск серверной программы + +Затем запустите Ваше приложение так же, как было указано в руководстве ранее, но без опции `--reload`: + +=== "Uvicorn" + +
+ + ```console + $ uvicorn main:app --host 0.0.0.0 --port 80 + + INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) + ``` + +
+ +=== "Hypercorn" + +
+ + ```console + $ hypercorn main:app --bind 0.0.0.0:80 + + Running on 0.0.0.0:8080 over http (CTRL + C to quit) + ``` + +
+ +!!! warning "Предупреждение" + + Не забудьте удалить опцию `--reload`, если ранее пользовались ею. + + Включение опции `--reload` требует дополнительных ресурсов, влияет на стабильность работы приложения и может повлечь прочие неприятности. + + Она сильно помогает во время **разработки**, но **не следует** использовать её при **реальной работе** приложения. + +## Hypercorn с Trio + +Starlette и **FastAPI** основаны на AnyIO, которая делает их совместимыми как с asyncio - стандартной библиотекой Python, так и с Trio. + + +Тем не менее Uvicorn совместим только с asyncio и обычно используется совместно с `uvloop`, высокопроизводительной заменой `asyncio`. + +Но если Вы хотите использовать **Trio** напрямую, то можете воспользоваться **Hypercorn**, так как они совместимы. ✨ + +### Установка Hypercorn с Trio + +Для начала, Вам нужно установить Hypercorn с поддержкой Trio: + +
+ +```console +$ pip install "hypercorn[trio]" +---> 100% +``` + +
+ +### Запуск с Trio + +Далее запустите Hypercorn с опцией `--worker-class` и аргументом `trio`: + +
+ +```console +$ hypercorn main:app --worker-class trio +``` + +
+ +Hypercorn, в свою очередь, запустит Ваше приложение использующее Trio. + +Таким образом, Вы сможете использовать Trio в своём приложении. Но лучше использовать AnyIO, для сохранения совместимости и с Trio, и с asyncio. 🎉 + +## Концепции развёртывания + +В вышеприведённых примерах серверные программы (например Uvicorn) запускали только **один процесс**, принимающий входящие запросы с любого IP (на это указывал аргумент `0.0.0.0`) на определённый порт (в примерах мы указывали порт `80`). + +Это основная идея. Но возможно, Вы озаботитесь добавлением дополнительных возможностей, таких как: + +* Использование более безопасного протокола HTTPS +* Настройки запуска приложения +* Перезагрузка приложения +* Запуск нескольких экземпляров приложения +* Управление памятью +* Использование перечисленных функций перед запуском приложения. + +Я поведаю Вам больше о каждой из этих концепций в следующих главах, с конкретными примерами стратегий работы с ними. 🚀 diff --git a/docs/ru/docs/external-links.md b/docs/ru/docs/external-links.md index 4daf65898b..2448ef82ef 100644 --- a/docs/ru/docs/external-links.md +++ b/docs/ru/docs/external-links.md @@ -9,70 +9,21 @@ !!! tip Если у вас есть статья, проект, инструмент или что-либо, связанное с **FastAPI**, что еще не перечислено здесь, создайте Pull Request. -## Статьи +{% for section_name, section_content in external_links.items() %} -### На английском +## {{ section_name }} -{% if external_links %} -{% for article in external_links.articles.english %} +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* {{ item.title }} by {{ item.author }}. -* {{ article.title }} by {{ article.author }}. {% endfor %} -{% endif %} - -### На японском - -{% if external_links %} -{% for article in external_links.articles.japanese %} - -* {{ article.title }} by {{ article.author }}. {% endfor %} -{% endif %} - -### На вьетнамском - -{% if external_links %} -{% for article in external_links.articles.vietnamese %} - -* {{ article.title }} by {{ article.author }}. {% endfor %} -{% endif %} - -### На русском - -{% if external_links %} -{% for article in external_links.articles.russian %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} - -### На немецком - -{% if external_links %} -{% for article in external_links.articles.german %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} - -## Подкасты - -{% if external_links %} -{% for article in external_links.podcasts.english %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} - -## Talks - -{% if external_links %} -{% for article in external_links.talks.english %} - -* {{ article.title }} by {{ article.author }}. -{% endfor %} -{% endif %} ## Проекты diff --git a/docs/ru/docs/features.md b/docs/ru/docs/features.md index e18f7bc87c..97841cc835 100644 --- a/docs/ru/docs/features.md +++ b/docs/ru/docs/features.md @@ -27,7 +27,7 @@ ### Только современный Python -Все эти возможности основаны на стандартных **аннотациях типов Python 3.6** (благодаря Pydantic). Не нужно изучать новый синтаксис. Только лишь стандартный современный Python. +Все эти возможности основаны на стандартных **аннотациях типов Python 3.8** (благодаря Pydantic). Не нужно изучать новый синтаксис. Только лишь стандартный современный Python. Если вам нужно освежить знания, как использовать аннотации типов в Python (даже если вы не используете FastAPI), выделите 2 минуты и просмотрите краткое руководство: [Введение в аннотации типов Python¶ ](python-types.md){.internal-link target=_blank}. diff --git a/docs/ru/docs/help-fastapi.md b/docs/ru/docs/help-fastapi.md new file mode 100644 index 0000000000..65ff768d1e --- /dev/null +++ b/docs/ru/docs/help-fastapi.md @@ -0,0 +1,255 @@ +# Помочь FastAPI - Получить помощь + +Нравится ли Вам **FastAPI**? + +Хотели бы Вы помочь FastAPI, его пользователям и автору? + +Может быть у Вас возникли трудности с **FastAPI** и Вам нужна помощь? + +Есть несколько очень простых способов оказания помощи (иногда достаточно всего лишь одного или двух кликов). + +И также есть несколько способов получить помощь. + +## Подписаться на новостную рассылку + +Вы можете подписаться на редкую [новостную рассылку **FastAPI и его друзья**](/newsletter/){.internal-link target=_blank} и быть в курсе о: + +* Новостях о FastAPI и его друзьях 🚀 +* Руководствах 📝 +* Возможностях ✨ +* Исправлениях 🚨 +* Подсказках и хитростях ✅ + +## Подписаться на FastAPI в Twitter + +Подписаться на @fastapi в **Twitter** для получения наисвежайших новостей о **FastAPI**. 🐦 + +## Добавить **FastAPI** звезду на GitHub + +Вы можете добавить FastAPI "звезду" на GitHub (кликнуть на кнопку звезды в верхнем правом углу экрана): https://github.com/tiangolo/fastapi. ⭐️ + +Чем больше звёзд, тем легче другим пользователям найти нас и увидеть, что проект уже стал полезным для многих. + +## Отслеживать свежие выпуски в репозитории на GitHub + +Вы можете "отслеживать" FastAPI на GitHub (кликните по кнопке "watch" наверху справа): https://github.com/tiangolo/fastapi. 👀 + +Там же Вы можете указать в настройках - "Releases only". + +С такой настройкой Вы будете получать уведомления на вашу электронную почту каждый раз, когда появится новый релиз (новая версия) **FastAPI** с исправлениями ошибок и новыми возможностями. + +## Связаться с автором + +Можно связаться со мной (Себястьян Рамирез / `tiangolo`), автором FastAPI. + +Вы можете: + +* Подписаться на меня на **GitHub**. + * Посмотреть другие мои проекты с открытым кодом, которые могут быть полезны Вам. + * Подписавшись на меня Вы сможете получать уведомления, что я создал новый проект с открытым кодом,. +* Подписаться на меня в **Twitter** или в Mastodon. + * Поделиться со мной, как Вы используете FastAPI (я обожаю читать про это). + * Получать уведомления, когда я делаю объявления и представляю новые инструменты. + * Вы также можете подписаться на @fastapi в Twitter (это отдельный аккаунт). +* Подписаться на меня в **Linkedin**. + * Получать уведомления, когда я делаю объявления и представляю новые инструменты (правда чаще всего я использую Twitter 🤷‍♂). +* Читать, что я пишу (или подписаться на меня) в **Dev.to** или в **Medium**. + * Читать другие идеи, статьи и читать об инструментах созданных мной. + * Подпишитесь на меня, чтобы прочитать, когда я опубликую что-нибудь новое. + +## Оставить сообщение в Twitter о **FastAPI** + +Оставьте сообщение в Twitter о **FastAPI** и позвольте мне и другим узнать - почему он Вам нравится. 🎉 + +Я люблю узнавать о том, как **FastAPI** используется, что Вам понравилось в нём, в каких проектах/компаниях Вы используете его и т.п. + +## Оставить голос за FastAPI + +* Голосуйте за **FastAPI** в Slant. +* Голосуйте за **FastAPI** в AlternativeTo. +* Расскажите, как Вы используете **FastAPI** на StackShare. + +## Помочь другим с их проблемами на GitHub + +Вы можете посмотреть, какие проблемы испытывают другие люди и попытаться помочь им. Чаще всего это вопросы, на которые, весьма вероятно, Вы уже знаете ответ. 🤓 + +Если Вы будете много помогать людям с решением их проблем, Вы можете стать официальным [Экспертом FastAPI](fastapi-people.md#experts){.internal-link target=_blank}. 🎉 + +Только помните, самое важное при этом - доброта. Столкнувшись с проблемой, люди расстраиваются и часто задают вопросы не лучшим образом, но постарайтесь быть максимально доброжелательным. 🤗 + +Идея сообщества **FastAPI** в том, чтобы быть добродушным и гостеприимными. Не допускайте издевательств или неуважительного поведения по отношению к другим. Мы должны заботиться друг о друге. + +--- + +Как помочь другим с их проблемами: + +### Понять вопрос + +* Удостоверьтесь, что поняли **цель** и обстоятельства случая вопрошающего. + +* Затем проверьте, что вопрос (в подавляющем большинстве - это вопросы) Вам **ясен**. + +* Во многих случаях вопрос касается решения, которое пользователь придумал сам, но может быть и решение **получше**. Если Вы поймёте проблему и обстоятельства случая, то сможете предложить **альтернативное решение**. + +* Ежели вопрос Вам непонятен, запросите больше **деталей**. + +### Воспроизвести проблему + +В большинстве случаев есть что-то связанное с **исходным кодом** вопрошающего. + +И во многих случаях будет предоставлен только фрагмент этого кода, которого недостаточно для **воспроизведения проблемы**. + +* Попросите предоставить минимальный воспроизводимый пример, который можно **скопировать** и запустить локально дабы увидеть такую же ошибку, или поведение, или лучше понять обстоятельства случая. + +* Если на Вас нахлынуло великодушие, то можете попытаться **создать похожий пример** самостоятельно, основываясь только на описании проблемы. Но имейте в виду, что это может занять много времени и, возможно, стоит сначала позадавать вопросы для прояснения проблемы. + +### Предложить решение + +* После того как Вы поняли вопрос, Вы можете дать **ответ**. + +* Следует понять **основную проблему и обстоятельства случая**, потому что может быть решение лучше, чем то, которое пытались реализовать. + +### Попросить закрыть проблему + +Если Вам ответили, высоки шансы, что Вам удалось решить проблему, поздравляю, **Вы - герой**! 🦸 + +* В таком случае, если вопрос решён, попросите **закрыть проблему**. + +## Отслеживать репозиторий на GitHub + +Вы можете "отслеживать" FastAPI на GitHub (кликните по кнопке "watch" наверху справа): https://github.com/tiangolo/fastapi. 👀 + +Если Вы выберете "Watching" вместо "Releases only", то будете получать уведомления когда кто-либо попросит о помощи с решением его проблемы. + +Тогда Вы можете попробовать решить эту проблему. + +## Запросить помощь с решением проблемы + +Вы можете создать новый запрос с просьбой о помощи в репозитории на GitHub, например: + +* Задать **вопрос** или попросить помощи в решении **проблемы**. +* Предложить новое **улучшение**. + +**Заметка**: Если Вы создаёте подобные запросы, то я попрошу Вас также оказывать аналогичную помощь другим. 😉 + +## Проверять пул-реквесты + +Вы можете помочь мне проверять пул-реквесты других участников. + +И повторюсь, постарайтесь быть доброжелательным. 🤗 + +--- + +О том, что нужно иметь в виду при проверке пул-реквестов: + +### Понять проблему + +* Во-первых, убедитесь, что **поняли проблему**, которую пул-реквест пытается решить. Для этого может потребоваться продолжительное обсуждение. + +* Также есть вероятность, что пул-реквест не актуален, так как проблему можно решить **другим путём**. В таком случае Вы можете указать на этот факт. + +### Не переживайте о стиле + +* Не стоит слишком беспокоиться о таких вещах, как стиль сообщений в коммитах или количество коммитов. При слиянии пул-реквеста с основной веткой, я буду сжимать и настраивать всё вручную. + +* Также не беспокойтесь о правилах стиля, для проверки сего есть автоматизированные инструменты. + +И если всё же потребуется какой-то другой стиль, я попрошу Вас об этом напрямую или добавлю сам коммиты с необходимыми изменениями. + +### Проверить код + +* Проверьте и прочитайте код, посмотрите, какой он имеет смысл, **запустите его локально** и посмотрите, действительно ли он решает поставленную задачу. + +* Затем, используя **комментарий**, сообщите, что Вы сделали проверку, тогда я буду знать, что Вы действительно проверили код. + +!!! Информация + К сожалению, я не могу так просто доверять пул-реквестам, у которых уже есть несколько одобрений. + + Бывали случаи, что пул-реквесты имели 3, 5 или больше одобрений, вероятно из-за привлекательного описания, но когда я проверял эти пул-реквесты, они оказывались сломаны, содержали ошибки или вовсе не решали проблему, которую, как они утверждали, должны были решить. 😅 + + Потому это действительно важно - проверять и запускать код, и комментарием уведомлять меня, что Вы проделали эти действия. 🤓 + +* Если Вы считаете, что пул-реквест можно упростить, то можете попросить об этом, но не нужно быть слишком придирчивым, может быть много субъективных точек зрения (и у меня тоже будет своя 🙈), поэтому будет лучше, если Вы сосредоточитесь на фундаментальных вещах. + +### Тестировать + +* Помогите мне проверить, что у пул-реквеста есть **тесты**. + +* Проверьте, что тесты **падали** до пул-реквеста. 🚨 + +* Затем проверьте, что тесты **не валятся** после пул-реквеста. ✅ + +* Многие пул-реквесты не имеют тестов, Вы можете **напомнить** о необходимости добавления тестов или даже **предложить** какие-либо свои тесты. Это одна из тех вещей, которые отнимают много времени и Вы можете помочь с этим. + +* Затем добавьте комментарий, что Вы испробовали в ходе проверки. Таким образом я буду знать, как Вы произвели проверку. 🤓 + +## Создать пул-реквест + +Вы можете [сделать вклад](contributing.md){.internal-link target=_blank} в код фреймворка используя пул-реквесты, например: + +* Исправить опечатку, которую Вы нашли в документации. +* Поделиться статьёй, видео или подкастом о FastAPI, которые Вы создали или нашли изменив этот файл. + * Убедитесь, что Вы добавили свою ссылку в начало соответствующего раздела. +* Помочь с [переводом документации](contributing.md#translations){.internal-link target=_blank} на Ваш язык. + * Вы также можете проверять переводы сделанные другими. +* Предложить новые разделы документации. +* Исправить существующуе проблемы/баги. + * Убедитесь, что добавили тесты. +* Добавить новую возможность. + * Убедитесь, что добавили тесты. + * Убедитесь, что добавили документацию, если она необходима. + +## Помочь поддерживать FastAPI + +Помогите мне поддерживать **FastAPI**! 🤓 + +Предстоит ещё много работы и, по большей части, **ВЫ** можете её сделать. + +Основные задачи, которые Вы можете выполнить прямо сейчас: + +* [Помочь другим с их проблемами на GitHub](#help-others-with-issues-in-github){.internal-link target=_blank} (смотрите вышестоящую секцию). +* [Проверить пул-реквесты](#review-pull-requests){.internal-link target=_blank} (смотрите вышестоящую секцию). + +Эти две задачи **отнимают больше всего времени**. Это основная работа по поддержке FastAPI. + +Если Вы можете помочь мне с этим, **Вы помогаете поддерживать FastAPI** и следить за тем, чтобы он продолжал **развиваться быстрее и лучше**. 🚀 + +## Подключиться к чату + +Подключайтесь к 👥 чату в Discord 👥 и общайтесь с другими участниками сообщества FastAPI. + +!!! Подсказка + Вопросы по проблемам с фреймворком лучше задавать в GitHub issues, так больше шансов, что Вы получите помощь от [Экспертов FastAPI](fastapi-people.md#experts){.internal-link target=_blank}. + + Используйте этот чат только для бесед на отвлечённые темы. + +### Не использовать чаты для вопросов + +Имейте в виду, что чаты позволяют больше "свободного общения", потому там легко задавать вопросы, которые слишком общие и на которые труднее ответить, так что Вы можете не получить нужные Вам ответы. + +В разделе "проблемы" на GitHub, есть шаблон, который поможет Вам написать вопрос правильно, чтобы Вам было легче получить хороший ответ или даже решить проблему самостоятельно, прежде чем Вы зададите вопрос. В GitHub я могу быть уверен, что всегда отвечаю на всё, даже если это займет какое-то время. И я не могу сделать то же самое в чатах. 😅 + +Кроме того, общение в чатах не так легкодоступно для поиска, как в GitHub, потому вопросы и ответы могут потеряться среди другого общения. И только проблемы решаемые на GitHub учитываются в получении лычки [Эксперт FastAPI](fastapi-people.md#experts){.internal-link target=_blank}, так что весьма вероятно, что Вы получите больше внимания на GitHub. + +С другой стороны, в чатах тысячи пользователей, а значит есть большие шансы в любое время найти там кого-то, с кем можно поговорить. 😄 + +## Спонсировать автора + +Вы также можете оказать мне финансовую поддержку посредством спонсорства через GitHub. + +Там можно просто купить мне кофе ☕️ в знак благодарности. 😄 + +А ещё Вы можете стать Серебряным или Золотым спонсором для FastAPI. 🏅🎉 + +## Спонсировать инструменты, на которых зиждется мощь FastAPI + +Как Вы могли заметить в документации, FastAPI опирается на плечи титанов: Starlette и Pydantic. + +Им тоже можно оказать спонсорскую поддержку: + +* Samuel Colvin (Pydantic) +* Encode (Starlette, Uvicorn) + +--- + +Благодарствую! 🚀 diff --git a/docs/ru/docs/history-design-future.md b/docs/ru/docs/history-design-future.md new file mode 100644 index 0000000000..2a5e428b1b --- /dev/null +++ b/docs/ru/docs/history-design-future.md @@ -0,0 +1,77 @@ +# История создания и дальнейшее развитие + +Однажды, один из пользователей **FastAPI** задал вопрос: + +> Какова история этого проекта? Создаётся впечатление, что он явился из ниоткуда и завоевал мир за несколько недель [...] + +Что ж, вот небольшая часть истории проекта. + +## Альтернативы + +В течение нескольких лет я, возглавляя различные команды разработчиков, создавал довольно сложные API для машинного обучения, распределённых систем, асинхронных задач, баз данных NoSQL и т.д. + +В рамках работы над этими проектами я исследовал, проверял и использовал многие фреймворки. + +Во многом история **FastAPI** - история его предшественников. + +Как написано в разделе [Альтернативы](alternatives.md){.internal-link target=_blank}: + +
+ +**FastAPI** не существовал бы, если б не было более ранних работ других людей. + +Они создали большое количество инструментов, которые и вдохновили меня на создание **FastAPI**. + +Я всячески избегал создания нового фреймворка в течение нескольких лет. Сначала я пытался собрать все нужные возможности, которые ныне есть в **FastAPI**, используя множество различных фреймворков, плагинов и инструментов. + +Но в какой-то момент не осталось другого выбора, кроме как создать что-то, что предоставляло бы все эти возможности сразу. Взять самые лучшие идеи из предыдущих инструментов и, используя введённые в Python подсказки типов (которых не было до версии 3.6), объединить их. + +
+ +## Исследования + +Благодаря опыту использования существующих альтернатив, мы с коллегами изучили их основные идеи и скомбинировали собранные знания наилучшим образом. + +Например, стало ясно, что необходимо брать за основу стандартные подсказки типов Python, а самым лучшим подходом является использование уже существующих стандартов. + +Итак, прежде чем приступить к написанию **FastAPI**, я потратил несколько месяцев на изучение OpenAPI, JSON Schema, OAuth2, и т.п. для понимания их взаимосвязей, совпадений и различий. + +## Дизайн + +Затем я потратил некоторое время на придумывание "API" разработчика, который я хотел иметь как пользователь (как разработчик, использующий FastAPI). + +Я проверил несколько идей на самых популярных редакторах кода среди Python-разработчиков: PyCharm, VS Code, Jedi. + +Данные по редакторам я взял из опроса Python-разработчиков, который охватываает около 80% пользователей. + +Это означает, что **FastAPI** был специально проверен на редакторах, используемых 80% Python-разработчиками. И поскольку большинство других редакторов, как правило, работают аналогичным образом, все его преимущества должны работать практически для всех редакторов. + +Таким образом, я смог найти наилучшие способы сократить дублирование кода, обеспечить повсеместное автодополнение, проверку типов и ошибок и т.д. + +И все это, чтобы все пользователи могли получать наилучший опыт разработки. + +## Зависимости + +Протестировав несколько вариантов, я решил, что в качестве основы буду использовать **Pydantic** и его преимущества. + +По моим предложениям был изменён код этого фреймворка, чтобы сделать его полностью совместимым с JSON Schema, поддержать различные способы определения ограничений и улучшить помощь редакторов (проверки типов, автозаполнение). + +В то же время, я принимал участие в разработке **Starlette**, ещё один из основных компонентов FastAPI. + +## Разработка + +К тому времени, когда я начал создавать **FastAPI**, большинство необходимых деталей уже существовало, дизайн был определён, зависимости и прочие инструменты были готовы, а знания о стандартах и спецификациях были четкими и свежими. + +## Будущее + +Сейчас уже ясно, что **FastAPI** со своими идеями стал полезен многим людям. + +При сравнении с альтернативами, выбор падает на него, поскольку он лучше подходит для множества вариантов использования. + +Многие разработчики и команды уже используют **FastAPI** в своих проектах (включая меня и мою команду). + +Но, тем не менее, грядёт добавление ещё многих улучшений и возможностей. + +У **FastAPI** великое будущее. + +И [ваш вклад в это](help-fastapi.md){.internal-link target=_blank} - очень ценнен. diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index 14a6d5a8b9..6c99f623dd 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -27,7 +27,7 @@ --- -FastAPI — это современный, быстрый (высокопроизводительный) веб-фреймворк для создания API используя Python 3.6+, в основе которого лежит стандартная аннотация типов Python. +FastAPI — это современный, быстрый (высокопроизводительный) веб-фреймворк для создания API используя Python 3.8+, в основе которого лежит стандартная аннотация типов Python. Ключевые особенности: @@ -109,7 +109,7 @@ FastAPI — это современный, быстрый (высокопрои ## Зависимости -Python 3.7+ +Python 3.8+ FastAPI стоит на плечах гигантов: @@ -321,11 +321,11 @@ def update_item(item_id: int, item: Item): Таким образом, вы объявляете **один раз** типы параметров, тело и т. д. в качестве параметров функции. -Вы делаете это испльзуя стандартную современную типизацию Python. +Вы делаете это используя стандартную современную типизацию Python. Вам не нужно изучать новый синтаксис, методы или классы конкретной библиотеки и т. д. -Только стандартный **Python 3.6+**. +Только стандартный **Python 3.8+**. Например, для `int`: @@ -439,7 +439,6 @@ item: Item Используется Pydantic: -* ujson - для более быстрого JSON "парсинга". * email_validator - для проверки электронной почты. Используется Starlette: diff --git a/docs/ru/docs/learn/index.md b/docs/ru/docs/learn/index.md new file mode 100644 index 0000000000..b2e4cabc75 --- /dev/null +++ b/docs/ru/docs/learn/index.md @@ -0,0 +1,5 @@ +# Обучение + +Здесь представлены вводные разделы и учебные пособия для изучения **FastAPI**. + +Вы можете считать это **книгой**, **курсом**, **официальным** и рекомендуемым способом изучения FastAPI. 😎 diff --git a/docs/ru/docs/project-generation.md b/docs/ru/docs/project-generation.md new file mode 100644 index 0000000000..76253d6f2f --- /dev/null +++ b/docs/ru/docs/project-generation.md @@ -0,0 +1,84 @@ +# Генераторы проектов - Шаблоны + +Чтобы начать работу быстрее, Вы можете использовать "генераторы проектов", в которые включены множество начальных настроек для функций безопасности, баз данных и некоторые эндпоинты API. + +В генераторе проектов всегда будут предустановлены какие-то настройки, которые Вам следует обновить и подогнать под свои нужды, но это может быть хорошей отправной точкой для Вашего проекта. + +## Full Stack FastAPI PostgreSQL + +GitHub: https://github.com/tiangolo/full-stack-fastapi-postgresql + +### Full Stack FastAPI PostgreSQL - Особенности + +* Полностью интегрирован с **Docker** (основан на Docker). +* Развёртывается в режиме Docker Swarm. +* Интегрирован с **Docker Compose** и оптимизирован для локальной разработки. +* **Готовый к реальной работе** веб-сервер Python использующий Uvicorn и Gunicorn. +* Бэкенд построен на фреймворке **FastAPI**: + * **Быстрый**: Высокопроизводительный, на уровне **NodeJS** и **Go** (благодаря Starlette и Pydantic). + * **Интуитивно понятный**: Отличная поддержка редактора. Автодополнение кода везде. Меньше времени на отладку. + * **Простой**: Разработан так, чтоб быть простым в использовании и изучении. Меньше времени на чтение документации. + * **Лаконичный**: Минимизировано повторение кода. Каждый объявленный параметр определяет несколько функций. + * **Надёжный**: Получите готовый к работе код. С автоматической интерактивной документацией. + * **Стандартизированный**: Основан на открытых стандартах API (OpenAPI и JSON Schema) и полностью совместим с ними. + * **Множество других возможностей** включая автоматическую проверку и сериализацию данных, интерактивную документацию, аутентификацию с помощью OAuth2 JWT-токенов и т.д. +* **Безопасное хранение паролей**, которые хэшируются по умолчанию. +* Аутентификация посредством **JWT-токенов**. +* Модели **SQLAlchemy** (независящие от расширений Flask, а значит могут быть непосредственно использованы процессами Celery). +* Базовая модель пользователя (измените или удалите её по необходимости). +* **Alembic** для организации миграций. +* **CORS** (Совместное использование ресурсов из разных источников). +* **Celery**, процессы которого могут выборочно импортировать и использовать модели и код из остальной части бэкенда. +* Тесты, на основе **Pytest**, интегрированные в Docker, чтобы Вы могли полностью проверить Ваше API, независимо от базы данных. Так как тесты запускаются в Docker, для них может создаваться новое хранилище данных каждый раз (Вы можете, по своему желанию, использовать ElasticSearch, MongoDB, CouchDB или другую СУБД, только лишь для проверки - будет ли Ваше API работать с этим хранилищем). +* Простая интеграция Python с **Jupyter Kernels** для разработки удалённо или в Docker с расширениями похожими на Atom Hydrogen или Visual Studio Code Jupyter. +* Фронтенд построен на фреймворке **Vue**: + * Сгенерирован с помощью Vue CLI. + * Поддерживает **аутентификацию с помощью JWT-токенов**. + * Страница логина. + * Перенаправление на страницу главной панели мониторинга после логина. + * Главная страница мониторинга с возможностью создания и изменения пользователей. + * Пользователь может изменять свои данные. + * **Vuex**. + * **Vue-router**. + * **Vuetify** для конструирования красивых компонентов страниц. + * **TypeScript**. + * Сервер Docker основан на **Nginx** (настроен для удобной работы с Vue-router). + * Многоступенчатая сборка Docker, то есть Вам не нужно сохранять или коммитить скомпилированный код. + * Тесты фронтенда запускаются во время сборки (можно отключить). + * Сделан настолько модульно, насколько возможно, поэтому работает "из коробки", но Вы можете повторно сгенерировать фронтенд с помощью Vue CLI или создать то, что Вам нужно и повторно использовать то, что захотите. +* **PGAdmin** для СУБД PostgreSQL, которые легко можно заменить на PHPMyAdmin и MySQL. +* **Flower** для отслеживания работы Celery. +* Балансировка нагрузки между фронтендом и бэкендом с помощью **Traefik**, а значит, Вы можете расположить их на одном домене, разделив url-пути, так как они обслуживаются разными контейнерами. +* Интеграция с Traefik включает автоматическую генерацию сертификатов Let's Encrypt для поддержки протокола **HTTPS**. +* GitLab **CI** (непрерывная интеграция), которая включает тестирование фронтенда и бэкенда. + +## Full Stack FastAPI Couchbase + +GitHub: https://github.com/tiangolo/full-stack-fastapi-couchbase + +⚠️ **ПРЕДУПРЕЖДЕНИЕ** ⚠️ + +Если Вы начинаете новый проект, ознакомьтесь с представленными здесь альтернативами. + +Например, генератор проектов Full Stack FastAPI PostgreSQL может быть более подходящей альтернативой, так как он активно поддерживается и используется. И он включает в себя все новые возможности и улучшения. + +Но никто не запрещает Вам использовать генератор с СУБД Couchbase, возможно, он всё ещё работает нормально. Или у Вас уже есть проект, созданный с помощью этого генератора ранее, и Вы, вероятно, уже обновили его в соответствии со своими потребностями. + +Вы можете прочитать о нём больше в документации соответствующего репозитория. + +## Full Stack FastAPI MongoDB + +...может быть когда-нибудь появится, в зависимости от наличия у меня свободного времени и прочих факторов. 😅 🎉 + +## Модели машинного обучения на основе spaCy и FastAPI + +GitHub: https://github.com/microsoft/cookiecutter-spacy-fastapi + +### Модели машинного обучения на основе spaCy и FastAPI - Особенности + +* Интеграция с моделями **spaCy** NER. +* Встроенный формат запросов к **когнитивному поиску Azure**. +* **Готовый к реальной работе** веб-сервер Python использующий Uvicorn и Gunicorn. +* Встроенное развёртывание на основе **Azure DevOps** Kubernetes (AKS) CI/CD. +* **Многоязычность**. Лёгкий выбор одного из встроенных в spaCy языков во время настройки проекта. +* **Легко подключить** модели из других фреймворков (Pytorch, Tensorflow) не ограничиваясь spaCy. diff --git a/docs/ru/docs/tutorial/background-tasks.md b/docs/ru/docs/tutorial/background-tasks.md index e608f6c8f3..73ba860bc3 100644 --- a/docs/ru/docs/tutorial/background-tasks.md +++ b/docs/ru/docs/tutorial/background-tasks.md @@ -57,21 +57,21 @@ **FastAPI** знает, что нужно сделать в каждом случае и как переиспользовать тот же объект `BackgroundTasks`, так чтобы все фоновые задачи собрались и запустились вместе в фоне: -=== "Python 3.6 и выше" - - ```Python hl_lines="13 15 22 25" - {!> ../../../docs_src/background_tasks/tutorial002.py!} - ``` - -=== "Python 3.10 и выше" +=== "Python 3.10+" ```Python hl_lines="11 13 20 23" {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + В этом примере сообщения будут записаны в `log.txt` *после* того, как ответ сервера был отправлен. -Если бы в запросе была очередь `q`, она бы первой записалась в `log.txt` фоновой задачей (потому что вызывается в зависимости `get_query`). +Если бы в запрос был передан query-параметр `q`, он бы первыми записался в `log.txt` фоновой задачей (потому что вызывается в зависимости `get_query`). После другая фоновая задача, которая была сгенерирована в функции, запишет сообщение из параметра `email`. diff --git a/docs/ru/docs/tutorial/body-fields.md b/docs/ru/docs/tutorial/body-fields.md new file mode 100644 index 0000000000..02a598004a --- /dev/null +++ b/docs/ru/docs/tutorial/body-fields.md @@ -0,0 +1,69 @@ +# Body - Поля + +Таким же способом, как вы объявляете дополнительную валидацию и метаданные в параметрах *функции обработки пути* с помощью функций `Query`, `Path` и `Body`, вы можете объявлять валидацию и метаданные внутри Pydantic моделей, используя функцию `Field` из Pydantic. + +## Импорт `Field` + +Сначала вы должны импортировать его: + +=== "Python 3.10+" + + ```Python hl_lines="2" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +!!! warning "Внимание" + Обратите внимание, что функция `Field` импортируется непосредственно из `pydantic`, а не из `fastapi`, как все остальные функции (`Query`, `Path`, `Body` и т.д.). + +## Объявление атрибутов модели + +Вы можете использовать функцию `Field` с атрибутами модели: + +=== "Python 3.10+" + + ```Python hl_lines="9-12" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +Функция `Field` работает так же, как `Query`, `Path` и `Body`, у неё такие же параметры и т.д. + +!!! note "Технические детали" + На самом деле, `Query`, `Path` и другие функции, которые вы увидите в дальнейшем, создают объекты подклассов общего класса `Param`, который сам по себе является подклассом `FieldInfo` из Pydantic. + + И `Field` (из Pydantic), и `Body`, оба возвращают объекты подкласса `FieldInfo`. + + У класса `Body` есть и другие подклассы, с которыми вы ознакомитесь позже. + + Помните, что когда вы импортируете `Query`, `Path` и другое из `fastapi`, это фактически функции, которые возвращают специальные классы. + +!!! tip "Подсказка" + Обратите внимание, что каждый атрибут модели с типом, значением по умолчанию и `Field` имеет ту же структуру, что и параметр *функции обработки пути* с `Field` вместо `Path`, `Query` и `Body`. + +## Добавление дополнительной информации + +Вы можете объявлять дополнительную информацию в `Field`, `Query`, `Body` и т.п. Она будет включена в сгенерированную JSON схему. + +Вы узнаете больше о добавлении дополнительной информации позже в документации, когда будете изучать, как задавать примеры принимаемых данных. + + +!!! warning "Внимание" + Дополнительные ключи, переданные в функцию `Field`, также будут присутствовать в сгенерированной OpenAPI схеме вашего приложения. + Поскольку эти ключи не являются обязательной частью спецификации OpenAPI, некоторые инструменты OpenAPI, например, [валидатор OpenAPI](https://validator.swagger.io/), могут не работать с вашей сгенерированной схемой. + +## Резюме + +Вы можете использовать функцию `Field` из Pydantic, чтобы задавать дополнительную валидацию и метаданные для атрибутов модели. + +Вы также можете использовать дополнительные ключевые аргументы, чтобы добавить метаданные JSON схемы. diff --git a/docs/ru/docs/tutorial/body-multiple-params.md b/docs/ru/docs/tutorial/body-multiple-params.md new file mode 100644 index 0000000000..e52ef6f6f0 --- /dev/null +++ b/docs/ru/docs/tutorial/body-multiple-params.md @@ -0,0 +1,309 @@ +# Body - Множество параметров + +Теперь, когда мы увидели, как использовать `Path` и `Query` параметры, давайте рассмотрим более продвинутые примеры обьявления тела запроса. + +## Обьединение `Path`, `Query` и параметров тела запроса + +Во-первых, конечно, вы можете объединять параметры `Path`, `Query` и объявления тела запроса в своих функциях обработки, **FastAPI** автоматически определит, что с ними нужно делать. + +Вы также можете объявить параметры тела запроса как необязательные, установив значение по умолчанию, равное `None`: + +=== "Python 3.10+" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="17-19" + {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! Заметка + Рекомендуется использовать версию с `Annotated`, если это возможно. + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + +!!! Заметка + Заметьте, что в данном случае параметр `item`, который будет взят из тела запроса, необязателен. Так как было установлено значение `None` по умолчанию. + +## Несколько параметров тела запроса + +В предыдущем примере, *операции пути* ожидали тело запроса в формате JSON-тело с параметрами, соответствующими атрибутам `Item`, например: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +Но вы также можете объявить множество параметров тела запроса, например `item` и `user`: + +=== "Python 3.10+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` + +В этом случае **FastAPI** заметит, что в функции есть более одного параметра тела (два параметра, которые являются моделями Pydantic). + +Таким образом, имена параметров будут использоваться в качестве ключей (имён полей) в теле запроса, и будет ожидаться запрос следующего формата: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + } +} +``` + +!!! Внимание + Обратите внимание, что хотя параметр `item` был объявлен таким же способом, как и раньше, теперь предпологается, что он находится внутри тела с ключом `item`. + + +**FastAPI** сделает автоматические преобразование из запроса, так что параметр `item` получит своё конкретное содержимое, и то же самое происходит с пользователем `user`. + +Произойдёт проверка составных данных, и создание документации в схеме OpenAPI и автоматических документах. + +## Отдельные значения в теле запроса + +Точно так же, как `Query` и `Path` используются для определения дополнительных данных для query и path параметров, **FastAPI** предоставляет аналогичный инструмент - `Body`. + +Например, расширяя предыдущую модель, вы можете решить, что вам нужен еще один ключ `importance` в том же теле запроса, помимо параметров `item` и `user`. + +Если вы объявите его без указания, какой именно объект (Path, Query, Body и .т.п.) ожидаете, то, поскольку это является простым типом данных, **FastAPI** будет считать, что это query-параметр. + +Но вы можете указать **FastAPI** обрабатывать его, как ещё один ключ тела запроса, используя `Body`: + +=== "Python 3.10+" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="24" + {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ``` + +В этом случае, **FastAPI** будет ожидать тело запроса в формате: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + }, + "importance": 5 +} +``` + +И всё будет работать так же - преобразование типов данных, валидация, документирование и т.д. + +## Множество body и query параметров + +Конечно, вы также можете объявлять query-параметры в любое время, дополнительно к любым body-параметрам. + +Поскольку по умолчанию, отдельные значения интерпретируются как query-параметры, вам не нужно явно добавлять `Query`, вы можете просто сделать так: + +```Python +q: Union[str, None] = None +``` + +Или в Python 3.10 и выше: + +```Python +q: str | None = None +``` + +Например: + +=== "Python 3.10+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="28" + {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="25" + {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + +!!! Информация + `Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`,`Path` и других, которые вы увидите позже. + +## Добавление одного body-параметра + +Предположим, у вас есть только один body-параметр `item` из Pydantic модели `Item`. + +По умолчанию, **FastAPI** ожидает получить тело запроса напрямую. + +Но если вы хотите чтобы он ожидал JSON с ключом `item` с содержимым модели внутри, также как это происходит при объявлении дополнительных body-параметров, вы можете использовать специальный параметр `embed` у типа `Body`: + +```Python +item: Item = Body(embed=True) +``` + +так же, как в этом примере: + +=== "Python 3.10+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="15" + {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + +В этом случае **FastAPI** будет ожидать тело запроса в формате: + +```JSON hl_lines="2" +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + } +} +``` + +вместо этого: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +## Резюме + +Вы можете добавлять несколько body-параметров вашей *функции операции пути*, несмотря даже на то, что запрос может содержать только одно тело. + +Но **FastAPI** справится с этим, предоставит правильные данные в вашей функции, а также сделает валидацию и документацию правильной схемы *операции пути*. + +Вы также можете объявить отдельные значения для получения в рамках тела запроса. + +И вы можете настроить **FastAPI** таким образом, чтобы включить тело запроса в ключ, даже если объявлен только один параметр. diff --git a/docs/ru/docs/tutorial/body-nested-models.md b/docs/ru/docs/tutorial/body-nested-models.md new file mode 100644 index 0000000000..bbf9b76859 --- /dev/null +++ b/docs/ru/docs/tutorial/body-nested-models.md @@ -0,0 +1,382 @@ +# Body - Вложенные модели + +С помощью **FastAPI**, вы можете определять, валидировать, документировать и использовать модели произвольной вложенности (благодаря библиотеке Pydantic). + +## Определение полей содержащих списки + +Вы можете определять атрибут как подтип. Например, тип `list` в Python: + +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial001.py!} + ``` + +Это приведёт к тому, что обьект `tags` преобразуется в список, несмотря на то что тип его элементов не объявлен. + +## Определение полей содержащих список с определением типов его элементов + +Однако в Python есть способ объявления списков с указанием типов для вложенных элементов: + +### Импортируйте `List` из модуля typing + +В Python 3.9 и выше вы можете использовать стандартный тип `list` для объявления аннотаций типов, как мы увидим ниже. 💡 + +Но в версиях Python до 3.9 (начиная с 3.6) сначала вам необходимо импортировать `List` из стандартного модуля `typing` в Python: + +```Python hl_lines="1" +{!> ../../../docs_src/body_nested_models/tutorial002.py!} +``` + +### Объявление `list` с указанием типов для вложенных элементов + +Объявление типов для элементов (внутренних типов) вложенных в такие типы как `list`, `dict`, `tuple`: + +* Если у вас Python версии ниже чем 3.9, импортируйте их аналог из модуля `typing` +* Передайте внутренний(ие) тип(ы) как "параметры типа", используя квадратные скобки: `[` и `]` + +В Python версии 3.9 это будет выглядеть так: + +```Python +my_list: list[str] +``` + +В версиях Python до 3.9 это будет выглядеть так: + +```Python +from typing import List + +my_list: List[str] +``` + +Это всё стандартный синтаксис Python для объявления типов. + +Используйте этот же стандартный синтаксис для атрибутов модели с внутренними типами. + +Таким образом, в нашем примере мы можем явно указать тип данных для поля `tags` как "список строк": + +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002.py!} + ``` + +## Типы множеств + +Но затем мы подумали и поняли, что теги не должны повторяться и, вероятно, они должны быть уникальными строками. + +И в Python есть специальный тип данных для множеств уникальных элементов - `set`. + +Тогда мы можем обьявить поле `tags` как множество строк: + +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 14" + {!> ../../../docs_src/body_nested_models/tutorial003.py!} + ``` + +С помощью этого, даже если вы получите запрос с повторяющимися данными, они будут преобразованы в множество уникальных элементов. + +И когда вы выводите эти данные, даже если исходный набор содержал дубликаты, они будут выведены в виде множества уникальных элементов. + +И они также будут соответствующим образом аннотированы / задокументированы. + +## Вложенные Модели + +У каждого атрибута Pydantic-модели есть тип. + +Но этот тип может сам быть другой моделью Pydantic. + +Таким образом вы можете объявлять глубоко вложенные JSON "объекты" с определёнными именами атрибутов, типами и валидацией. + +Всё это может быть произвольно вложенным. + +### Определение подмодели + +Например, мы можем определить модель `Image`: + +=== "Python 3.10+" + + ```Python hl_lines="7-9" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +### Использование вложенной модели в качестве типа + +Также мы можем использовать эту модель как тип атрибута: + +=== "Python 3.10+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +Это означает, что **FastAPI** будет ожидать тело запроса, аналогичное этому: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": ["rock", "metal", "bar"], + "image": { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + } +} +``` + +Ещё раз: сделав такое объявление, с помощью **FastAPI** вы получите: + +* Поддержку редакторов IDE (автодополнение и т.д), даже для вложенных моделей +* Преобразование данных +* Валидацию данных +* Автоматическую документацию + +## Особые типы и валидация + +Помимо обычных простых типов, таких как `str`, `int`, `float`, и т.д. Вы можете использовать более сложные базовые типы, которые наследуются от типа `str`. + +Чтобы увидеть все варианты, которые у вас есть, ознакомьтесь с документацией по необычным типам Pydantic. Вы увидите некоторые примеры в следующей главе. + +Например, так как в модели `Image` у нас есть поле `url`, то мы можем объявить его как тип `HttpUrl` из модуля Pydantic вместо типа `str`: + +=== "Python 3.10+" + + ```Python hl_lines="2 8" + {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005.py!} + ``` + +Строка будет проверена на соответствие допустимому URL-адресу и задокументирована в JSON схему / OpenAPI. + +## Атрибуты, содержащие списки подмоделей + +Вы также можете использовать модели Pydantic в качестве типов вложенных в `list`, `set` и т.д: + +=== "Python 3.10+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006.py!} + ``` + +Такая реализация будет ожидать (конвертировать, валидировать, документировать и т.д) JSON-содержимое в следующем формате: + +```JSON hl_lines="11" +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": [ + "rock", + "metal", + "bar" + ], + "images": [ + { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + }, + { + "url": "http://example.com/dave.jpg", + "name": "The Baz" + } + ] +} +``` + +!!! info "Информация" + Заметьте, что теперь у ключа `images` есть список объектов изображений. + +## Глубоко вложенные модели + +Вы можете определять модели с произвольным уровнем вложенности: + +=== "Python 3.10+" + + ```Python hl_lines="7 12 18 21 25" + {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007.py!} + ``` + +!!! info "Информация" + Заметьте, что у объекта `Offer` есть список объектов `Item`, которые, в свою очередь, могут содержать необязательный список объектов `Image` + +## Тела с чистыми списками элементов + +Если верхний уровень значения тела JSON-объекта представляет собой JSON `array` (в Python - `list`), вы можете объявить тип в параметре функции, так же, как в моделях Pydantic: + +```Python +images: List[Image] +``` + +в Python 3.9 и выше: + +```Python +images: list[Image] +``` + +например так: + +=== "Python 3.9+" + + ```Python hl_lines="13" + {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_nested_models/tutorial008.py!} + ``` + +## Универсальная поддержка редактора + +И вы получаете поддержку редактора везде. + +Даже для элементов внутри списков: + + + +Вы не могли бы получить такую поддержку редактора, если бы работали напрямую с `dict`, а не с моделями Pydantic. + +Но вы также не должны беспокоиться об этом, входящие словари автоматически конвертируются, а ваш вывод также автоматически преобразуется в формат JSON. + +## Тела запросов с произвольными словарями (`dict` ) + +Вы также можете объявить тело запроса как `dict` с ключами определенного типа и значениями другого типа данных. + +Без необходимости знать заранее, какие значения являются допустимыми для имён полей/атрибутов (как это было бы в случае с моделями Pydantic). + +Это было бы полезно, если вы хотите получить ключи, которые вы еще не знаете. + +--- + +Другой полезный случай - когда вы хотите чтобы ключи были другого типа данных, например, `int`. + +Именно это мы сейчас и увидим здесь. + +В этом случае вы принимаете `dict`, пока у него есть ключи типа `int` со значениями типа `float`: + +=== "Python 3.9+" + + ```Python hl_lines="7" + {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/body_nested_models/tutorial009.py!} + ``` + +!!! tip "Совет" + Имейте в виду, что JSON поддерживает только ключи типа `str`. + + Но Pydantic обеспечивает автоматическое преобразование данных. + + Это значит, что даже если пользователи вашего API могут отправлять только строки в качестве ключей, при условии, что эти строки содержат целые числа, Pydantic автоматический преобразует и валидирует эти данные. + + А `dict`, с именем `weights`, который вы получите в качестве ответа Pydantic, действительно будет иметь ключи типа `int` и значения типа `float`. + +## Резюме + +С помощью **FastAPI** вы получаете максимальную гибкость, предоставляемую моделями Pydantic, сохраняя при этом простоту, краткость и элегантность вашего кода. + +И дополнительно вы получаете: + +* Поддержку редактора (автодополнение доступно везде!) +* Преобразование данных (также известно как парсинг / сериализация) +* Валидацию данных +* Документацию схемы данных +* Автоматическую генерацию документации diff --git a/docs/ru/docs/tutorial/body.md b/docs/ru/docs/tutorial/body.md new file mode 100644 index 0000000000..c03d40c3fb --- /dev/null +++ b/docs/ru/docs/tutorial/body.md @@ -0,0 +1,165 @@ +# Тело запроса + +Когда вам необходимо отправить данные из клиента (допустим, браузера) в ваш API, вы отправляете их как **тело запроса**. + +Тело **запроса** --- это данные, отправляемые клиентом в ваш API. Тело **ответа** --- это данные, которые ваш API отправляет клиенту. + +Ваш API почти всегда отправляет тело **ответа**. Но клиентам не обязательно всегда отправлять тело **запроса**. + +Чтобы объявить тело **запроса**, необходимо использовать модели Pydantic, со всей их мощью и преимуществами. + +!!! info "Информация" + Чтобы отправить данные, необходимо использовать один из методов: `POST` (обычно), `PUT`, `DELETE` или `PATCH`. + + Отправка тела с запросом `GET` имеет неопределенное поведение в спецификациях, тем не менее, оно поддерживается FastAPI только для очень сложных/экстремальных случаев использования. + + Поскольку это не рекомендуется, интерактивная документация со Swagger UI не будет отображать информацию для тела при использовании метода GET, а промежуточные прокси-серверы могут не поддерживать такой вариант запроса. + +## Импортирование `BaseModel` из Pydantic + +Первое, что вам необходимо сделать, это импортировать `BaseModel` из пакета `pydantic`: + +```Python hl_lines="4" +{!../../../docs_src/body/tutorial001.py!} +``` + +## Создание вашей собственной модели + +После этого вы описываете вашу модель данных как класс, наследующий от `BaseModel`. + +Используйте аннотации типов Python для всех атрибутов: + +```Python hl_lines="7-11" +{!../../../docs_src/body/tutorial001.py!} +``` + +Также как и при описании параметров запроса, когда атрибут модели имеет значение по умолчанию, он является необязательным. Иначе он обязателен. Используйте `None`, чтобы сделать его необязательным без использования конкретных значений по умолчанию. + +Например, модель выше описывает вот такой JSON "объект" (или словарь Python): + +```JSON +{ + "name": "Foo", + "description": "An optional description", + "price": 45.2, + "tax": 3.5 +} +``` + +...поскольку `description` и `tax` являются необязательными (с `None` в качестве значения по умолчанию), вот такой JSON "объект" также подходит: + +```JSON +{ + "name": "Foo", + "price": 45.2 +} +``` + +## Объявление как параметра функции + +Чтобы добавить параметр к вашему *обработчику*, объявите его также, как вы объявляли параметры пути или параметры запроса: + +```Python hl_lines="18" +{!../../../docs_src/body/tutorial001.py!} +``` + +...и укажите созданную модель в качестве типа параметра, `Item`. + +## Результаты + +Всего лишь с помощью аннотации типов Python, **FastAPI**: + +* Читает тело запроса как JSON. +* Приводит к соответствующим типам (если есть необходимость). +* Проверяет корректность данных. + * Если данные некорректны, будет возращена читаемая и понятная ошибка, показывающая что именно и в каком месте некорректно в данных. +* Складывает полученные данные в параметр `item`. + * Поскольку внутри функции вы объявили его с типом `Item`, то теперь у вас есть поддержка со стороны редактора (автодополнение и т.п.) для всех атрибутов и их типов. +* Генерирует декларативное описание модели в виде JSON Schema, так что вы можете его использовать где угодно, если это имеет значение для вашего проекта. +* Эти схемы являются частью сгенерированной схемы OpenAPI и используются для автоматического документирования UI. + +## Автоматическое документирование + +Схема JSON ваших моделей будет частью сгенерированной схемы OpenAPI и будет отображена в интерактивной документации API: + + + +Также она будет указана в документации по API внутри каждой *операции пути*, в которой используются: + + + +## Поддержка редактора + +В вашем редакторе внутри вашей функции у вас будут подсказки по типам и автодополнение (это не будет работать, если вы получаете словарь вместо модели Pydantic): + + + +Также вы будете получать ошибки в случае несоответствия типов: + + + +Это не случайно, весь фреймворк построен вокруг такого дизайна. + +И это все тщательно протестировано еще на этапе разработки дизайна, до реализации, чтобы это работало со всеми редакторами. + +Для поддержки этого даже были внесены некоторые изменения в сам Pydantic. + +На всех предыдущих скриншотах используется Visual Studio Code. + +Но у вас будет такая же поддержка и с PyCharm, и вообще с любым редактором Python: + + + +!!! tip "Подсказка" + Если вы используете PyCharm в качестве редактора, то вам стоит попробовать плагин Pydantic PyCharm Plugin. + + Он улучшает поддержку редактором моделей Pydantic в части: + + * автодополнения, + * проверки типов, + * рефакторинга, + * поиска, + * инспектирования. + +## Использование модели + +Внутри функции вам доступны все атрибуты объекта модели напрямую: + +```Python hl_lines="21" +{!../../../docs_src/body/tutorial002.py!} +``` + +## Тело запроса + параметры пути + +Вы можете одновременно объявлять параметры пути и тело запроса. + +**FastAPI** распознает, какие параметры функции соответствуют параметрам пути и должны быть **получены из пути**, а какие параметры функции, объявленные как модели Pydantic, должны быть **получены из тела запроса**. + +```Python hl_lines="17-18" +{!../../../docs_src/body/tutorial003.py!} +``` + +## Тело запроса + параметры пути + параметры запроса + +Вы также можете одновременно объявить параметры для **пути**, **запроса** и **тела запроса**. + +**FastAPI** распознает каждый из них и возьмет данные из правильного источника. + +```Python hl_lines="18" +{!../../../docs_src/body/tutorial004.py!} +``` + +Параметры функции распознаются следующим образом: + +* Если параметр также указан в **пути**, то он будет использоваться как параметр пути. +* Если аннотация типа параметра содержит **примитивный тип** (`int`, `float`, `str`, `bool` и т.п.), он будет интерпретирован как параметр **запроса**. +* Если аннотация типа параметра представляет собой **модель Pydantic**, он будет интерпретирован как параметр **тела запроса**. + +!!! note "Заметка" + FastAPI понимает, что значение параметра `q` не является обязательным, потому что имеет значение по умолчанию `= None`. + + Аннотация `Optional` в `Optional[str]` не используется FastAPI, но помогает вашему редактору лучше понимать ваш код и обнаруживать ошибки. + +## Без Pydantic + +Если вы не хотите использовать модели Pydantic, вы все еще можете использовать параметры **тела запроса**. Читайте в документации раздел [Тело - Несколько параметров: Единичные значения в теле](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. diff --git a/docs/ru/docs/tutorial/cookie-params.md b/docs/ru/docs/tutorial/cookie-params.md new file mode 100644 index 0000000000..5f99458b69 --- /dev/null +++ b/docs/ru/docs/tutorial/cookie-params.md @@ -0,0 +1,49 @@ +# Параметры Cookie + +Вы можете задать параметры Cookie таким же способом, как `Query` и `Path` параметры. + +## Импорт `Cookie` + +Сначала импортируйте `Cookie`: + +=== "Python 3.10+" + + ```Python hl_lines="1" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +## Объявление параметров `Cookie` + +Затем объявляйте параметры cookie, используя ту же структуру, что и с `Path` и `Query`. + +Первое значение - это значение по умолчанию, вы можете передать все дополнительные параметры проверки или аннотации: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +!!! note "Технические детали" + `Cookie` - это класс, родственный `Path` и `Query`. Он также наследуется от общего класса `Param`. + + Но помните, что когда вы импортируете `Query`, `Path`, `Cookie` и другое из `fastapi`, это фактически функции, которые возвращают специальные классы. + +!!! info "Дополнительная информация" + Для объявления cookies, вам нужно использовать `Cookie`, иначе параметры будут интерпретированы как параметры запроса. + +## Резюме + +Объявляйте cookies с помощью `Cookie`, используя тот же общий шаблон, что и `Query`, и `Path`. diff --git a/docs/ru/docs/tutorial/cors.md b/docs/ru/docs/tutorial/cors.md new file mode 100644 index 0000000000..8c7fbc046d --- /dev/null +++ b/docs/ru/docs/tutorial/cors.md @@ -0,0 +1,84 @@ +# CORS (Cross-Origin Resource Sharing) + +Понятие CORS или "Cross-Origin Resource Sharing" относится к ситуациям, при которых запущенный в браузере фронтенд содержит JavaScript-код, который взаимодействует с бэкендом, находящимся на другом "источнике" ("origin"). + +## Источник + +Источник - это совокупность протокола (`http`, `https`), домена (`myapp.com`, `localhost`, `localhost.tiangolo.com`) и порта (`80`, `443`, `8080`). + +Поэтому это три разных источника: + +* `http://localhost` +* `https://localhost` +* `http://localhost:8080` + +Даже если они все расположены в `localhost`, они используют разные протоколы и порты, а значит, являются разными источниками. + +## Шаги + +Допустим, у вас есть фронтенд, запущенный в браузере по адресу `http://localhost:8080`, и его JavaScript-код пытается взаимодействовать с бэкендом, запущенным по адресу `http://localhost` (поскольку мы не указали порт, браузер по умолчанию будет использовать порт `80`). + +Затем браузер отправит бэкенду HTTP-запрос `OPTIONS`, и если бэкенд вернёт соответствующие заголовки для авторизации взаимодействия с другим источником (`http://localhost:8080`), то браузер разрешит JavaScript-коду на фронтенде отправить запрос на этот бэкенд. + +Чтобы это работало, у бэкенда должен быть список "разрешённых источников" ("allowed origins"). + +В таком случае этот список должен содержать `http://localhost:8080`, чтобы фронтенд работал корректно. + +## Подстановочный символ `"*"` + +В качестве списка источников можно указать подстановочный символ `"*"` ("wildcard"), чтобы разрешить любые источники. + +Но тогда не будут разрешены некоторые виды взаимодействия, включая всё связанное с учётными данными: куки, заголовки Authorization с Bearer-токенами наподобие тех, которые мы использовали ранее и т.п. + +Поэтому, чтобы всё работало корректно, лучше явно указывать список разрешённых источников. + +## Использование `CORSMiddleware` + +Вы можете настроить этот механизм в вашем **FastAPI** приложении, используя `CORSMiddleware`. + +* Импортируйте `CORSMiddleware`. +* Создайте список разрешённых источников (в виде строк). +* Добавьте его как "middleware" к вашему **FastAPI** приложению. + +Вы также можете указать, разрешает ли ваш бэкенд использование: + +* Учётных данных (включая заголовки Authorization, куки и т.п.). +* Отдельных HTTP-методов (`POST`, `PUT`) или всех вместе, используя `"*"`. +* Отдельных HTTP-заголовков или всех вместе, используя `"*"`. + +```Python hl_lines="2 6-11 13-19" +{!../../../docs_src/cors/tutorial001.py!} +``` + +`CORSMiddleware` использует для параметров "запрещающие" значения по умолчанию, поэтому вам нужно явным образом разрешить использование отдельных источников, методов или заголовков, чтобы браузеры могли использовать их в кросс-доменном контексте. + +Поддерживаются следующие аргументы: + +* `allow_origins` - Список источников, на которые разрешено выполнять кросс-доменные запросы. Например, `['https://example.org', 'https://www.example.org']`. Можно использовать `['*']`, чтобы разрешить любые источники. +* `allow_origin_regex` - Регулярное выражение для определения источников, на которые разрешено выполнять кросс-доменные запросы. Например, `'https://.*\.example\.org'`. +* `allow_methods` - Список HTTP-методов, которые разрешены для кросс-доменных запросов. По умолчанию равно `['GET']`. Можно использовать `['*']`, чтобы разрешить все стандартные методы. +* `allow_headers` - Список HTTP-заголовков, которые должны поддерживаться при кросс-доменных запросах. По умолчанию равно `[]`. Можно использовать `['*']`, чтобы разрешить все заголовки. Заголовки `Accept`, `Accept-Language`, `Content-Language` и `Content-Type` всегда разрешены для простых CORS-запросов. +* `allow_credentials` - указывает, что куки разрешены в кросс-доменных запросах. По умолчанию равно `False`. Также, `allow_origins` нельзя присвоить `['*']`, если разрешено использование учётных данных. В таком случае должен быть указан список источников. +* `expose_headers` - Указывает любые заголовки ответа, которые должны быть доступны браузеру. По умолчанию равно `[]`. +* `max_age` - Устанавливает максимальное время в секундах, в течение которого браузер кэширует CORS-ответы. По умолчанию равно `600`. + +`CORSMiddleware` отвечает на два типа HTTP-запросов... + +### CORS-запросы с предварительной проверкой + +Это любые `OPTIONS` запросы с заголовками `Origin` и `Access-Control-Request-Method`. + +В этом случае middleware перехватит входящий запрос и отправит соответствующие CORS-заголовки в ответе, а также ответ `200` или `400` в информационных целях. + +### Простые запросы + +Любые запросы с заголовком `Origin`. В этом случае middleware передаст запрос дальше как обычно, но добавит соответствующие CORS-заголовки к ответу. + +## Больше информации + +Для получения более подробной информации о CORS, обратитесь к Документации CORS от Mozilla. + +!!! note "Технические детали" + Вы также можете использовать `from starlette.middleware.cors import CORSMiddleware`. + + **FastAPI** предоставляет несколько middleware в `fastapi.middleware` только для вашего удобства как разработчика. Но большинство доступных middleware взяты напрямую из Starlette. diff --git a/docs/ru/docs/tutorial/debugging.md b/docs/ru/docs/tutorial/debugging.md new file mode 100644 index 0000000000..38709e56df --- /dev/null +++ b/docs/ru/docs/tutorial/debugging.md @@ -0,0 +1,112 @@ +# Отладка + +Вы можете подключить отладчик в своем редакторе, например, в Visual Studio Code или PyCharm. + +## Вызов `uvicorn` + +В вашем FastAPI приложении, импортируйте и вызовите `uvicorn` напрямую: + +```Python hl_lines="1 15" +{!../../../docs_src/debugging/tutorial001.py!} +``` + +### Описание `__name__ == "__main__"` + +Главная цель использования `__name__ == "__main__"` в том, чтобы код выполнялся при запуске файла с помощью: + +
+ +```console +$ python myapp.py +``` + +
+ +но не вызывался, когда другой файл импортирует это, например: + +```Python +from myapp import app +``` + +#### Больше деталей + +Давайте назовём ваш файл `myapp.py`. + +Если вы запустите его с помощью: + +
+ +```console +$ python myapp.py +``` + +
+ +то встроенная переменная `__name__`, автоматически создаваемая Python в вашем файле, будет иметь значение строкового типа `"__main__"`. + +Тогда выполнится условие и эта часть кода: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +будет запущена. + +--- + +Но этого не произойдет, если вы импортируете этот модуль (файл). + +Таким образом, если у вас есть файл `importer.py` с таким импортом: + +```Python +from myapp import app + +# Some more code +``` + +то автоматическая создаваемая внутри файла `myapp.py` переменная `__name__` будет иметь значение отличающееся от `"__main__"`. + +Следовательно, строка: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +не будет выполнена. + +!!! Информация + Для получения дополнительной информации, ознакомьтесь с официальной документацией Python. + +## Запуск вашего кода с помощью отладчика + +Так как вы запускаете сервер Uvicorn непосредственно из вашего кода, вы можете вызвать Python программу (ваше FastAPI приложение) напрямую из отладчика. + +--- + +Например, в Visual Studio Code вы можете выполнить следующие шаги: + +* Перейдите на панель "Debug". +* Выберите "Add configuration...". +* Выберите "Python" +* Запустите отладчик "`Python: Current File (Integrated Terminal)`". + +Это запустит сервер с вашим **FastAPI** кодом, остановится на точках останова, и т.д. + +Вот как это может выглядеть: + + + +--- + +Если используете Pycharm, вы можете выполнить следующие шаги: + +* Открыть "Run" меню. +* Выбрать опцию "Debug...". +* Затем в появившемся контекстном меню. +* Выбрать файл для отладки (в данном случае, `main.py`). + +Это запустит сервер с вашим **FastAPI** кодом, остановится на точках останова, и т.д. + +Вот как это может выглядеть: + + diff --git a/docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md new file mode 100644 index 0000000000..b6ad25dafc --- /dev/null +++ b/docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md @@ -0,0 +1,478 @@ +# Классы как зависимости + +Прежде чем углубиться в систему **Внедрения Зависимостей**, давайте обновим предыдущий пример. + +## `Словарь` из предыдущего примера + +В предыдущем примере мы возвращали `словарь` из нашей зависимости: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +Но затем мы получаем `словарь` в параметре `commons` *функции операции пути*. И мы знаем, что редакторы не могут обеспечить достаточную поддержку для `словаря`, поскольку они не могут знать их ключи и типы значений. + +Мы можем сделать лучше... + +## Что делает зависимость + +До сих пор вы видели зависимости, объявленные как функции. + +Но это не единственный способ объявления зависимостей (хотя, вероятно, более распространенный). + +Ключевым фактором является то, что зависимость должна быть "вызываемой". + +В Python "**вызываемый**" - это все, что Python может "вызвать", как функцию. + +Так, если у вас есть объект `something` (который может _не_ быть функцией) и вы можете "вызвать" его (выполнить) как: + +```Python +something() +``` + +или + +```Python +something(some_argument, some_keyword_argument="foo") +``` + +в таком случае он является "вызываемым". + +## Классы как зависимости + +Вы можете заметить, что для создания экземпляра класса в Python используется тот же синтаксис. + +Например: + +```Python +class Cat: + def __init__(self, name: str): + self.name = name + + +fluffy = Cat(name="Mr Fluffy") +``` + +В данном случае `fluffy` является экземпляром класса `Cat`. + +А чтобы создать `fluffy`, вы "вызываете" `Cat`. + +Таким образом, класс в Python также является **вызываемым**. + +Тогда в **FastAPI** в качестве зависимости можно использовать класс Python. + +На самом деле FastAPI проверяет, что переданный объект является "вызываемым" (функция, класс или что-либо еще) и указаны необходимые для его вызова параметры. + +Если вы передаёте что-то, что можно "вызывать" в качестве зависимости в **FastAPI**, то он будет анализировать параметры, необходимые для "вызова" этого объекта и обрабатывать их так же, как параметры *функции операции пути*. Включая подзависимости. + +Это относится и к вызываемым объектам без параметров. Работа с ними происходит точно так же, как и для *функций операции пути* без параметров. + +Теперь мы можем изменить зависимость `common_parameters`, указанную выше, на класс `CommonQueryParams`: + +=== "Python 3.10+" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12-16" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9-13" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +Обратите внимание на метод `__init__`, используемый для создания экземпляра класса: + +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +...имеет те же параметры, что и ранее используемая функция `common_parameters`: + +=== "Python 3.10+" + + ```Python hl_lines="8" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="6" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +Эти параметры и будут использоваться **FastAPI** для "решения" зависимости. + +В обоих случаях она будет иметь: + +* Необязательный параметр запроса `q`, представляющий собой `str`. +* Параметр запроса `skip`, представляющий собой `int`, по умолчанию `0`. +* Параметр запроса `limit`, представляющий собой `int`, по умолчанию равный `100`. + +В обоих случаях данные будут конвертированы, валидированы, документированы по схеме OpenAPI и т.д. + +## Как это использовать + +Теперь вы можете объявить свою зависимость, используя этот класс. + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +**FastAPI** вызывает класс `CommonQueryParams`. При этом создается "экземпляр" этого класса, который будет передан в качестве параметра `commons` в вашу функцию. + +## Аннотация типа или `Depends` + +Обратите внимание, что в приведенном выше коде мы два раза пишем `CommonQueryParams`: + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` + +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +Последний параметр `CommonQueryParams`, в: + +```Python +... Depends(CommonQueryParams) +``` + +...это то, что **FastAPI** будет использовать, чтобы узнать, что является зависимостью. + +Из него FastAPI извлечёт объявленные параметры и именно их будет вызывать. + +--- + +В этом случае первый `CommonQueryParams`, в: + +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, ... + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python + commons: CommonQueryParams ... + ``` + +...не имеет никакого специального значения для **FastAPI**. FastAPI не будет использовать его для преобразования данных, валидации и т.д. (поскольку для этого используется `Depends(CommonQueryParams)`). + +На самом деле можно написать просто: + +=== "Python 3.6+" + + ```Python + commons: Annotated[Any, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python + commons = Depends(CommonQueryParams) + ``` + +...как тут: + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial003_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial003_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + +Но объявление типа приветствуется, так как в этом случае ваш редактор будет знать, что будет передано в качестве параметра `commons`, и тогда он сможет помочь вам с автодополнением, проверкой типов и т.д: + + + +## Сокращение + +Но вы видите, что здесь мы имеем некоторое повторение кода, дважды написав `CommonQueryParams`: + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` + +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +Для случаев, когда зависимостью является *конкретный* класс, который **FastAPI** "вызовет" для создания экземпляра этого класса, можно использовать укороченную запись. + + +Вместо того чтобы писать: + +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` + +...следует написать: + +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, Depends()] + ``` + +=== "Python 3.6 без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python + commons: CommonQueryParams = Depends() + ``` + +Вы объявляете зависимость как тип параметра и используете `Depends()` без какого-либо параметра, вместо того чтобы *снова* писать полный класс внутри `Depends(CommonQueryParams)`. + +Аналогичный пример будет выглядеть следующим образом: + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial004_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial004_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + +...и **FastAPI** будет знать, что делать. + +!!! tip "Подсказка" + Если это покажется вам более запутанным, чем полезным, не обращайте внимания, это вам не *нужно*. + + Это просто сокращение. Потому что **FastAPI** заботится о том, чтобы помочь вам свести к минимуму повторение кода. diff --git a/docs/ru/docs/tutorial/dependencies/global-dependencies.md b/docs/ru/docs/tutorial/dependencies/global-dependencies.md new file mode 100644 index 0000000000..eb1b4d7c1c --- /dev/null +++ b/docs/ru/docs/tutorial/dependencies/global-dependencies.md @@ -0,0 +1,34 @@ +# Глобальные зависимости + +Для некоторых типов приложений может потребоваться добавить зависимости ко всему приложению. + +Подобно тому, как вы можете [добавлять зависимости через параметр `dependencies` в *декораторах операций пути*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, вы можете добавлять зависимости сразу ко всему `FastAPI` приложению. + +В этом случае они будут применяться ко всем *операциям пути* в приложении: + +=== "Python 3.9+" + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial012_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial012_an.py!} + ``` + +=== "Python 3.8 non-Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать 'Annotated' версию, если это возможно. + + ```Python hl_lines="15" + {!> ../../../docs_src/dependencies/tutorial012.py!} + ``` + +Все способы [добавления зависимостей в *декораторах операций пути*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} по-прежнему применимы, но в данном случае зависимости применяются ко всем *операциям пути* приложения. + +## Зависимости для групп *операций пути* + +Позднее, читая о том, как структурировать более крупные [приложения, содержащие много файлов](../../tutorial/bigger-applications.md){.internal-link target=_blank}, вы узнаете, как объявить один параметр dependencies для целой группы *операций пути*. diff --git a/docs/ru/docs/tutorial/extra-data-types.md b/docs/ru/docs/tutorial/extra-data-types.md new file mode 100644 index 0000000000..0f613a6b29 --- /dev/null +++ b/docs/ru/docs/tutorial/extra-data-types.md @@ -0,0 +1,82 @@ +# Дополнительные типы данных + +До сих пор вы использовали простые типы данных, такие как: + +* `int` +* `float` +* `str` +* `bool` + +Но вы также можете использовать и более сложные типы. + +При этом у вас останутся те же возможности , что и до сих пор: + +* Отличная поддержка редактора. +* Преобразование данных из входящих запросов. +* Преобразование данных для ответа. +* Валидация данных. +* Автоматическая аннотация и документация. + +## Другие типы данных + +Ниже перечислены некоторые из дополнительных типов данных, которые вы можете использовать: + +* `UUID`: + * Стандартный "Универсальный уникальный идентификатор", используемый в качестве идентификатора во многих базах данных и системах. + * В запросах и ответах будет представлен как `str`. +* `datetime.datetime`: + * Встроенный в Python `datetime.datetime`. + * В запросах и ответах будет представлен как `str` в формате ISO 8601, например: `2008-09-15T15:53:00+05:00`. +* `datetime.date`: + * Встроенный в Python `datetime.date`. + * В запросах и ответах будет представлен как `str` в формате ISO 8601, например: `2008-09-15`. +* `datetime.time`: + * Встроенный в Python `datetime.time`. + * В запросах и ответах будет представлен как `str` в формате ISO 8601, например: `14:23:55.003`. +* `datetime.timedelta`: + * Встроенный в Python `datetime.timedelta`. + * В запросах и ответах будет представлен в виде общего количества секунд типа `float`. + * Pydantic также позволяет представить его как "Кодировку разницы во времени ISO 8601", см. документацию для получения дополнительной информации. +* `frozenset`: + * В запросах и ответах обрабатывается так же, как и `set`: + * В запросах будет прочитан список, исключены дубликаты и преобразован в `set`. + * В ответах `set` будет преобразован в `list`. + * В сгенерированной схеме будет указано, что значения `set` уникальны (с помощью JSON-схемы `uniqueItems`). +* `bytes`: + * Встроенный в Python `bytes`. + * В запросах и ответах будет рассматриваться как `str`. + * В сгенерированной схеме будет указано, что это `str` в формате `binary`. +* `Decimal`: + * Встроенный в Python `Decimal`. + * В запросах и ответах обрабатывается так же, как и `float`. +* Вы можете проверить все допустимые типы данных pydantic здесь: Типы данных Pydantic. + +## Пример + +Вот пример *операции пути* с параметрами, который демонстрирует некоторые из вышеперечисленных типов. + +=== "Python 3.8 и выше" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="1 2 11-15" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` + +Обратите внимание, что параметры внутри функции имеют свой естественный тип данных, и вы, например, можете выполнять обычные манипуляции с датами, такие как: + +=== "Python 3.8 и выше" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` diff --git a/docs/ru/docs/tutorial/extra-models.md b/docs/ru/docs/tutorial/extra-models.md new file mode 100644 index 0000000000..30176b4e3c --- /dev/null +++ b/docs/ru/docs/tutorial/extra-models.md @@ -0,0 +1,252 @@ +# Дополнительные модели + +В продолжение прошлого примера будет уже обычным делом иметь несколько связанных между собой моделей. + +Это особенно применимо в случае моделей пользователя, потому что: + +* **Модель для ввода** должна иметь возможность содержать пароль. +* **Модель для вывода** не должна содержать пароль. +* **Модель для базы данных**, возможно, должна содержать хэшированный пароль. + +!!! danger "Внимание" + Никогда не храните пароли пользователей в чистом виде. Всегда храните "безопасный хэш", который вы затем сможете проверить. + + Если вам это не знакомо, вы можете узнать про "хэш пароля" в [главах о безопасности](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}. + +## Множественные модели + +Ниже изложена основная идея того, как могут выглядеть эти модели с полями для паролей, а также описаны места, где они используются: + +=== "Python 3.10+" + + ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" + {!> ../../../docs_src/extra_models/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" + {!> ../../../docs_src/extra_models/tutorial001.py!} + ``` + +### Про `**user_in.dict()` + +#### `.dict()` из Pydantic + +`user_in` - это Pydantic-модель класса `UserIn`. + +У Pydantic-моделей есть метод `.dict()`, который возвращает `dict` с данными модели. + +Поэтому, если мы создадим Pydantic-объект `user_in` таким способом: + +```Python +user_in = UserIn(username="john", password="secret", email="john.doe@example.com") +``` + +и затем вызовем: + +```Python +user_dict = user_in.dict() +``` + +то теперь у нас есть `dict` с данными модели в переменной `user_dict` (это `dict` вместо объекта Pydantic-модели). + +И если мы вызовем: + +```Python +print(user_dict) +``` + +мы можем получить `dict` с такими данными: + +```Python +{ + 'username': 'john', + 'password': 'secret', + 'email': 'john.doe@example.com', + 'full_name': None, +} +``` + +#### Распаковка `dict` + +Если мы возьмём `dict` наподобие `user_dict` и передадим его в функцию (или класс), используя `**user_dict`, Python распакует его. Он передаст ключи и значения `user_dict` напрямую как аргументы типа ключ-значение. + +Поэтому, продолжая описанный выше пример с `user_dict`, написание такого кода: + +```Python +UserInDB(**user_dict) +``` + +Будет работать так же, как примерно такой код: + +```Python +UserInDB( + username="john", + password="secret", + email="john.doe@example.com", + full_name=None, +) +``` + +Или, если для большей точности мы напрямую используем `user_dict` с любым потенциальным содержимым, то этот пример будет выглядеть так: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], +) +``` + +#### Pydantic-модель из содержимого другой модели + +Как в примере выше мы получили `user_dict` из `user_in.dict()`, этот код: + +```Python +user_dict = user_in.dict() +UserInDB(**user_dict) +``` + +будет равнозначен такому: + +```Python +UserInDB(**user_in.dict()) +``` + +...потому что `user_in.dict()` - это `dict`, и затем мы указываем, чтобы Python его "распаковал", когда передаём его в `UserInDB` и ставим перед ним `**`. + +Таким образом мы получаем Pydantic-модель на основе данных из другой Pydantic-модели. + +#### Распаковка `dict` и дополнительные именованные аргументы + +И затем, если мы добавим дополнительный именованный аргумент `hashed_password=hashed_password` как здесь: + +```Python +UserInDB(**user_in.dict(), hashed_password=hashed_password) +``` + +... то мы получим что-то подобное: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], + hashed_password = hashed_password, +) +``` + +!!! warning "Предупреждение" + Цель использованных в примере вспомогательных функций - не более чем демонстрация возможных операций с данными, но, конечно, они не обеспечивают настоящую безопасность. + +## Сократите дублирование + +Сокращение дублирования кода - это одна из главных идей **FastAPI**. + +Поскольку дублирование кода повышает риск появления багов, проблем с безопасностью, проблем десинхронизации кода (когда вы обновляете код в одном месте, но не обновляете в другом), и т.д. + +А все описанные выше модели используют много общих данных и дублируют названия атрибутов и типов. + +Мы можем это улучшить. + +Мы можем определить модель `UserBase`, которая будет базовой для остальных моделей. И затем мы можем создать подклассы этой модели, которые будут наследовать её атрибуты (объявления типов, валидацию, и т.п.). + +Все операции конвертации, валидации, документации, и т.п. будут по-прежнему работать нормально. + +В этом случае мы можем определить только различия между моделями (с `password` в чистом виде, с `hashed_password` и без пароля): + +=== "Python 3.10+" + + ```Python hl_lines="7 13-14 17-18 21-22" + {!> ../../../docs_src/extra_models/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} + ``` + +## `Union` или `anyOf` + +Вы можете определить ответ как `Union` из двух типов. Это означает, что ответ должен соответствовать одному из них. + +Он будет определён в OpenAPI как `anyOf`. + +Для этого используйте стандартные аннотации типов в Python `typing.Union`: + +!!! note "Примечание" + При объявлении `Union`, сначала указывайте наиболее детальные типы, затем менее детальные. В примере ниже более детальный `PlaneItem` стоит перед `CarItem` в `Union[PlaneItem, CarItem]`. + +=== "Python 3.10+" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003.py!} + ``` + +### `Union` в Python 3.10 + +В этом примере мы передаём `Union[PlaneItem, CarItem]` в качестве значения аргумента `response_model`. + +Поскольку мы передаём его как **значение аргумента** вместо того, чтобы поместить его в **аннотацию типа**, нам придётся использовать `Union` даже в Python 3.10. + +Если оно было бы указано в аннотации типа, то мы могли бы использовать вертикальную черту как в примере: + +```Python +some_variable: PlaneItem | CarItem +``` + +Но если мы помещаем его в `response_model=PlaneItem | CarItem` мы получим ошибку, потому что Python попытается произвести **некорректную операцию** между `PlaneItem` и `CarItem` вместо того, чтобы интерпретировать это как аннотацию типа. + +## Список моделей + +Таким же образом вы можете определять ответы как списки объектов. + +Для этого используйте `typing.List` из стандартной библиотеки Python (или просто `list` в Python 3.9 и выше): + +=== "Python 3.9+" + + ```Python hl_lines="18" + {!> ../../../docs_src/extra_models/tutorial004_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} + ``` + +## Ответ с произвольным `dict` + +Вы также можете определить ответ, используя произвольный одноуровневый `dict` и определяя только типы ключей и значений без использования Pydantic-моделей. + +Это полезно, если вы заранее не знаете корректных названий полей/атрибутов (которые будут нужны при использовании Pydantic-модели). + +В этом случае вы можете использовать `typing.Dict` (или просто `dict` в Python 3.9 и выше): + +=== "Python 3.9+" + + ```Python hl_lines="6" + {!> ../../../docs_src/extra_models/tutorial005_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} + ``` + +## Резюме + +Используйте несколько Pydantic-моделей и свободно применяйте наследование для каждой из них. + +Вам не обязательно иметь единственную модель данных для каждой сущности, если эта сущность должна иметь возможность быть в разных "состояниях". Как в случае с "сущностью" пользователя, у которого есть состояния с полями `password`, `password_hash` и без пароля. diff --git a/docs/ru/docs/tutorial/first-steps.md b/docs/ru/docs/tutorial/first-steps.md new file mode 100644 index 0000000000..b46f235bc7 --- /dev/null +++ b/docs/ru/docs/tutorial/first-steps.md @@ -0,0 +1,333 @@ +# Первые шаги + +Самый простой 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 "схема" + +OpenAPI - это спецификация, которая определяет, как описывать схему API. + +Определение схемы содержит пути (paths) API, их параметры и т.п. + +#### "Схема" данных + +Термин "схема" также может относиться к формату или структуре некоторых данных, например, JSON. + +Тогда, подразумеваются атрибуты JSON, их типы данных и т.п. + +#### OpenAPI и JSON Schema + +OpenAPI описывает схему API. Эта схема содержит определения (или "схемы") данных, отправляемых и получаемых API. Для описания структуры данных в JSON используется стандарт **JSON Schema**. + +#### Рассмотрим `openapi.json` + +Если Вас интересует, как выглядит исходная схема OpenAPI, то FastAPI автоматически генерирует JSON-схему со всеми описаниями API. + +Можете посмотреть здесь: 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 + +Схема OpenAPI является основой для обеих систем интерактивной документации. + +Существуют десятки альтернативных инструментов, основанных на OpenAPI. Вы можете легко добавить любой из них к **FastAPI** приложению. + +Вы также можете использовать OpenAPI для автоматической генерации кода для клиентов, которые взаимодействуют с API. Например, для фронтенд-, мобильных или IoT-приложений. + +## Рассмотрим поэтапно + +### Шаг 1: импортируйте `FastAPI` + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI` это класс в Python, который предоставляет всю функциональность для API. + +!!! note "Технические детали" + `FastAPI` это класс, который наследуется непосредственно от `Starlette`. + + Вы можете использовать всю функциональность Starlette в `FastAPI`. + +### Шаг 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: определите *операцию пути (path operation)* + +#### Путь (path) + +"Путь" это часть URL, после первого символа `/`, следующего за именем домена. + +Для URL: + +``` +https://example.com/items/foo +``` + +...путь выглядит так: + +``` +/items/foo +``` + +!!! info "Дополнительная иформация" + Термин "path" также часто называется "endpoint" или "route". + +При создании API, "путь" является основным способом разделения "задач" и "ресурсов". + +#### Операция (operation) + +"Операция" это один из "методов" HTTP. + +Таких, как: + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +...и более экзотических: + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +По протоколу HTTP можно обращаться к каждому пути, используя один (или несколько) из этих "методов". + +--- + +При создании API принято использовать конкретные HTTP-методы для выполнения определенных действий. + +Обычно используют: + +* `POST`: создать данные. +* `GET`: прочитать. +* `PUT`: изменить (обновить). +* `DELETE`: удалить. + +В OpenAPI каждый HTTP метод называется "**операция**". + +Мы также будем придерживаться этого термина. + +#### Определите *декоратор операции пути (path operation decorator)* + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Декоратор `@app.get("/")` указывает **FastAPI**, что функция, прямо под ним, отвечает за обработку запросов, поступающих по адресу: + +* путь `/` +* использующих get операцию + +!!! info "`@decorator` Дополнительная информация" + Синтаксис `@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 def`: + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note "Технические детали" + Если не знаете в чём разница, посмотрите [Конкурентность: *"Нет времени?"*](../async.md#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/ru/docs/tutorial/header-params.md b/docs/ru/docs/tutorial/header-params.md new file mode 100644 index 0000000000..1be4ac7071 --- /dev/null +++ b/docs/ru/docs/tutorial/header-params.md @@ -0,0 +1,227 @@ +# Header-параметры + +Вы можете определить параметры заголовка таким же образом, как вы определяете параметры `Query`, `Path` и `Cookie`. + +## Импорт `Header` + +Сперва импортируйте `Header`: + +=== "Python 3.10+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="1" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +## Объявление параметров `Header` + +Затем объявите параметры заголовка, используя ту же структуру, что и с `Path`, `Query` и `Cookie`. + +Первое значение является значением по умолчанию, вы можете передать все дополнительные параметры валидации или аннотации: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +!!! note "Технические детали" + `Header` - это "родственный" класс `Path`, `Query` и `Cookie`. Он также наследуется от того же общего класса `Param`. + + Но помните, что когда вы импортируете `Query`, `Path`, `Header` и другие из `fastapi`, на самом деле это функции, которые возвращают специальные классы. + +!!! info "Дополнительная информация" + Чтобы объявить заголовки, важно использовать `Header`, иначе параметры интерпретируются как query-параметры. + +## Автоматическое преобразование + +`Header` обладает небольшой дополнительной функциональностью в дополнение к тому, что предоставляют `Path`, `Query` и `Cookie`. + +Большинство стандартных заголовков разделены символом "дефис", также известным как "минус" (`-`). + +Но переменная вроде `user-agent` недопустима в Python. + +По умолчанию `Header` преобразует символы имен параметров из символа подчеркивания (`_`) в дефис (`-`) для извлечения и документирования заголовков. + +Кроме того, HTTP-заголовки не чувствительны к регистру, поэтому вы можете объявить их в стандартном стиле Python (также известном как "snake_case"). + +Таким образом вы можете использовать `user_agent`, как обычно, в коде Python, вместо того, чтобы вводить заглавные буквы как `User_Agent` или что-то подобное. + +Если по какой-либо причине вам необходимо отключить автоматическое преобразование подчеркиваний в дефисы, установите для параметра `convert_underscores` в `Header` значение `False`: + +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11" + {!> ../../../docs_src/header_params/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12" + {!> ../../../docs_src/header_params/tutorial002_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="8" + {!> ../../../docs_src/header_params/tutorial002_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} + ``` + +!!! warning "Внимание" + Прежде чем установить для `convert_underscores` значение `False`, имейте в виду, что некоторые HTTP-прокси и серверы запрещают использование заголовков с подчеркиванием. + +## Повторяющиеся заголовки + +Есть возможность получать несколько заголовков с одним и тем же именем, но разными значениями. + +Вы можете определить эти случаи, используя список в объявлении типа. + +Вы получите все значения из повторяющегося заголовка в виде `list` Python. + +Например, чтобы объявить заголовок `X-Token`, который может появляться более одного раза, вы можете написать: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial003_py310.py!} + ``` + +=== "Python 3.9+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_py39.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} + ``` + +Если вы взаимодействуете с этой *операцией пути*, отправляя два HTTP-заголовка, таких как: + +``` +X-Token: foo +X-Token: bar +``` + +Ответ был бы таким: + +```JSON +{ + "X-Token values": [ + "bar", + "foo" + ] +} +``` + +## Резюме + +Объявляйте заголовки с помощью `Header`, используя тот же общий шаблон, как при `Query`, `Path` и `Cookie`. + +И не беспокойтесь о символах подчеркивания в ваших переменных, **FastAPI** позаботится об их преобразовании. diff --git a/docs/ru/docs/tutorial/index.md b/docs/ru/docs/tutorial/index.md new file mode 100644 index 0000000000..ea3a1c37ae --- /dev/null +++ b/docs/ru/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Учебник - Руководство пользователя + +В этом руководстве шаг за шагом показано, как использовать **FastApi** с большинством его функций. + +Каждый раздел постепенно основывается на предыдущих, но он структурирован по отдельным темам, так что вы можете перейти непосредственно к конкретной теме для решения ваших конкретных потребностей в API. + +Он также создан для использования в качестве будущего справочника. + +Так что вы можете вернуться и посмотреть именно то, что вам нужно. + +## Запустите код + +Все блоки кода можно копировать и использовать напрямую (на самом деле это проверенные файлы Python). + +Чтобы запустить любой из примеров, скопируйте код в файл `main.py` и запустите `uvicorn` с параметрами: + +
+ +```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. +``` + +
+ +**НАСТОЯТЕЛЬНО рекомендуется**, чтобы вы написали или скопировали код, отредактировали его и запустили локально. + +Использование кода в вашем редакторе — это то, что действительно показывает вам преимущества FastAPI, видя, как мало кода вам нужно написать, все проверки типов, автодополнение и т.д. + +--- + +## Установка FastAPI + +Первый шаг — установить FastAPI. + +Для руководства вы, возможно, захотите установить его со всеми дополнительными зависимостями и функциями: + +
+ +```console +$ pip install "fastapi[all]" + +---> 100% +``` + +
+ +...это также включает `uvicorn`, который вы можете использовать в качестве сервера, который запускает ваш код. + +!!! note "Технические детали" + Вы также можете установить его по частям. + + Это то, что вы, вероятно, сделаете, когда захотите развернуть свое приложение в рабочей среде: + + ``` + pip install fastapi + ``` + + Также установите `uvicorn` для работы в качестве сервера: + + ``` + pip install "uvicorn[standard]" + ``` + + И то же самое для каждой из необязательных зависимостей, которые вы хотите использовать. + +## Продвинутое руководство пользователя + +Существует также **Продвинутое руководство пользователя**, которое вы сможете прочитать после руководства **Учебник - Руководство пользователя**. + +**Продвинутое руководство пользователя** основано на этом, использует те же концепции и учит вас некоторым дополнительным функциям. + +Но вы должны сначала прочитать **Учебник - Руководство пользователя** (то, что вы читаете прямо сейчас). + +Он разработан таким образом, что вы можете создать полноценное приложение, используя только **Учебник - Руководство пользователя**, а затем расширить его различными способами, в зависимости от ваших потребностей, используя некоторые дополнительные идеи из **Продвинутого руководства пользователя**. diff --git a/docs/ru/docs/tutorial/metadata.md b/docs/ru/docs/tutorial/metadata.md new file mode 100644 index 0000000000..331c96734b --- /dev/null +++ b/docs/ru/docs/tutorial/metadata.md @@ -0,0 +1,111 @@ +# URL-адреса метаданных и документации + +Вы можете настроить несколько конфигураций метаданных в вашем **FastAPI** приложении. + +## Метаданные для API + +Вы можете задать следующие поля, которые используются в спецификации OpenAPI и в UI автоматической документации API: + +| Параметр | Тип | Описание | +|------------|--|-------------| +| `title` | `str` | Заголовок API. | +| `description` | `str` | Краткое описание API. Может быть использован Markdown. | +| `version` | `string` | Версия API. Версия вашего собственного приложения, а не OpenAPI. К примеру `2.5.0`. | +| `terms_of_service` | `str` | Ссылка к условиям пользования API. Если указано, то это должен быть URL-адрес. | +| `contact` | `dict` | Контактная информация для открытого API. Может содержать несколько полей.
поля contact
ПараметрТипОписание
namestrИдентификационное имя контактного лица/организации.
urlstrURL указывающий на контактную информацию. ДОЛЖЕН быть в формате URL.
emailstrEmail адрес контактного лица/организации. ДОЛЖЕН быть в формате email адреса.
| +| `license_info` | `dict` | Информация о лицензии открытого API. Может содержать несколько полей.
поля license_info
ПараметрТипОписание
namestrОБЯЗАТЕЛЬНО (если установлен параметр license_info). Название лицензии, используемой для API
urlstrURL, указывающий на лицензию, используемую для API. ДОЛЖЕН быть в формате URL.
| + +Вы можете задать их следующим образом: + +```Python hl_lines="3-16 19-31" +{!../../../docs_src/metadata/tutorial001.py!} +``` + +!!! tip "Подсказка" + Вы можете использовать Markdown в поле `description`, и оно будет отображено в выводе. + +С этой конфигурацией автоматическая документация API будут выглядеть так: + + + +## Метаданные для тегов + +Вы также можете добавить дополнительные метаданные для различных тегов, используемых для группировки ваших операций пути с помощью параметра `openapi_tags`. + +Он принимает список, содержащий один словарь для каждого тега. + +Каждый словарь может содержать в себе: + +* `name` (**обязательно**): `str`-значение с тем же именем тега, которое вы используете в параметре `tags` в ваших *операциях пути* и `APIRouter`ах. +* `description`: `str`-значение с кратким описанием для тега. Может содержать Markdown и будет отображаться в UI документации. +* `externalDocs`: `dict`-значение описывающее внешнюю документацию. Включает в себя: + * `description`: `str`-значение с кратким описанием для внешней документации. + * `url` (**обязательно**): `str`-значение с URL-адресом для внешней документации. + +### Создание метаданных для тегов + +Давайте попробуем сделать это на примере с тегами для `users` и `items`. + +Создайте метаданные для ваших тегов и передайте их в параметре `openapi_tags`: + +```Python hl_lines="3-16 18" +{!../../../docs_src/metadata/tutorial004.py!} +``` + +Помните, что вы можете использовать Markdown внутри описания, к примеру "login" будет отображен жирным шрифтом (**login**) и "fancy" будет отображаться курсивом (_fancy_). + +!!! tip "Подсказка" + Вам необязательно добавлять метаданные для всех используемых тегов + +### Используйте собственные теги +Используйте параметр `tags` с вашими *операциями пути* (и `APIRouter`ами), чтобы присвоить им различные теги: + +```Python hl_lines="21 26" +{!../../../docs_src/metadata/tutorial004.py!} +``` + +!!! info "Дополнительная информация" + Узнайте больше о тегах в [Конфигурации операции пути](../path-operation-configuration/#tags){.internal-link target=_blank}. + +### Проверьте документацию + +Теперь, если вы проверите документацию, вы увидите всю дополнительную информацию: + + + +### Порядок расположения тегов + +Порядок расположения словарей метаданных для каждого тега определяет также порядок, отображаемый в документах UI + +К примеру, несмотря на то, что `users` будут идти после `items` в алфавитном порядке, они отображаются раньше, потому что мы добавляем свои метаданные в качестве первого словаря в списке. + +## URL-адреса OpenAPI + +По умолчанию схема OpenAPI отображена по адресу `/openapi.json`. + +Но вы можете изменить это с помощью параметра `openapi_url`. + +К примеру, чтобы задать её отображение по адресу `/api/v1/openapi.json`: + +```Python hl_lines="3" +{!../../../docs_src/metadata/tutorial002.py!} +``` + +Если вы хотите отключить схему OpenAPI полностью, вы можете задать `openapi_url=None`, это также отключит пользовательские интерфейсы документации, которые его использует. + +## URL-адреса документации + +Вы можете изменить конфигурацию двух пользовательских интерфейсов документации, среди которых + +* **Swagger UI**: отображаемый по адресу `/docs`. + * Вы можете задать его URL с помощью параметра `docs_url`. + * Вы можете отключить это с помощью настройки `docs_url=None`. +* **ReDoc**: отображаемый по адресу `/redoc`. + * Вы можете задать его URL с помощью параметра `redoc_url`. + * Вы можете отключить это с помощью настройки `redoc_url=None`. + +К примеру, чтобы задать отображение Swagger UI по адресу `/documentation` и отключить ReDoc: + +```Python hl_lines="3" +{!../../../docs_src/metadata/tutorial003.py!} +``` diff --git a/docs/ru/docs/tutorial/path-operation-configuration.md b/docs/ru/docs/tutorial/path-operation-configuration.md new file mode 100644 index 0000000000..db99409f46 --- /dev/null +++ b/docs/ru/docs/tutorial/path-operation-configuration.md @@ -0,0 +1,179 @@ +# Конфигурация операций пути + +Существует несколько параметров, которые вы можете передать вашему *декоратору операций пути* для его настройки. + +!!! warning "Внимание" + Помните, что эти параметры передаются непосредственно *декоратору операций пути*, а не вашей *функции-обработчику операций пути*. + +## Коды состояния + +Вы можете определить (HTTP) `status_code`, который будет использован в ответах вашей *операции пути*. + +Вы можете передать только `int`-значение кода, например `404`. + +Но если вы не помните, для чего нужен каждый числовой код, вы можете использовать сокращенные константы в параметре `status`: + +=== "Python 3.10+" + + ```Python hl_lines="1 15" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} + ``` + +Этот код состояния будет использован в ответе и будет добавлен в схему OpenAPI. + +!!! note "Технические детали" + Вы также можете использовать `from starlette import status`. + + **FastAPI** предоставляет тот же `starlette.status` под псевдонимом `fastapi.status` для удобства разработчика. Но его источник - это непосредственно Starlette. + +## Теги + +Вы можете добавлять теги к вашим *операциям пути*, добавив параметр `tags` с `list` заполненным `str`-значениями (обычно в нём только одна строка): + +=== "Python 3.10+" + + ```Python hl_lines="15 20 25" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} + ``` + +Они будут добавлены в схему OpenAPI и будут использованы в автоматической документации интерфейса: + + + +### Теги с перечислениями + +Если у вас большое приложение, вы можете прийти к необходимости добавить **несколько тегов**, и возможно, вы захотите убедиться в том, что всегда используете **один и тот же тег** для связанных *операций пути*. + +В этих случаях, имеет смысл хранить теги в классе `Enum`. + +**FastAPI** поддерживает это так же, как и в случае с обычными строками: + +```Python hl_lines="1 8-10 13 18" +{!../../../docs_src/path_operation_configuration/tutorial002b.py!} +``` + +## Краткое и развёрнутое содержание + +Вы можете добавить параметры `summary` и `description`: + +=== "Python 3.10+" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} + ``` + +## Описание из строк документации + +Так как описания обычно длинные и содержат много строк, вы можете объявить описание *операции пути* в функции строки документации и **FastAPI** прочитает её отсюда. + +Вы можете использовать Markdown в строке документации, и он будет интерпретирован и отображён корректно (с учетом отступа в строке документации). + +=== "Python 3.10+" + + ```Python hl_lines="17-25" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} + ``` + +Он будет использован в интерактивной документации: + + + +## Описание ответа + +Вы можете указать описание ответа с помощью параметра `response_description`: + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} + ``` + +!!! info "Дополнительная информация" + Помните, что `response_description` относится конкретно к ответу, а `description` относится к *операции пути* в целом. + +!!! check "Технические детали" + OpenAPI указывает, что каждой *операции пути* необходимо описание ответа. + + Если вдруг вы не укажете его, то **FastAPI** автоматически сгенерирует это описание с текстом "Successful response". + + + +## Обозначение *операции пути* как устаревшей + +Если вам необходимо пометить *операцию пути* как устаревшую, при этом не удаляя её, передайте параметр `deprecated`: + +```Python hl_lines="16" +{!../../../docs_src/path_operation_configuration/tutorial006.py!} +``` + +Он будет четко помечен как устаревший в интерактивной документации: + + + +Проверьте, как будут выглядеть устаревшие и не устаревшие *операции пути*: + + + +## Резюме + +Вы можете легко конфигурировать и добавлять метаданные в ваши *операции пути*, передавая параметры *декораторам операций пути*. diff --git a/docs/ru/docs/tutorial/path-params-numeric-validations.md b/docs/ru/docs/tutorial/path-params-numeric-validations.md new file mode 100644 index 0000000000..bd2c29d0a0 --- /dev/null +++ b/docs/ru/docs/tutorial/path-params-numeric-validations.md @@ -0,0 +1,292 @@ +# Path-параметры и валидация числовых данных + +Так же, как с помощью `Query` вы можете добавлять валидацию и метаданные для query-параметров, так и с помощью `Path` вы можете добавлять такую же валидацию и метаданные для path-параметров. + +## Импорт Path + +Сначала импортируйте `Path` из `fastapi`, а также импортируйте `Annotated`: + +=== "Python 3.10+" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3-4" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="1" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +!!! info "Информация" + Поддержка `Annotated` была добавлена в FastAPI начиная с версии 0.95.0 (и с этой версии рекомендуется использовать этот подход). + + Если вы используете более старую версию, вы столкнётесь с ошибками при попытке использовать `Annotated`. + + Убедитесь, что вы [обновили версию FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} как минимум до 0.95.1 перед тем, как использовать `Annotated`. + +## Определите метаданные + +Вы можете указать все те же параметры, что и для `Query`. + +Например, чтобы указать значение метаданных `title` для path-параметра `item_id`, вы можете написать: + +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +!!! note "Примечание" + Path-параметр всегда является обязательным, поскольку он составляет часть пути. + + Поэтому следует объявить его с помощью `...`, чтобы обозначить, что этот параметр обязательный. + + Тем не менее, даже если вы объявите его как `None` или установите для него значение по умолчанию, это ни на что не повлияет и параметр останется обязательным. + +## Задайте нужный вам порядок параметров + +!!! tip "Подсказка" + Это не имеет большого значения, если вы используете `Annotated`. + +Допустим, вы хотите объявить query-параметр `q` как обязательный параметр типа `str`. + +И если вам больше ничего не нужно указывать для этого параметра, то нет необходимости использовать `Query`. + +Но вам по-прежнему нужно использовать `Path` для path-параметра `item_id`. И если по какой-либо причине вы не хотите использовать `Annotated`, то могут возникнуть небольшие сложности. + +Если вы поместите параметр со значением по умолчанию перед другим параметром, у которого нет значения по умолчанию, то Python укажет на ошибку. + +Но вы можете изменить порядок параметров, чтобы параметр без значения по умолчанию (query-параметр `q`) шёл первым. + +Это не имеет значения для **FastAPI**. Он распознает параметры по их названиям, типам и значениям по умолчанию (`Query`, `Path`, и т.д.), ему не важен их порядок. + +Поэтому вы можете определить функцию так: + +=== "Python 3.8 без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002.py!} + ``` + +Но имейте в виду, что если вы используете `Annotated`, вы не столкнётесь с этой проблемой, так как вы не используете `Query()` или `Path()` в качестве значения по умолчанию для параметра функции. + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an.py!} + ``` + +## Задайте нужный вам порядок параметров, полезные приёмы + +!!! tip "Подсказка" + Это не имеет большого значения, если вы используете `Annotated`. + +Здесь описан **небольшой приём**, который может оказаться удобным, хотя часто он вам не понадобится. + +Если вы хотите: + +* объявить query-параметр `q` без `Query` и без значения по умолчанию +* объявить path-параметр `item_id` с помощью `Path` +* указать их в другом порядке +* не использовать `Annotated` + +...то вы можете использовать специальную возможность синтаксиса Python. + +Передайте `*` в качестве первого параметра функции. + +Python не будет ничего делать с `*`, но он будет знать, что все следующие параметры являются именованными аргументами (парами ключ-значение), также известными как kwargs, даже если у них нет значений по умолчанию. + +```Python hl_lines="7" +{!../../../docs_src/path_params_numeric_validations/tutorial003.py!} +``` + +### Лучше с `Annotated` + +Имейте в виду, что если вы используете `Annotated`, то, поскольку вы не используете значений по умолчанию для параметров функции, то у вас не возникнет подобной проблемы и вам не придётся использовать `*`. + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an.py!} + ``` + +## Валидация числовых данных: больше или равно + +С помощью `Query` и `Path` (и других классов, которые мы разберём позже) вы можете добавлять ограничения для числовых данных. + +В этом примере при указании `ge=1`, параметр `item_id` должен быть больше или равен `1` ("`g`reater than or `e`qual"). + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004.py!} + ``` + +## Валидация числовых данных: больше и меньше или равно + +То же самое применимо к: + +* `gt`: больше (`g`reater `t`han) +* `le`: меньше или равно (`l`ess than or `e`qual) + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005.py!} + ``` + +## Валидация числовых данных: числа с плавающей точкой, больше и меньше + +Валидация также применима к значениям типа `float`. + +В этом случае становится важной возможность добавить ограничение gt, вместо ge, поскольку в таком случае вы можете, например, создать ограничение, чтобы значение было больше `0`, даже если оно меньше `1`. + +Таким образом, `0.5` будет корректным значением. А `0.0` или `0` — нет. + +То же самое справедливо и для lt. + +=== "Python 3.9+" + + ```Python hl_lines="13" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006.py!} + ``` + +## Резюме + +С помощью `Query`, `Path` (и других классов, которые мы пока не затронули) вы можете добавлять метаданные и строковую валидацию тем же способом, как и в главе [Query-параметры и валидация строк](query-params-str-validations.md){.internal-link target=_blank}. + +А также вы можете добавить валидацию числовых данных: + +* `gt`: больше (`g`reater `t`han) +* `ge`: больше или равно (`g`reater than or `e`qual) +* `lt`: меньше (`l`ess `t`han) +* `le`: меньше или равно (`l`ess than or `e`qual) + +!!! info "Информация" + `Query`, `Path` и другие классы, которые мы разберём позже, являются наследниками общего класса `Param`. + + Все они используют те же параметры для дополнительной валидации и метаданных, которые вы видели ранее. + +!!! note "Технические детали" + `Query`, `Path` и другие "классы", которые вы импортируете из `fastapi`, на самом деле являются функциями, которые при вызове возвращают экземпляры одноимённых классов. + + Объект `Query`, который вы импортируете, является функцией. И при вызове она возвращает экземпляр одноимённого класса `Query`. + + Использование функций (вместо использования классов напрямую) нужно для того, чтобы ваш редактор не подсвечивал ошибки, связанные с их типами. + + Таким образом вы можете использовать привычный вам редактор и инструменты разработки, не добавляя дополнительных конфигураций для игнорирования подобных ошибок. diff --git a/docs/ru/docs/tutorial/path-params.md b/docs/ru/docs/tutorial/path-params.md new file mode 100644 index 0000000000..55b498ef0a --- /dev/null +++ b/docs/ru/docs/tutorial/path-params.md @@ -0,0 +1,251 @@ +# Path-параметры + +Вы можете определить "параметры" или "переменные" пути, используя синтаксис форматированных строк 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`. + +Та же ошибка возникнет, если вместо `int` передать `float` , например: http://127.0.0.1:8000/items/4.2 + +!!! check "Заметка" + **FastAPI** обеспечивает проверку типов, используя всё те же определения типов. + + Обратите внимание, что в тексте ошибки явно указано место не прошедшее проверку. + + Это очень полезно при разработке и отладке кода, который взаимодействует с API. + +## Документация + +И теперь, когда откроете браузер по адресу: http://127.0.0.1:8000/docs, то увидите вот такую автоматически сгенерированную документацию API: + + + +!!! check "Заметка" + Ещё раз, просто используя определения типов, **FastAPI** обеспечивает автоматическую интерактивную документацию (с интеграцией Swagger UI). + + Обратите внимание, что параметр пути объявлен целочисленным. + +## Преимущества стандартизации, альтернативная документация + +Поскольку сгенерированная схема соответствует стандарту OpenAPI, её можно использовать со множеством совместимых инструментов. + +Именно поэтому, FastAPI сам предоставляет альтернативную документацию API (используя ReDoc), которую можно получить по адресу: http://127.0.0.1:8000/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`, "подразумевая", что он получает параметр `user_id` со значением `"me"`. + +Аналогично, вы не можете переопределить операцию с путем: + +```Python hl_lines="6 11" +{!../../../docs_src/path_params/tutorial003b.py!} +``` + +Первый будет выполняться всегда, так как путь совпадает первым. + +## Предопределенные значения + +Что если нам нужно заранее определить допустимые *параметры пути*, которые *операция пути* может принимать? В таком случае можно использовать стандартное перечисление `Enum` Python. + +### Создание класса `Enum` + +Импортируйте `Enum` и создайте подкласс, который наследуется от `str` и `Enum`. + +Мы наследуемся от `str`, чтобы документация API могла понять, что значения должны быть типа `string` и отображалась правильно. + +Затем создайте атрибуты класса с фиксированными допустимыми значениями: + +```Python hl_lines="1 6-9" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +!!! info "Дополнительная информация" + Перечисления (enum) доступны в Python начиная с версии 3.4. + +!!! 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!} +``` + +#### Получение *значения перечисления* + +Можно получить фактическое значение (в данном случае - `str`) с помощью `model_name.value` или в общем случае `your_enum_member.value`: + +```Python hl_lines="20" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +!!! tip "Подсказка" + Значение `"lenet"` также можно получить с помощью `ModelName.lenet.value`. + +#### Возврат *элементов перечисления* + +Из *операции пути* можно вернуть *элементы перечисления*, даже вложенные в тело JSON (например в `dict`). + +Они будут преобразованы в соответствующие значения (в данном случае - строки) перед их возвратом клиенту: + +```Python hl_lines="18 21 23" +{!../../../docs_src/path_params/tutorial005.py!} +``` +Вы отправите клиенту такой JSON-ответ: + +```JSON +{ + "model_name": "alexnet", + "message": "Deep Learning FTW!" +} +``` + +## Path-параметры, содержащие пути + +Предположим, что есть *операция пути* с путем `/files/{file_path}`. + +Но вам нужно, чтобы `file_path` сам содержал *путь*, например, `home/johndoe/myfile.txt`. + +Тогда URL для этого файла будет такой: `/files/home/johndoe/myfile.txt`. + +### Поддержка OpenAPI + +OpenAPI не поддерживает способов объявления *параметра пути*, содержащего внутри *путь*, так как это может привести к сценариям, которые сложно определять и тестировать. + +Тем не менее это можно сделать в **FastAPI**, используя один из внутренних инструментов Starlette. + +Документация по-прежнему будет работать, хотя и не добавит никакой информации о том, что параметр должен содержать путь. + +### Конвертер пути + +Благодаря одной из опций Starlette, можете объявить *параметр пути*, содержащий *путь*, используя URL вроде: + +``` +/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/ru/docs/tutorial/query-params-str-validations.md b/docs/ru/docs/tutorial/query-params-str-validations.md new file mode 100644 index 0000000000..cc826b8711 --- /dev/null +++ b/docs/ru/docs/tutorial/query-params-str-validations.md @@ -0,0 +1,919 @@ +# Query-параметры и валидация строк + +**FastAPI** позволяет определять дополнительную информацию и валидацию для ваших параметров. + +Давайте рассмотрим следующий пример: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} + ``` + +Query-параметр `q` имеет тип `Union[str, None]` (или `str | None` в Python 3.10). Это означает, что входной параметр будет типа `str`, но может быть и `None`. Ещё параметр имеет значение по умолчанию `None`, из-за чего FastAPI определит параметр как необязательный. + +!!! note "Технические детали" + FastAPI определит параметр `q` как необязательный, потому что его значение по умолчанию `= None`. + + `Union` в `Union[str, None]` позволит редактору кода оказать вам лучшую поддержку и найти ошибки. + +## Расширенная валидация + +Добавим дополнительное условие валидации параметра `q` - **длина строки не более 50 символов** (условие проверяется всякий раз, когда параметр `q` не является `None`). + +### Импорт `Query` и `Annotated` + +Чтобы достичь этого, первым делом нам нужно импортировать: + +* `Query` из пакета `fastapi`: +* `Annotated` из пакета `typing` (или из `typing_extensions` для Python ниже 3.9) + +=== "Python 3.10+" + + В Python 3.9 или выше, `Annotated` является частью стандартной библиотеки, таким образом вы можете импортировать его из `typing`. + + ```Python hl_lines="1 3" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} + ``` + +=== "Python 3.8+" + + В версиях Python ниже Python 3.9 `Annotation` импортируется из `typing_extensions`. + + Эта библиотека будет установлена вместе с FastAPI. + + ```Python hl_lines="3-4" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} + ``` + +## `Annotated` как тип для query-параметра `q` + +Помните, как ранее я говорил об Annotated? Он может быть использован для добавления метаданных для ваших параметров в разделе [Введение в аннотации типов Python](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? + +Пришло время использовать их в FastAPI. 🚀 + +У нас была аннотация следующего типа: + +=== "Python 3.10+" + + ```Python + q: str | None = None + ``` + +=== "Python 3.8+" + + ```Python + q: Union[str, None] = None + ``` + +Вот что мы получим, если обернём это в `Annotated`: + +=== "Python 3.10+" + + ```Python + q: Annotated[str | None] = None + ``` + +=== "Python 3.8+" + + ```Python + q: Annotated[Union[str, None]] = None + ``` + +Обе эти версии означают одно и тоже. `q` - это параметр, который может быть `str` или `None`, и по умолчанию он будет принимать `None`. + +Давайте повеселимся. 🎉 + +## Добавим `Query` в `Annotated` для query-параметра `q` + +Теперь, когда у нас есть `Annotated`, где мы можем добавить больше метаданных, добавим `Query` со значением параметра `max_length` равным 50: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} + ``` + +Обратите внимание, что значение по умолчанию всё ещё `None`, так что параметр остаётся необязательным. + +Однако теперь, имея `Query(max_length=50)` внутри `Annotated`, мы говорим FastAPI, что мы хотим извлечь это значение из параметров query-запроса (что произойдёт в любом случае 🤷), и что мы хотим иметь **дополнительные условия валидации** для этого значения (для чего мы и делаем это - чтобы получить дополнительную валидацию). 😎 + +Теперь FastAPI: + +* **Валидирует** (проверяет), что полученные данные состоят максимум из 50 символов +* Показывает **исчерпывающую ошибку** (будет описание местонахождения ошибки и её причины) для клиента в случаях, когда данные не валидны +* **Задокументирует** параметр в схему OpenAPI *операции пути* (что будет отображено в **UI автоматической документации**) + +## Альтернативный (устаревший) способ задать `Query` как значение по умолчанию + +В предыдущих версиях FastAPI (ниже 0.95.0) необходимо было использовать `Query` как значение по умолчанию для query-параметра. Так было вместо размещения его в `Annotated`, так что велика вероятность, что вам встретится такой код. Сейчас объясню. + +!!! tip "Подсказка" + При написании нового кода и везде где это возможно, используйте `Annotated`, как было описано ранее. У этого способа есть несколько преимуществ (о них дальше) и никаких недостатков. 🍰 + +Вот как вы могли бы использовать `Query()` в качестве значения по умолчанию параметра вашей функции, установив для параметра `max_length` значение 50: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + ``` + +В таком случае (без использования `Annotated`), мы заменили значение по умолчанию с `None` на `Query()` в функции. Теперь нам нужно установить значение по умолчанию для query-параметра `Query(default=None)`, что необходимо для тех же целей, как когда ранее просто указывалось значение по умолчанию (по крайней мере, для FastAPI). + +Таким образом: + +```Python +q: Union[str, None] = Query(default=None) +``` + +...делает параметр необязательным со значением по умолчанию `None`, также как это делает: + +```Python +q: Union[str, None] = None +``` + +И для Python 3.10 и выше: + +```Python +q: str | None = Query(default=None) +``` + +...делает параметр необязательным со значением по умолчанию `None`, также как это делает: + +```Python +q: str | None = None +``` + +Но он явно объявляет его как query-параметр. + +!!! info "Дополнительная информация" + Запомните, важной частью объявления параметра как необязательного является: + + ```Python + = None + ``` + + или: + + ```Python + = Query(default=None) + ``` + + так как `None` указан в качестве значения по умолчанию, параметр будет **необязательным**. + + `Union[str, None]` позволит редактору кода оказать вам лучшую поддержку. Но это не то, на что обращает внимание FastAPI для определения необязательности параметра. + +Теперь, мы можем указать больше параметров для `Query`. В данном случае, параметр `max_length` применяется к строкам: + +```Python +q: Union[str, None] = Query(default=None, max_length=50) +``` + +Входные данные будут проверены. Если данные недействительны, тогда будет указано на ошибку в запросе (будет описание местонахождения ошибки и её причины). Кроме того, параметр задокументируется в схеме OpenAPI данной *операции пути*. + +### Использовать `Query` как значение по умолчанию или добавить в `Annotated` + +Когда `Query` используется внутри `Annotated`, вы не можете использовать параметр `default` у `Query`. + +Вместо этого, используйте обычное указание значения по умолчанию для параметра функции. Иначе, это будет несовместимо. + +Следующий пример не рабочий: + +```Python +q: Annotated[str, Query(default="rick")] = "morty" +``` + +...потому что нельзя однозначно определить, что именно должно быть значением по умолчанию: `"rick"` или `"morty"`. + +Вам следует использовать (предпочтительно): + +```Python +q: Annotated[str, Query()] = "rick" +``` + +...или как в старом коде, который вам может попасться: + +```Python +q: str = Query(default="rick") +``` + +### Преимущества `Annotated` + +**Рекомендуется использовать `Annotated`** вместо значения по умолчанию в параметрах функции, потому что так **лучше** по нескольким причинам. 🤓 + +Значение **по умолчанию** у **параметров функции** - это **действительно значение по умолчанию**, что более интуитивно понятно для пользователей Python. 😌 + +Вы можете **вызвать** ту же функцию в **иных местах** без FastAPI, и она **сработает как ожидается**. Если это **обязательный** параметр (без значения по умолчанию), ваш **редактор кода** сообщит об ошибке. **Python** также укажет на ошибку, если вы вызовете функцию без передачи ей обязательного параметра. + +Если вы вместо `Annotated` используете **(устаревший) стиль значений по умолчанию**, тогда при вызове этой функции без FastAPI в **другом месте** вам необходимо **помнить** о передаче аргументов функции, чтобы она работала корректно. В противном случае, значения будут отличаться от тех, что вы ожидаете (например, `QueryInfo` или что-то подобное вместо `str`). И ни ваш редактор кода, ни Python не будут жаловаться на работу этой функции, только когда вычисления внутри дадут сбой. + +Так как `Annotated` может принимать более одной аннотации метаданных, то теперь вы можете использовать ту же функцию с другими инструментами, например Typer. 🚀 + +## Больше валидации + +Вы также можете добавить параметр `min_length`: + +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} + ``` + +## Регулярные выражения + +Вы можете определить регулярное выражение, которому должен соответствовать параметр: + +=== "Python 3.10+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} + ``` + +Данное регулярное выражение проверяет, что полученное значение параметра: + +* `^`: начало строки. +* `fixedquery`: в точности содержит строку `fixedquery`. +* `$`: конец строки, не имеет символов после `fixedquery`. + +Не переживайте, если **"регулярное выражение"** вызывает у вас трудности. Это достаточно сложная тема для многих людей. Вы можете сделать множество вещей без использования регулярных выражений. + +Но когда они вам понадобятся, и вы закончите их освоение, то не будет проблемой использовать их в **FastAPI**. + +## Значения по умолчанию + +Вы точно также можете указать любое значение `по умолчанию`, как ранее указывали `None`. + +Например, вы хотите для параметра запроса `q` указать, что он должен состоять минимум из 3 символов (`min_length=3`) и иметь значение по умолчанию `"fixedquery"`: + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial005.py!} + ``` + +!!! note "Технические детали" + Наличие значения по умолчанию делает параметр необязательным. + +## Обязательный параметр + +Когда вам не требуется дополнительная валидация или дополнительные метаданные для параметра запроса, вы можете сделать параметр `q` обязательным просто не указывая значения по умолчанию. Например: + +```Python +q: str +``` + +вместо: + +```Python +q: Union[str, None] = None +``` + +Но у нас query-параметр определён как `Query`. Например: + +=== "Annotated" + + ```Python + q: Annotated[Union[str, None], Query(min_length=3)] = None + ``` + +=== "без Annotated" + + ```Python + q: Union[str, None] = Query(default=None, min_length=3) + ``` + +В таком случае, чтобы сделать query-параметр `Query` обязательным, вы можете просто не указывать значение по умолчанию: + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006.py!} + ``` + + !!! tip "Подсказка" + Обратите внимание, что даже когда `Query()` используется как значение по умолчанию для параметра функции, мы не передаём `default=None` в `Query()`. + + Лучше будет использовать версию с `Annotated`. 😉 + +### Обязательный параметр с Ellipsis (`...`) + +Альтернативный способ указать обязательность параметра запроса - это указать параметр `default` через многоточие `...`: + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006b_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006b_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006b.py!} + ``` + +!!! info "Дополнительная информация" + Если вы ранее не сталкивались с `...`: это специальное значение, часть языка Python и называется "Ellipsis". + + Используется в Pydantic и FastAPI для определения, что значение требуется обязательно. + +Таким образом, **FastAPI** определяет, что параметр является обязательным. + +### Обязательный параметр с `None` + +Вы можете определить, что параметр может принимать `None`, но всё ещё является обязательным. Это может потребоваться для того, чтобы пользователи явно указали параметр, даже если его значение будет `None`. + +Чтобы этого добиться, вам нужно определить `None` как валидный тип для параметра запроса, но также указать `default=...`: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} + ``` + +!!! tip "Подсказка" + Pydantic, мощь которого используется в FastAPI для валидации и сериализации, имеет специальное поведение для `Optional` или `Union[Something, None]` без значения по умолчанию. Вы можете узнать об этом больше в документации Pydantic, раздел Обязательные Опциональные поля. + +### Использование Pydantic's `Required` вместо Ellipsis (`...`) + +Если вас смущает `...`, вы можете использовать `Required` из Pydantic: + +=== "Python 3.9+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/query_params_str_validations/tutorial006d_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="2 9" + {!> ../../../docs_src/query_params_str_validations/tutorial006d_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="2 8" + {!> ../../../docs_src/query_params_str_validations/tutorial006d.py!} + ``` + +!!! tip "Подсказка" + Запомните, когда вам необходимо объявить query-параметр обязательным, вы можете просто не указывать параметр `default`. Таким образом, вам редко придётся использовать `...` или `Required`. + +## Множество значений для query-параметра + +Для query-параметра `Query` можно указать, что он принимает список значений (множество значений). + +Например, query-параметр `q` может быть указан в URL несколько раз. И если вы ожидаете такой формат запроса, то можете указать это следующим образом: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} + ``` + +=== "Python 3.9+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} + ``` + +Затем, получив такой URL: + +``` +http://localhost:8000/items/?q=foo&q=bar +``` + +вы бы получили несколько значений (`foo` и `bar`), которые относятся к параметру `q`, в виде Python `list` внутри вашей *функции обработки пути*, в *параметре функции* `q`. + +Таким образом, ответ на этот URL будет: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +!!! tip "Подсказка" + Чтобы объявить query-параметр типом `list`, как в примере выше, вам нужно явно использовать `Query`, иначе он будет интерпретирован как тело запроса. + +Интерактивная документация API будет обновлена соответствующим образом, где будет разрешено множество значений: + + + +### Query-параметр со множеством значений по умолчанию + +Вы также можете указать тип `list` со списком значений по умолчанию на случай, если вам их не предоставят: + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial012_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial012_an.py!} + ``` + +=== "Python 3.9+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```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]` (или `list[str]` в Python 3.9+): + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial013.py!} + ``` + +!!! note "Технические детали" + Запомните, что в таком случае, FastAPI не будет проверять содержимое списка. + + Например, для List[int] список будет провалидирован (и задокументирован) на содержание только целочисленных элементов. Но для простого `list` такой проверки не будет. + +## Больше метаданных + +Вы можете добавить больше информации об query-параметре. + +Указанная информация будет включена в генерируемую OpenAPI документацию и использована в пользовательском интерфейсе и внешних инструментах. + +!!! note "Технические детали" + Имейте в виду, что разные инструменты могут иметь разные уровни поддержки OpenAPI. + + Некоторые из них могут не отображать (на данный момент) всю заявленную дополнительную информацию, хотя в большинстве случаев отсутствующая функция уже запланирована к разработке. + +Вы можете указать название query-параметра, используя параметр `title`: + +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} + ``` + +Добавить описание, используя параметр `description`: + +=== "Python 3.10+" + + ```Python hl_lines="14" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="15" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="13" + {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} + ``` + +## Псевдонимы параметров + +Представьте, что вы хотите использовать query-параметр с названием `item-query`. + +Например: + +``` +http://127.0.0.1:8000/items/?item-query=foobaritems +``` + +Но `item-query` является невалидным именем переменной в Python. + +Наиболее похожее валидное имя `item_query`. + +Но вам всё равно необходим `item-query`... + +Тогда вы можете объявить `псевдоним`, и этот псевдоним будет использоваться для поиска значения параметра запроса: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} + ``` + +## Устаревшие параметры + +Предположим, вы больше не хотите использовать какой-либо параметр. + +Вы решили оставить его, потому что клиенты всё ещё им пользуются. Но вы хотите отобразить это в документации как устаревший функционал. + +Тогда для `Query` укажите параметр `deprecated=True`: + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="16" + {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="18" + {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} + ``` + +В документации это будет отображено следующим образом: + + + +## Исключить из OpenAPI + +Чтобы исключить query-параметр из генерируемой OpenAPI схемы (а также из системы автоматической генерации документации), укажите в `Query` параметр `include_in_schema=False`: + +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} + ``` + +## Резюме + +Вы можете объявлять дополнительные правила валидации и метаданные для ваших параметров запроса. + +Общие метаданные: + +* `alias` +* `title` +* `description` +* `deprecated` +* `include_in_schema` + +Специфичные правила валидации для строк: + +* `min_length` +* `max_length` +* `regex` + +В рассмотренных примерах показано объявление правил валидации для строковых значений `str`. + +В следующих главах вы увидете, как объявлять правила валидации для других типов (например, чисел). diff --git a/docs/ru/docs/tutorial/query-params.md b/docs/ru/docs/tutorial/query-params.md new file mode 100644 index 0000000000..6e885cb656 --- /dev/null +++ b/docs/ru/docs/tutorial/query-params.md @@ -0,0 +1,225 @@ +# Query-параметры + +Когда вы объявляете параметры функции, которые не являются параметрами пути, они автоматически интерпретируются как "query"-параметры. + +```Python hl_lines="9" +{!../../../docs_src/query_params/tutorial001.py!} +``` + +Query-параметры представляют из себя набор пар ключ-значение, которые идут после знака `?` в URL-адресе, разделенные символами `&`. + +Например, в этом URL-адресе: + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +...параметры запроса такие: + +* `skip`: со значением `0` +* `limit`: со значением `10` + +Будучи частью URL-адреса, они "по умолчанию" являются строками. + +Но когда вы объявляете их с использованием аннотаций (в примере выше, как `int`), они конвертируются в указанный тип данных и проходят проверку на соответствие ему. + +Все те же правила, которые применяются к path-параметрам, также применяются и query-параметрам: + +* Поддержка от редактора кода (очевидно) +* "Парсинг" данных +* Проверка на соответствие данных (Валидация) +* Автоматическая документация + +## Значения по умолчанию + +Поскольку query-параметры не являются фиксированной частью пути, они могут быть не обязательными и иметь значения по умолчанию. + +В примере выше значения по умолчанию равны `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`: т.к это было значение по умолчанию + +## Необязательные параметры + +Аналогично, вы можете объявлять необязательные query-параметры, установив их значение по умолчанию, равное `None`: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial002.py!} + ``` + +В этом случае, параметр `q` будет не обязательным и будет иметь значение `None` по умолчанию. + +!!! Важно + Также обратите внимание, что **FastAPI** достаточно умён чтобы заметить, что параметр `item_id` является path-параметром, а `q` нет, поэтому, это параметр запроса. + +## Преобразование типа параметра запроса + +Вы также можете объявлять параметры с типом `bool`, которые будут преобразованы соответственно: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../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` типа данных `bool` . В противном случае - `False`. + + +## Смешивание query-параметров и path-параметров + +Вы можете объявлять несколько query-параметров и path-параметров одновременно,**FastAPI** сам разберётся, что чем является. + +И вы не обязаны объявлять их в каком-либо определенном порядке. + +Они будут обнаружены по именам: + +=== "Python 3.10+" + + ```Python hl_lines="6 8" + {!> ../../../docs_src/query_params/tutorial004_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} + ``` + +## Обязательные query-параметры + +Когда вы объявляете значение по умолчанию для параметра, который не является path-параметром (в этом разделе, мы пока что познакомились только с path-параметрами), то это значение не является обязательным. + +Если вы не хотите задавать конкретное значение, но хотите сделать параметр необязательным, вы можете установить значение по умолчанию равным `None`. + +Но если вы хотите сделать query-параметр обязательным, вы можете просто не указывать значение по умолчанию: + +```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 3.10+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params/tutorial006_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params/tutorial006.py!} + ``` + +В этом примере, у нас есть 3 параметра запроса: + +* `needy`, обязательный `str`. +* `skip`, типа `int` и со значением по умолчанию `0`. +* `limit`, необязательный `int`. + +!!! подсказка + Вы можете использовать класс `Enum` также, как ранее применяли его с [Path-параметрами](path-params.md#predefined-values){.internal-link target=_blank}. diff --git a/docs/ru/docs/tutorial/request-files.md b/docs/ru/docs/tutorial/request-files.md new file mode 100644 index 0000000000..00f8c83770 --- /dev/null +++ b/docs/ru/docs/tutorial/request-files.md @@ -0,0 +1,314 @@ +# Загрузка файлов + +Используя класс `File`, мы можем позволить клиентам загружать файлы. + +!!! info "Дополнительная информация" + Чтобы получать загруженные файлы, сначала установите `python-multipart`. + + Например: `pip install python-multipart`. + + Это связано с тем, что загружаемые файлы передаются как данные формы. + +## Импорт `File` + +Импортируйте `File` и `UploadFile` из модуля `fastapi`: + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` + +## Определите параметры `File` + +Создайте параметры `File` так же, как вы это делаете для `Body` или `Form`: + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` + +!!! info "Дополнительная информация" + `File` - это класс, который наследуется непосредственно от `Form`. + + Но помните, что когда вы импортируете `Query`, `Path`, `File` и другие из `fastapi`, на самом деле это функции, которые возвращают специальные классы. + +!!! tip "Подсказка" + Для объявления тела файла необходимо использовать `File`, поскольку в противном случае параметры будут интерпретироваться как параметры запроса или параметры тела (JSON). + +Файлы будут загружены как данные формы. + +Если вы объявите тип параметра у *функции операции пути* как `bytes`, то **FastAPI** прочитает файл за вас, и вы получите его содержимое в виде `bytes`. + +Следует иметь в виду, что все содержимое будет храниться в памяти. Это хорошо подходит для небольших файлов. + +Однако возможны случаи, когда использование `UploadFile` может оказаться полезным. + +## Загрузка файла с помощью `UploadFile` + +Определите параметр файла с типом `UploadFile`: + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="13" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="12" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` + +Использование `UploadFile` имеет ряд преимуществ перед `bytes`: + +* Использовать `File()` в значении параметра по умолчанию не обязательно. +* При этом используется "буферный" файл: + * Файл, хранящийся в памяти до максимального предела размера, после преодоления которого он будет храниться на диске. +* Это означает, что он будет хорошо работать с большими файлами, такими как изображения, видео, большие бинарные файлы и т.д., не потребляя при этом всю память. +* Из загруженного файла можно получить метаданные. +* Он реализует file-like `async` интерфейс. +* Он предоставляет реальный объект Python `SpooledTemporaryFile` который вы можете передать непосредственно другим библиотекам, которые ожидают файл в качестве объекта. + +### `UploadFile` + +`UploadFile` имеет следующие атрибуты: + +* `filename`: Строка `str` с исходным именем файла, который был загружен (например, `myimage.jpg`). +* `content_type`: Строка `str` с типом содержимого (MIME type / media type) (например, `image/jpeg`). +* `file`: `SpooledTemporaryFile` (a file-like объект). Это фактический файл Python, который можно передавать непосредственно другим функциям или библиотекам, ожидающим файл в качестве объекта. + +`UploadFile` имеет следующие методы `async`. Все они вызывают соответствующие файловые методы (используя внутренний SpooledTemporaryFile). + +* `write(data)`: Записать данные `data` (`str` или `bytes`) в файл. +* `read(size)`: Прочитать количество `size` (`int`) байт/символов из файла. +* `seek(offset)`: Перейти к байту на позиции `offset` (`int`) в файле. + * Наример, `await myfile.seek(0)` перейдет к началу файла. + * Это особенно удобно, если вы один раз выполнили команду `await myfile.read()`, а затем вам нужно прочитать содержимое файла еще раз. +* `close()`: Закрыть файл. + +Поскольку все эти методы являются `async` методами, вам следует использовать "await" вместе с ними. + +Например, внутри `async` *функции операции пути* можно получить содержимое с помощью: + +```Python +contents = await myfile.read() +``` + +Если вы находитесь внутри обычной `def` *функции операции пути*, можно получить прямой доступ к файлу `UploadFile.file`, например: + +```Python +contents = myfile.file.read() +``` + +!!! note "Технические детали `async`" + При использовании методов `async` **FastAPI** запускает файловые методы в пуле потоков и ожидает их. + +!!! note "Технические детали Starlette" + **FastAPI** наследует `UploadFile` непосредственно из **Starlette**, но добавляет некоторые детали для совместимости с **Pydantic** и другими частями FastAPI. + +## Про данные формы ("Form Data") + +Способ, которым HTML-формы (`
`) отправляют данные на сервер, обычно использует "специальную" кодировку для этих данных, отличную от JSON. + +**FastAPI** позаботится о том, чтобы считать эти данные из нужного места, а не из JSON. + +!!! note "Технические детали" + Данные из форм обычно кодируются с использованием "media type" `application/x-www-form-urlencoded` когда он не включает файлы. + + Но когда форма включает файлы, она кодируется как multipart/form-data. Если вы используете `File`, **FastAPI** будет знать, что ему нужно получить файлы из нужной части тела. + + Если вы хотите узнать больше об этих кодировках и полях форм, перейдите по ссылке MDN web docs for POST. + +!!! warning "Внимание" + В операции *функции операции пути* можно объявить несколько параметров `File` и `Form`, но нельзя также объявлять поля `Body`, которые предполагается получить в виде JSON, поскольку тело запроса будет закодировано с помощью `multipart/form-data`, а не `application/json`. + + Это не является ограничением **FastAPI**, это часть протокола HTTP. + +## Необязательная загрузка файлов + +Вы можете сделать загрузку файла необязательной, используя стандартные аннотации типов и установив значение по умолчанию `None`: + +=== "Python 3.10+" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10 18" + {!> ../../../docs_src/request_files/tutorial001_02_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="7 15" + {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} + ``` + +## `UploadFile` с дополнительными метаданными + +Вы также можете использовать `File()` вместе с `UploadFile`, например, для установки дополнительных метаданных: + +=== "Python 3.9+" + + ```Python hl_lines="9 15" + {!> ../../../docs_src/request_files/tutorial001_03_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8 14" + {!> ../../../docs_src/request_files/tutorial001_03_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="7 13" + {!> ../../../docs_src/request_files/tutorial001_03.py!} + ``` + +## Загрузка нескольких файлов + +Можно одновременно загружать несколько файлов. + +Они будут связаны с одним и тем же "полем формы", отправляемым с помощью данных формы. + +Для этого необходимо объявить список `bytes` или `UploadFile`: + +=== "Python 3.9+" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="11 16" + {!> ../../../docs_src/request_files/tutorial002_an.py!} + ``` + +=== "Python 3.9+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="8 13" + {!> ../../../docs_src/request_files/tutorial002_py39.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002.py!} + ``` + +Вы получите, как и было объявлено, список `list` из `bytes` или `UploadFile`. + +!!! note "Technical Details" + Можно также использовать `from starlette.responses import HTMLResponse`. + + **FastAPI** предоставляет тот же `starlette.responses`, что и `fastapi.responses`, просто для удобства разработчика. Однако большинство доступных ответов поступает непосредственно из Starlette. + +### Загрузка нескольких файлов с дополнительными метаданными + +Так же, как и раньше, вы можете использовать `File()` для задания дополнительных параметров, даже для `UploadFile`: + +=== "Python 3.9+" + + ```Python hl_lines="11 18-20" + {!> ../../../docs_src/request_files/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12 19-21" + {!> ../../../docs_src/request_files/tutorial003_an.py!} + ``` + +=== "Python 3.9+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="9 16" + {!> ../../../docs_src/request_files/tutorial003_py39.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="11 18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + +## Резюме + +Используйте `File`, `bytes` и `UploadFile` для работы с файлами, которые будут загружаться и передаваться в виде данных формы. diff --git a/docs/ru/docs/tutorial/request-forms-and-files.md b/docs/ru/docs/tutorial/request-forms-and-files.md new file mode 100644 index 0000000000..3f587c38a3 --- /dev/null +++ b/docs/ru/docs/tutorial/request-forms-and-files.md @@ -0,0 +1,69 @@ +# Файлы и формы в запросе + +Вы можете определять файлы и поля формы одновременно, используя `File` и `Form`. + +!!! info "Дополнительная информация" + Чтобы получать загруженные файлы и/или данные форм, сначала установите `python-multipart`. + + Например: `pip install python-multipart`. + +## Импортируйте `File` и `Form` + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms_and_files/tutorial001.py!} + ``` + +## Определите параметры `File` и `Form` + +Создайте параметры файла и формы таким же образом, как для `Body` или `Query`: + +=== "Python 3.9+" + + ```Python hl_lines="10-12" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="8" + {!> ../../../docs_src/request_forms_and_files/tutorial001.py!} + ``` + +Файлы и поля формы будут загружены в виде данных формы, и вы получите файлы и поля формы. + +Вы можете объявить некоторые файлы как `bytes`, а некоторые - как `UploadFile`. + +!!! warning "Внимание" + Вы можете объявить несколько параметров `File` и `Form` в операции *path*, но вы не можете также объявить поля `Body`, которые вы ожидаете получить в виде JSON, так как запрос будет иметь тело, закодированное с помощью `multipart/form-data` вместо `application/json`. + + Это не ограничение **Fast API**, это часть протокола HTTP. + +## Резюме + +Используйте `File` и `Form` вместе, когда необходимо получить данные и файлы в одном запросе. diff --git a/docs/ru/docs/tutorial/request-forms.md b/docs/ru/docs/tutorial/request-forms.md new file mode 100644 index 0000000000..0fc9e4eda4 --- /dev/null +++ b/docs/ru/docs/tutorial/request-forms.md @@ -0,0 +1,92 @@ +# Данные формы + +Когда вам нужно получить поля формы вместо JSON, вы можете использовать `Form`. + +!!! info "Дополнительная информация" + Чтобы использовать формы, сначала установите `python-multipart`. + + Например, выполните команду `pip install python-multipart`. + +## Импорт `Form` + +Импортируйте `Form` из `fastapi`: + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать 'Annotated' версию, если это возможно. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms/tutorial001.py!} + ``` + +## Определение параметров `Form` + +Создайте параметры формы так же, как это делается для `Body` или `Query`: + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать 'Annotated' версию, если это возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/request_forms/tutorial001.py!} + ``` + +Например, в одном из способов использования спецификации OAuth2 (называемом "потоком пароля") требуется отправить `username` и `password` в виде полей формы. + +Данный способ требует отправку данных для авторизации посредством формы (а не JSON) и обязательного наличия в форме строго именованных полей `username` и `password`. + +Вы можете настроить `Form` точно так же, как настраиваете и `Body` ( `Query`, `Path`, `Cookie`), включая валидации, примеры, псевдонимы (например, `user-name` вместо `username`) и т.д. + +!!! info "Дополнительная информация" + `Form` - это класс, который наследуется непосредственно от `Body`. + +!!! tip "Подсказка" + Вам необходимо явно указывать параметр `Form` при объявлении каждого поля, иначе поля будут интерпретироваться как параметры запроса или параметры тела (JSON). + +## О "полях формы" + +Обычно способ, которым HTML-формы (`
`) отправляют данные на сервер, использует "специальное" кодирование для этих данных, отличное от JSON. + +**FastAPI** гарантирует правильное чтение этих данных из соответствующего места, а не из JSON. + +!!! note "Технические детали" + Данные из форм обычно кодируются с использованием "типа медиа" `application/x-www-form-urlencoded`. + + Но когда форма содержит файлы, она кодируется как `multipart/form-data`. Вы узнаете о работе с файлами в следующей главе. + + Если вы хотите узнать больше про кодировки и поля формы, ознакомьтесь с документацией MDN для `POST` на веб-сайте. + +!!! warning "Предупреждение" + Вы можете объявлять несколько параметров `Form` в *операции пути*, но вы не можете одновременно с этим объявлять поля `Body`, которые вы ожидаете получить в виде JSON, так как запрос будет иметь тело, закодированное с использованием `application/x-www-form-urlencoded`, а не `application/json`. + + Это не ограничение **FastAPI**, это часть протокола HTTP. + +## Резюме + +Используйте `Form` для объявления входных параметров данных формы. diff --git a/docs/ru/docs/tutorial/response-model.md b/docs/ru/docs/tutorial/response-model.md new file mode 100644 index 0000000000..38b45e2a57 --- /dev/null +++ b/docs/ru/docs/tutorial/response-model.md @@ -0,0 +1,480 @@ +# Модель ответа - Возвращаемый тип + +Вы можете объявить тип ответа, указав аннотацию **возвращаемого значения** для *функции операции пути*. + +FastAPI позволяет использовать **аннотации типов** таким же способом, как и для ввода данных в **параметры** функции, вы можете использовать модели Pydantic, списки, словари, скалярные типы (такие, как int, bool и т.д.). + +=== "Python 3.10+" + + ```Python hl_lines="16 21" + {!> ../../../docs_src/response_model/tutorial001_01_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01.py!} + ``` + +FastAPI будет использовать этот возвращаемый тип для: + +* **Валидации** ответа. + * Если данные невалидны (например, отсутствует одно из полей), это означает, что код *вашего* приложения работает некорректно и функция возвращает не то, что вы ожидаете. В таком случае приложение вернет server error вместо того, чтобы отправить неправильные данные. Таким образом, вы и ваши пользователи можете быть уверены, что получите корректные данные в том виде, в котором они ожидаются. +* Добавьте **JSON схему** для ответа внутри *операции пути* OpenAPI. + * Она будет использована для **автоматически генерируемой документации**. + * А также - для автоматической кодогенерации пользователями. + +Но самое важное: + +* Ответ будет **ограничен и отфильтрован** - т.е. в нем останутся только те данные, которые определены в возвращаемом типе. + * Это особенно важно для **безопасности**, далее мы рассмотрим эту тему подробнее. + +## Параметр `response_model` + +Бывают случаи, когда вам необходимо (или просто хочется) возвращать данные, которые не полностью соответствуют объявленному типу. + +Допустим, вы хотите, чтобы ваша функция **возвращала словарь (dict)** или объект из базы данных, но при этом **объявляете выходной тип как модель Pydantic**. Тогда именно указанная модель будет использована для автоматической документации, валидации и т.п. для объекта, который вы вернули (например, словаря или объекта из базы данных). + +Но если указать аннотацию возвращаемого типа, статическая проверка типов будет выдавать ошибку (абсолютно корректную в данном случае). Она будет говорить о том, что ваша функция должна возвращать данные одного типа (например, dict), а в аннотации вы объявили другой тип (например, модель Pydantic). + +В таком случае можно использовать параметр `response_model` внутри *декоратора операции пути* вместо аннотации возвращаемого значения функции. + +Параметр `response_model` может быть указан для любой *операции пути*: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* и др. + +=== "Python 3.10+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001.py!} + ``` + +!!! note "Технические детали" + Помните, что параметр `response_model` является параметром именно декоратора http-методов (`get`, `post`, и т.п.). Не следует его указывать для *функций операций пути*, как вы бы поступили с другими параметрами или с телом запроса. + +`response_model` принимает те же типы, которые можно указать для какого-либо поля в модели Pydantic. Таким образом, это может быть как одиночная модель Pydantic, так и `список (list)` моделей Pydantic. Например, `List[Item]`. + +FastAPI будет использовать значение `response_model` для того, чтобы автоматически генерировать документацию, производить валидацию и т.п. А также для **конвертации и фильтрации выходных данных** в объявленный тип. + +!!! tip "Подсказка" + Если вы используете анализаторы типов со строгой проверкой (например, mypy), можно указать `Any` в качестве типа возвращаемого значения функции. + + Таким образом вы информируете ваш редактор кода, что намеренно возвращаете данные неопределенного типа. Но возможности FastAPI, такие как автоматическая генерация документации, валидация, фильтрация и т.д. все так же будут работать, просто используя параметр `response_model`. + +### Приоритет `response_model` + +Если одновременно указать аннотацию типа для ответа функции и параметр `response_model` - последний будет иметь больший приоритет и FastAPI будет использовать именно его. + +Таким образом вы можете объявить корректные аннотации типов к вашим функциям, даже если они возвращают тип, отличающийся от указанного в `response_model`. Они будут считаны во время статической проверки типов вашими помощниками, например, mypy. При этом вы все так же используете возможности FastAPI для автоматической документации, валидации и т.д. благодаря `response_model`. + +Вы можете указать значение `response_model=None`, чтобы отключить создание модели ответа для данной *операции пути*. Это может понадобиться, если вы добавляете аннотации типов для данных, не являющихся валидными полями Pydantic. Мы увидим пример кода для такого случая в одном из разделов ниже. + +## Получить и вернуть один и тот же тип данных + +Здесь мы объявили модель `UserIn`, которая хранит пользовательский пароль в открытом виде: + +=== "Python 3.10+" + + ```Python hl_lines="7 9" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9 11" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +!!! info "Информация" + Чтобы использовать `EmailStr`, прежде необходимо установить `email_validator`. + Используйте `pip install email-validator` + или `pip install pydantic[email]`. + +Далее мы используем нашу модель в аннотациях типа как для аргумента функции, так и для выходного значения: + +=== "Python 3.10+" + + ```Python hl_lines="16" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +Теперь всякий раз, когда клиент создает пользователя с паролем, API будет возвращать его пароль в ответе. + +В данном случае это не такая уж большая проблема, поскольку ответ получит тот же самый пользователь, который и создал пароль. + +Но что если мы захотим использовать эту модель для какой-либо другой *операции пути*? Мы можем, сами того не желая, отправить пароль любому другому пользователю. + +!!! danger "Осторожно" + Никогда не храните пароли пользователей в открытом виде, а также никогда не возвращайте их в ответе, как в примере выше. В противном случае - убедитесь, что вы хорошо продумали и учли все возможные риски такого подхода и вам известно, что вы делаете. + +## Создание модели для ответа + +Вместо этого мы можем создать входную модель, хранящую пароль в открытом виде и выходную модель без пароля: + +=== "Python 3.10+" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +В таком случае, даже несмотря на то, что наша *функция операции пути* возвращает тот же самый объект пользователя с паролем, полученным на вход: + +=== "Python 3.10+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +...мы указали в `response_model` модель `UserOut`, в которой отсутствует поле, содержащее пароль - и он будет исключен из ответа: + +=== "Python 3.10+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +Таким образом **FastAPI** позаботится о фильтрации ответа и исключит из него всё, что не указано в выходной модели (при помощи Pydantic). + +### `response_model` или возвращаемый тип данных + +В нашем примере модели входных данных и выходных данных различаются. И если мы укажем аннотацию типа выходного значения функции как `UserOut` - проверка типов выдаст ошибку из-за того, что мы возвращаем некорректный тип. Поскольку это 2 разных класса. + +Поэтому в нашем примере мы можем объявить тип ответа только в параметре `response_model`. + +...но продолжайте читать дальше, чтобы узнать как можно это обойти. + +## Возвращаемый тип и Фильтрация данных + +Продолжим рассматривать предыдущий пример. Мы хотели **аннотировать входные данные одним типом**, а выходное значение - **другим типом**. + +Мы хотим, чтобы FastAPI продолжал **фильтровать** данные, используя `response_model`. + +В прошлом примере, т.к. входной и выходной типы являлись разными классами, мы были вынуждены использовать параметр `response_model`. И как следствие, мы лишались помощи статических анализаторов для проверки ответа функции. + +Но в подавляющем большинстве случаев мы будем хотеть, чтобы модель ответа лишь **фильтровала/удаляла** некоторые данные из ответа, как в нашем примере. + +И в таких случаях мы можем использовать классы и наследование, чтобы пользоваться преимуществами **аннотаций типов** и получать более полную статическую проверку типов. Но при этом все так же получать **фильтрацию ответа** от FastAPI. + +=== "Python 3.10+" + + ```Python hl_lines="7-10 13-14 18" + {!> ../../../docs_src/response_model/tutorial003_01_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9-13 15-16 20" + {!> ../../../docs_src/response_model/tutorial003_01.py!} + ``` + +Таким образом, мы получаем поддержку редактора кода и mypy в части типов, сохраняя при этом фильтрацию данных от FastAPI. + +Как это возможно? Давайте разберемся. 🤓 + +### Аннотации типов и инструменты для их проверки + +Для начала давайте рассмотрим как наш редактор кода, mypy и другие помощники разработчика видят аннотации типов. + +У модели `BaseUser` есть некоторые поля. Затем `UserIn` наследуется от `BaseUser` и добавляет новое поле `password`. Таким образом модель будет включать в себя все поля из первой модели (родителя), а также свои собственные. + +Мы аннотируем возвращаемый тип функции как `BaseUser`, но фактически мы будем возвращать объект типа `UserIn`. + +Редакторы, mypy и другие инструменты не будут иметь возражений против такого подхода, поскольку `UserIn` является подклассом `BaseUser`. Это означает, что такой тип будет *корректным*, т.к. ответ может быть чем угодно, если это будет `BaseUser`. + +### Фильтрация Данных FastAPI + +FastAPI знает тип ответа функции, так что вы можете быть уверены, что на выходе будут **только** те поля, которые вы указали. + +FastAPI совместно с Pydantic выполнит некоторую магию "под капотом", чтобы убедиться, что те же самые правила наследования классов не используются для фильтрации возвращаемых данных, в противном случае вы могли бы в конечном итоге вернуть гораздо больше данных, чем ожидали. + +Таким образом, вы можете получить все самое лучшее из обоих миров: аннотации типов с **поддержкой инструментов для разработки** и **фильтрацию данных**. + +## Автоматическая документация + +Если посмотреть на сгенерированную документацию, вы можете убедиться, что в ней присутствуют обе JSON схемы - как для входной модели, так и для выходной: + + + +И также обе модели будут использованы в интерактивной документации API: + + + +## Другие аннотации типов + +Бывают случаи, когда вы возвращаете что-то, что не является валидным типом для Pydantic и вы указываете аннотацию ответа функции только для того, чтобы работала поддержка различных инструментов (редактор кода, mypy и др.). + +### Возвращаем Response + +Самый частый сценарий использования - это [возвращать Response напрямую, как описано в расширенной документации](../advanced/response-directly.md){.internal-link target=_blank}. + +```Python hl_lines="8 10-11" +{!> ../../../docs_src/response_model/tutorial003_02.py!} +``` + +Это поддерживается FastAPI по-умолчанию, т.к. аннотация проставлена в классе (или подклассе) `Response`. + +И ваши помощники разработки также будут счастливы, т.к. оба класса `RedirectResponse` и `JSONResponse` являются подклассами `Response`. Таким образом мы получаем корректную аннотацию типа. + +### Подкласс Response в аннотации типа + +Вы также можете указать подкласс `Response` в аннотации типа: + +```Python hl_lines="8-9" +{!> ../../../docs_src/response_model/tutorial003_03.py!} +``` + +Это сработает, потому что `RedirectResponse` является подклассом `Response` и FastAPI автоматически обработает этот простейший случай. + +### Некорректные аннотации типов + +Но когда вы возвращаете какой-либо другой произвольный объект, который не является допустимым типом Pydantic (например, объект из базы данных), и вы аннотируете его подобным образом для функции, FastAPI попытается создать из этого типа модель Pydantic и потерпит неудачу. + +То же самое произошло бы, если бы у вас было что-то вроде Union различных типов и один или несколько из них не являлись бы допустимыми типами для Pydantic. Например, такой вариант приведет к ошибке 💥: + +=== "Python 3.10+" + + ```Python hl_lines="8" + {!> ../../../docs_src/response_model/tutorial003_04_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/response_model/tutorial003_04.py!} + ``` + +...такой код вызовет ошибку, потому что в аннотации указан неподдерживаемый Pydantic тип. А также этот тип не является классом или подклассом `Response`. + +### Возможно ли отключить генерацию модели ответа? + +Продолжим рассматривать предыдущий пример. Допустим, что вы хотите отказаться от автоматической валидации ответа, документации, фильтрации и т.д. + +Но в то же время, хотите сохранить аннотацию возвращаемого типа для функции, чтобы обеспечить работу помощников и анализаторов типов (например, mypy). + +В таком случае, вы можете отключить генерацию модели ответа, указав `response_model=None`: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/response_model/tutorial003_05_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/response_model/tutorial003_05.py!} + ``` + +Тогда FastAPI не станет генерировать модель ответа и вы сможете сохранить такую аннотацию типа, которая вам требуется, никак не влияя на работу FastAPI. 🤓 + +## Параметры модели ответа + +Модель ответа может иметь значения по умолчанию, например: + +=== "Python 3.10+" + + ```Python hl_lines="9 11-12" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +* `description: Union[str, None] = None` (или `str | None = None` в Python 3.10), где `None` является значением по умолчанию. +* `tax: float = 10.5`, где `10.5` является значением по умолчанию. +* `tags: List[str] = []`, где пустой список `[]` является значением по умолчанию. + +но вы, возможно, хотели бы исключить их из ответа, если данные поля не были заданы явно. + +Например, у вас есть модель с множеством необязательных полей в NoSQL базе данных, но вы не хотите отправлять в качестве ответа очень длинный JSON с множеством значений по умолчанию. + +### Используйте параметр `response_model_exclude_unset` + +Установите для *декоратора операции пути* параметр `response_model_exclude_unset=True`: + +=== "Python 3.10+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +и тогда значения по умолчанию не будут включены в ответ. В нем будут только те поля, значения которых фактически были установлены. + +Итак, если вы отправите запрос на данную *операцию пути* для элемента, с ID = `Foo` - ответ (с исключенными значениями по-умолчанию) будет таким: + +```JSON +{ + "name": "Foo", + "price": 50.2 +} +``` + +!!! info "Информация" + "Под капотом" FastAPI использует метод `.dict()` у объектов моделей Pydantic с параметром `exclude_unset`, чтобы достичь такого эффекта. + +!!! info "Информация" + Вы также можете использовать: + + * `response_model_exclude_defaults=True` + * `response_model_exclude_none=True` + + как описано в документации Pydantic для параметров `exclude_defaults` и `exclude_none`. + +#### Если значение поля отличается от значения по-умолчанию + +Если для некоторых полей модели, имеющих значения по-умолчанию, значения были явно установлены - как для элемента с ID = `Bar`, ответ будет таким: + +```Python hl_lines="3 5" +{ + "name": "Bar", + "description": "The bartenders", + "price": 62, + "tax": 20.2 +} +``` + +они не будут исключены из ответа. + +#### Если значение поля совпадает с его значением по умолчанию + +Если данные содержат те же значения, которые являются для этих полей по умолчанию, но были установлены явно - как для элемента с ID = `baz`, ответ будет таким: + +```Python hl_lines="3 5-6" +{ + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [] +} +``` + +FastAPI достаточно умен (на самом деле, это заслуга Pydantic), чтобы понять, что, хотя `description`, `tax` и `tags` хранят такие же данные, какие должны быть по умолчанию - для них эти значения были установлены явно (а не получены из значений по умолчанию). + +И поэтому, они также будут включены в JSON ответа. + +!!! tip "Подсказка" + Значением по умолчанию может быть что угодно, не только `None`. + + Им может быть и список (`[]`), значение 10.5 типа `float`, и т.п. + +### `response_model_include` и `response_model_exclude` + +Вы также можете использовать параметры *декоратора операции пути*, такие, как `response_model_include` и `response_model_exclude`. + +Они принимают аргументы типа `set`, состоящий из строк (`str`) с названиями атрибутов, которые либо требуется включить в ответ (при этом исключив все остальные), либо наоборот исключить (оставив в ответе все остальные поля). + +Это можно использовать как быстрый способ исключить данные из ответа, не создавая отдельную модель Pydantic. + +!!! tip "Подсказка" + Но по-прежнему рекомендуется следовать изложенным выше советам и использовать несколько моделей вместо данных параметров. + + Потому как JSON схема OpenAPI, генерируемая вашим приложением (а также документация) все еще будет содержать все поля, даже если вы использовали `response_model_include` или `response_model_exclude` и исключили некоторые атрибуты. + + То же самое применимо к параметру `response_model_by_alias`. + +=== "Python 3.10+" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial005_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial005.py!} + ``` + +!!! tip "Подсказка" + При помощи кода `{"name","description"}` создается объект множества (`set`) с двумя строковыми значениями. + + Того же самого можно достичь используя `set(["name", "description"])`. + +#### Что если использовать `list` вместо `set`? + +Если вы забыли про `set` и использовали структуру `list` или `tuple`, FastAPI автоматически преобразует этот объект в `set`, чтобы обеспечить корректную работу: + +=== "Python 3.10+" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial006_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial006.py!} + ``` + +## Резюме + +Используйте параметр `response_model` у *декоратора операции пути* для того, чтобы задать модель ответа и в большей степени для того, чтобы быть уверенным, что приватная информация будет отфильтрована. + +А также используйте `response_model_exclude_unset`, чтобы возвращать только те значения, которые были заданы явно. diff --git a/docs/ru/docs/tutorial/response-status-code.md b/docs/ru/docs/tutorial/response-status-code.md new file mode 100644 index 0000000000..b2f9b77048 --- /dev/null +++ b/docs/ru/docs/tutorial/response-status-code.md @@ -0,0 +1,89 @@ +# HTTP коды статуса ответа + +Вы можете задать HTTP код статуса ответа с помощью параметра `status_code` подобно тому, как вы определяете схему ответа в любой из *операций пути*: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* и других. + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +!!! note "Примечание" + Обратите внимание, что `status_code` является атрибутом метода-декоратора (`get`, `post` и т.д.), а не *функции-обработчика пути* в отличие от всех остальных параметров и тела запроса. + +Параметр `status_code` принимает число, обозначающее HTTP код статуса ответа. + +!!! info "Информация" + В качестве значения параметра `status_code` также может использоваться `IntEnum`, например, из библиотеки `http.HTTPStatus` в Python. + +Это позволит: + +* Возвращать указанный код статуса в ответе. +* Документировать его как код статуса ответа в OpenAPI схеме (а значит, и в пользовательском интерфейсе): + + + +!!! note "Примечание" + Некоторые коды статуса ответа (см. следующий раздел) указывают на то, что ответ не имеет тела. + + FastAPI знает об этом и создаст документацию OpenAPI, в которой будет указано, что тело ответа отсутствует. + +## Об HTTP кодах статуса ответа + +!!! note "Примечание" + Если вы уже знаете, что представляют собой HTTP коды статуса ответа, можете перейти к следующему разделу. + +В протоколе HTTP числовой код состояния из 3 цифр отправляется как часть ответа. + +У кодов статуса есть названия, чтобы упростить их распознавание, но важны именно числовые значения. + +Кратко о значениях кодов: + +* `1XX` – статус-коды информационного типа. Они редко используются разработчиками напрямую. Ответы с этими кодами не могут иметь тела. +* **`2XX`** – статус-коды, сообщающие об успешной обработке запроса. Они используются чаще всего. + * `200` – это код статуса ответа по умолчанию, который означает, что все прошло "OK". + * Другим примером может быть статус `201`, "Created". Он обычно используется после создания новой записи в базе данных. + * Особый случай – `204`, "No Content". Этот статус ответа используется, когда нет содержимого для возврата клиенту, и поэтому ответ не должен иметь тела. +* **`3XX`** – статус-коды, сообщающие о перенаправлениях. Ответы с этими кодами статуса могут иметь или не иметь тело, за исключением ответов со статусом `304`, "Not Modified", у которых не должно быть тела. +* **`4XX`** – статус-коды, сообщающие о клиентской ошибке. Это ещё одна наиболее часто используемая категория. + * Пример – код `404` для статуса "Not Found". + * Для общих ошибок со стороны клиента можно просто использовать код `400`. +* `5XX` – статус-коды, сообщающие о серверной ошибке. Они почти никогда не используются разработчиками напрямую. Когда что-то идет не так в какой-то части кода вашего приложения или на сервере, он автоматически вернёт один из 5XX кодов. + +!!! tip "Подсказка" + Чтобы узнать больше о HTTP кодах статуса и о том, для чего каждый из них предназначен, ознакомьтесь с документацией MDN об HTTP кодах статуса ответа. + +## Краткие обозначения для запоминания названий кодов + +Рассмотрим предыдущий пример еще раз: + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +`201` – это код статуса "Создано". + +Но вам не обязательно запоминать, что означает каждый из этих кодов. + +Для удобства вы можете использовать переменные из `fastapi.status`. + +```Python hl_lines="1 6" +{!../../../docs_src/response_status_code/tutorial002.py!} +``` + +Они содержат те же числовые значения, но позволяют использовать подсказки редактора для выбора кода статуса: + + + +!!! note "Технические детали" + Вы также можете использовать `from starlette import status` вместо `from fastapi import status`. + + **FastAPI** позволяет использовать как `starlette.status`, так и `fastapi.status` исключительно для удобства разработчиков. Но поставляется fastapi.status непосредственно из Starlette. + +## Изменение кода статуса по умолчанию + +Позже, в [Руководстве для продвинутых пользователей](../advanced/response-change-status-code.md){.internal-link target=_blank}, вы узнаете, как возвращать HTTP коды статуса, отличные от используемого здесь кода статуса по умолчанию. diff --git a/docs/ru/docs/tutorial/schema-extra-example.md b/docs/ru/docs/tutorial/schema-extra-example.md new file mode 100644 index 0000000000..a13ab59354 --- /dev/null +++ b/docs/ru/docs/tutorial/schema-extra-example.md @@ -0,0 +1,189 @@ +# Объявление примера запроса данных + +Вы можете объявлять примеры данных, которые ваше приложение может получать. + +Вот несколько способов, как это можно сделать. + +## Pydantic `schema_extra` + +Вы можете объявить ключ `example` для модели Pydantic, используя класс `Config` и переменную `schema_extra`, как описано в Pydantic документации: Настройка схемы: + +=== "Python 3.10+" + + ```Python hl_lines="13-21" + {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="15-23" + {!> ../../../docs_src/schema_extra_example/tutorial001.py!} + ``` + +Эта дополнительная информация будет включена в **JSON Schema** выходных данных для этой модели, и она будет использоваться в документации к API. + +!!! tip Подсказка + Вы можете использовать тот же метод для расширения JSON-схемы и добавления своей собственной дополнительной информации. + + Например, вы можете использовать это для добавления дополнительной информации для пользовательского интерфейса в вашем веб-приложении и т.д. + +## Дополнительные аргументы поля `Field` + +При использовании `Field()` с моделями Pydantic, вы также можете объявлять дополнительную информацию для **JSON Schema**, передавая любые другие произвольные аргументы в функцию. + +Вы можете использовать это, чтобы добавить аргумент `example` для каждого поля: + +=== "Python 3.10+" + + ```Python hl_lines="2 8-11" + {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4 10-13" + {!> ../../../docs_src/schema_extra_example/tutorial002.py!} + ``` + +!!! warning Внимание + Имейте в виду, что эти дополнительные переданные аргументы не добавляют никакой валидации, только дополнительную информацию для документации. + +## Использование `example` и `examples` в OpenAPI + +При использовании любой из этих функций: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +вы также можете добавить аргумент, содержащий `example` или группу `examples` с дополнительной информацией, которая будет добавлена в **OpenAPI**. + +### Параметр `Body` с аргументом `example` + +Здесь мы передаём аргумент `example`, как пример данных ожидаемых в параметре `Body()`: + +=== "Python 3.10+" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="23-28" + {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip Заметка + Рекомендуется использовать версию с `Annotated`, если это возможно. + + ```Python hl_lines="18-23" + {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip Заметка + Рекомендуется использовать версию с `Annotated`, если это возможно. + + ```Python hl_lines="20-25" + {!> ../../../docs_src/schema_extra_example/tutorial003.py!} + ``` + +### Аргумент "example" в UI документации + +С любым из вышеуказанных методов это будет выглядеть так в `/docs`: + + + +### `Body` с аргументом `examples` + +В качестве альтернативы одному аргументу `example`, вы можете передавать `examples` используя тип данных `dict` с **несколькими примерами**, каждый из которых содержит дополнительную информацию, которая также будет добавлена в **OpenAPI**. + +Ключи `dict` указывают на каждый пример, а значения для каждого из них - на еще один тип `dict` с дополнительной информацией. + +Каждый конкретный пример типа `dict` в аргументе `examples` может содержать: + +* `summary`: Краткое описание для примера. +* `description`: Полное описание, которое может содержать текст в формате Markdown. +* `value`: Это конкретный пример, который отображается, например, в виде типа `dict`. +* `externalValue`: альтернатива параметру `value`, URL-адрес, указывающий на пример. Хотя это может не поддерживаться таким же количеством инструментов разработки и тестирования API, как параметр `value`. + +=== "Python 3.10+" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="24-50" + {!> ../../../docs_src/schema_extra_example/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip Заметка + Рекомендуется использовать версию с `Annotated`, если это возможно. + + ```Python hl_lines="19-45" + {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip Заметка + Рекомендуется использовать версию с `Annotated`, если это возможно. + + ```Python hl_lines="21-47" + {!> ../../../docs_src/schema_extra_example/tutorial004.py!} + ``` + +### Аргумент "examples" в UI документации + +С аргументом `examples`, добавленным в `Body()`, страница документации `/docs` будет выглядеть так: + + + +## Технические Детали + +!!! warning Внимание + Эти технические детали относятся к стандартам **JSON Schema** и **OpenAPI**. + + Если предложенные выше идеи уже работают для вас, возможно этого будет достаточно и эти детали вам не потребуются, можете спокойно их пропустить. + +Когда вы добавляете пример внутрь модели Pydantic, используя `schema_extra` или `Field(example="something")`, этот пример добавляется в **JSON Schema** для данной модели Pydantic. + +И эта **JSON Schema** модели Pydantic включается в **OpenAPI** вашего API, а затем используется в UI документации. + +Поля `example` как такового не существует в стандартах **JSON Schema**. В последних версиях JSON-схемы определено поле `examples`, но OpenAPI 3.0.3 основан на более старой версии JSON-схемы, которая не имела поля `examples`. + +Таким образом, OpenAPI 3.0.3 определяет своё собственное поле `example` для модифицированной версии **JSON Schema**, которую он использует чтобы достичь той же цели (однако это именно поле `example`, а не `examples`), и именно это используется API в UI документации (с интеграцией Swagger UI). + +Итак, хотя поле `example` не является частью JSON-схемы, оно является частью настраиваемой версии JSON-схемы в OpenAPI, и именно это поле будет использоваться в UI документации. + +Однако, когда вы используете поле `example` или `examples` с любой другой функцией (`Query()`, `Body()`, и т.д.), эти примеры не добавляются в JSON-схему, которая описывает эти данные (даже в собственную версию JSON-схемы OpenAPI), они добавляются непосредственно в объявление *операции пути* в OpenAPI (вне частей OpenAPI, которые используют JSON-схему). + +Для функций `Path()`, `Query()`, `Header()`, и `Cookie()`, аргументы `example` или `examples` добавляются в определение OpenAPI, к объекту `Parameter Object` (в спецификации). + +И для функций `Body()`, `File()` и `Form()` аргументы `example` или `examples` аналогично добавляются в определение OpenAPI, к объекту `Request Body Object`, в поле `content` в объекте `Media Type Object` (в спецификации). + +С другой стороны, существует более новая версия OpenAPI: **3.1.0**, недавно выпущенная. Она основана на последней версии JSON-схемы и большинство модификаций из OpenAPI JSON-схемы удалены в обмен на новые возможности из последней версии JSON-схемы, так что все эти мелкие отличия устранены. Тем не менее, Swagger UI в настоящее время не поддерживает OpenAPI 3.1.0, поэтому пока лучше продолжать использовать вышеупомянутые методы. diff --git a/docs/ru/docs/tutorial/security/index.md b/docs/ru/docs/tutorial/security/index.md new file mode 100644 index 0000000000..d5fe4e76f8 --- /dev/null +++ b/docs/ru/docs/tutorial/security/index.md @@ -0,0 +1,101 @@ +# Настройка авторизации + +Существует множество способов обеспечения безопасности, аутентификации и авторизации. + +Обычно эта тема является достаточно сложной и трудной. + +Во многих фреймворках и системах только работа с определением доступов к приложению и аутентификацией требует значительных затрат усилий и написания множества кода (во многих случаях его объём может составлять более 50% от всего написанного кода). + +**FastAPI** предоставляет несколько инструментов, которые помогут вам настроить **Авторизацию** легко, быстро, стандартным способом, без необходимости изучать все её тонкости. + +Но сначала давайте рассмотрим некоторые небольшие концепции. + +## Куда-то торопишься? + +Если вам не нужна информация о каких-либо из следующих терминов и вам просто нужно добавить защиту с аутентификацией на основе логина и пароля *прямо сейчас*, переходите к следующим главам. + +## OAuth2 + +OAuth2 - это протокол, который определяет несколько способов обработки аутентификации и авторизации. + +Он довольно обширен и охватывает несколько сложных вариантов использования. + +OAuth2 включает в себя способы аутентификации с использованием "третьей стороны". + +Это то, что используют под собой все кнопки "вход с помощью Facebook, Google, Twitter, GitHub" на страницах авторизации. + +### OAuth 1 + +Ранее использовался протокол OAuth 1, который сильно отличается от OAuth2 и является более сложным, поскольку он включал прямые описания того, как шифровать сообщение. + +В настоящее время он не очень популярен и не используется. + +OAuth2 не указывает, как шифровать сообщение, он ожидает, что ваше приложение будет обслуживаться по протоколу HTTPS. + +!!! tip "Подсказка" + В разделе **Развертывание** вы увидите [как настроить протокол HTTPS бесплатно, используя Traefik и Let's Encrypt.](https://fastapi.tiangolo.com/ru/deployment/https/) + + +## OpenID Connect + +OpenID Connect - это еще один протокол, основанный на **OAuth2**. + +Он просто расширяет OAuth2, уточняя некоторые вещи, не имеющие однозначного определения в OAuth2, в попытке сделать его более совместимым. + +Например, для входа в Google используется OpenID Connect (который под собой использует OAuth2). + +Но вход в Facebook не поддерживает OpenID Connect. У него есть собственная вариация OAuth2. + +### OpenID (не "OpenID Connect") + +Также ранее использовался стандарт "OpenID", который пытался решить ту же проблему, что и **OpenID Connect**, но не был основан на OAuth2. + +Таким образом, это была полноценная дополнительная система. + +В настоящее время не очень популярен и не используется. + +## OpenAPI + +OpenAPI (ранее известный как Swagger) - это открытая спецификация для создания API (в настоящее время является частью Linux Foundation). + +**FastAPI** основан на **OpenAPI**. + +Это то, что делает возможным наличие множества автоматических интерактивных интерфейсов документирования, сгенерированного кода и т.д. + +В OpenAPI есть способ использовать несколько "схем" безопасности. + +Таким образом, вы можете воспользоваться преимуществами Всех этих стандартных инструментов, включая интерактивные системы документирования. + +OpenAPI может использовать следующие схемы авторизации: + +* `apiKey`: уникальный идентификатор для приложения, который может быть получен из: + * Параметров запроса. + * Заголовка. + * Cookies. +* `http`: стандартные системы аутентификации по протоколу HTTP, включая: + * `bearer`: заголовок `Authorization` со значением `Bearer {уникальный токен}`. Это унаследовано от OAuth2. + * Базовая аутентификация по протоколу HTTP. + * HTTP Digest и т.д. +* `oauth2`: все способы обеспечения безопасности OAuth2 называемые "потоки" (англ. "flows"). + * Некоторые из этих "потоков" подходят для реализации аутентификации через сторонний сервис использующий OAuth 2.0 (например, Google, Facebook, Twitter, GitHub и т.д.): + * `implicit` + * `clientCredentials` + * `authorizationCode` + * Но есть один конкретный "поток", который может быть идеально использован для обработки аутентификации непосредственно в том же приложении: + * `password`: в некоторых следующих главах будут рассмотрены примеры этого. +* `openIdConnect`: способ определить, как автоматически обнаруживать данные аутентификации OAuth2. + * Это автоматическое обнаружение определено в спецификации OpenID Connect. + + +!!! tip "Подсказка" + Интеграция сторонних сервисов для аутентификации/авторизации таких как Google, Facebook, Twitter, GitHub и т.д. осуществляется достаточно легко. + + Самой сложной проблемой является создание такого провайдера аутентификации/авторизации, но **FastAPI** предоставляет вам инструменты, позволяющие легко это сделать, выполняя при этом всю тяжелую работу за вас. + +## Преимущества **FastAPI** + +Fast API предоставляет несколько инструментов для каждой из этих схем безопасности в модуле `fastapi.security`, которые упрощают использование этих механизмов безопасности. + +В следующих главах вы увидите, как обезопасить свой API, используя инструменты, предоставляемые **FastAPI**. + +И вы также увидите, как он автоматически интегрируется в систему интерактивной документации. diff --git a/docs/ru/docs/tutorial/static-files.md b/docs/ru/docs/tutorial/static-files.md new file mode 100644 index 0000000000..ec09eb5a3a --- /dev/null +++ b/docs/ru/docs/tutorial/static-files.md @@ -0,0 +1,40 @@ +# Статические Файлы + +Вы можете предоставлять статические файлы автоматически из директории, используя `StaticFiles`. + +## Использование `StaticFiles` + +* Импортируйте `StaticFiles`. +* "Примонтируйте" экземпляр `StaticFiles()` с указанием определенной директории. + +```Python hl_lines="2 6" +{!../../../docs_src/static_files/tutorial001.py!} +``` + +!!! заметка "Технические детали" + Вы также можете использовать `from starlette.staticfiles import StaticFiles`. + + **FastAPI** предоставляет `starlette.staticfiles` под псевдонимом `fastapi.staticfiles`, просто для вашего удобства, как разработчика. Но на самом деле это берётся напрямую из библиотеки Starlette. + +### Что такое "Монтирование" + +"Монтирование" означает добавление полноценного "независимого" приложения в определенную директорию, которое затем обрабатывает все подпути. + +Это отличается от использования `APIRouter`, так как примонтированное приложение является полностью независимым. +OpenAPI и документация из вашего главного приложения не будет содержать ничего из примонтированного приложения, и т.д. + +Вы можете прочитать больше об этом в **Расширенном руководстве пользователя**. + +## Детали + +Первый параметр `"/static"` относится к подпути, по которому это "подприложение" будет "примонтировано". Таким образом, любой путь начинающийся со `"/static"` будет обработан этим приложением. + +Параметр `directory="static"` относится к имени директории, которая содержит ваши статические файлы. + +`name="static"` даёт имя маршруту, которое может быть использовано внутри **FastAPI**. + +Все эти параметры могут отличаться от "`static`", настройте их в соответствии с вашими нуждами и конкретными деталями вашего собственного приложения. + +## Больше информации + +Для получения дополнительной информации о деталях и настройках ознакомьтесь с Документацией Starlette о статических файлах. diff --git a/docs/ru/docs/tutorial/testing.md b/docs/ru/docs/tutorial/testing.md new file mode 100644 index 0000000000..ca47a6f51e --- /dev/null +++ b/docs/ru/docs/tutorial/testing.md @@ -0,0 +1,212 @@ +# Тестирование + +Благодаря Starlette, тестировать приложения **FastAPI** легко и приятно. + +Тестирование основано на библиотеке HTTPX, которая в свою очередь основана на библиотеке Requests, так что все действия знакомы и интуитивно понятны. + +Используя эти инструменты, Вы можете напрямую задействовать pytest с **FastAPI**. + +## Использование класса `TestClient` + +!!! info "Информация" + Для использования класса `TestClient` необходимо установить библиотеку `httpx`. + + Например, так: `pip install httpx`. + +Импортируйте `TestClient`. + +Создайте объект `TestClient`, передав ему в качестве параметра Ваше приложение **FastAPI**. + +Создайте функцию, название которой должно начинаться с `test_` (это стандарт из соглашений `pytest`). + +Используйте объект `TestClient` так же, как Вы используете `httpx`. + +Напишите простое утверждение с `assert` дабы проверить истинность Python-выражения (это тоже стандарт `pytest`). + +```Python hl_lines="2 12 15-18" +{!../../../docs_src/app_testing/tutorial001.py!} +``` + +!!! tip "Подсказка" + Обратите внимание, что тестирующая функция является обычной `def`, а не асинхронной `async def`. + + И вызов клиента также осуществляется без `await`. + + Это позволяет вам использовать `pytest` без лишних усложнений. + +!!! note "Технические детали" + Также можно написать `from starlette.testclient import TestClient`. + + **FastAPI** предоставляет тот же самый `starlette.testclient` как `fastapi.testclient`. Это всего лишь небольшое удобство для Вас, как разработчика. + +!!! tip "Подсказка" + Если для тестирования Вам, помимо запросов к приложению FastAPI, необходимо вызывать асинхронные функции (например, для подключения к базе данных с помощью асинхронного драйвера), то ознакомьтесь со страницей [Асинхронное тестирование](../advanced/async-tests.md){.internal-link target=_blank} в расширенном руководстве. + +## Разделение тестов и приложения + +В реальном приложении Вы, вероятно, разместите тесты в отдельном файле. + +Кроме того, Ваше приложение **FastAPI** может состоять из нескольких файлов, модулей и т.п. + +### Файл приложения **FastAPI** + +Допустим, структура файлов Вашего приложения похожа на ту, что описана на странице [Более крупные приложения](./bigger-applications.md){.internal-link target=_blank}: + +``` +. +├── app +│   ├── __init__.py +│   └── main.py +``` + +Здесь файл `main.py` является "точкой входа" в Ваше приложение и содержит инициализацию Вашего приложения **FastAPI**: + + +```Python +{!../../../docs_src/app_testing/main.py!} +``` + +### Файл тестов + +Также у Вас может быть файл `test_main.py` содержащий тесты. Можно разместить тестовый файл и файл приложения в одной директории (в директориях для Python-кода желательно размещать и файл `__init__.py`): + +``` hl_lines="5" +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Так как оба файла находятся в одной директории, для импорта объекта приложения из файла `main` в файл `test_main` Вы можете использовать относительный импорт: + +```Python hl_lines="3" +{!../../../docs_src/app_testing/test_main.py!} +``` + +...и писать дальше тесты, как и раньше. + +## Тестирование: расширенный пример + +Теперь давайте расширим наш пример и добавим деталей, чтоб посмотреть, как тестировать различные части приложения. + +### Расширенный файл приложения **FastAPI** + +Мы продолжим работу с той же файловой структурой, что и ранее: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Предположим, что в файле `main.py` с приложением **FastAPI** есть несколько **операций пути**. + +В нём описана операция `GET`, которая может вернуть ошибку. + +Ещё есть операция `POST` и она тоже может вернуть ошибку. + +Обе *операции пути* требуют наличия в запросе заголовка `X-Token`. + +=== "Python 3.10+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py310/main.py!} + ``` + +=== "Python 3.9+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} + ``` + +=== "Python 3.8+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an/main.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + По возможности используйте версию с `Annotated`. + + ```Python + {!> ../../../docs_src/app_testing/app_b_py310/main.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + По возможности используйте версию с `Annotated`. + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + +### Расширенный файл тестов + +Теперь обновим файл `test_main.py`, добавив в него тестов: + +```Python +{!> ../../../docs_src/app_testing/app_b/test_main.py!} +``` + +Если Вы не знаете, как передать информацию в запросе, можете воспользоваться поисковиком (погуглить) и задать вопрос: "Как передать информацию в запросе с помощью `httpx`", можно даже спросить: "Как передать информацию в запросе с помощью `requests`", поскольку дизайн HTTPX основан на дизайне Requests. + +Затем Вы просто применяете найденные ответы в тестах. + +Например: + +* Передаёте *path*-параметры или *query*-параметры, вписав их непосредственно в строку URL. +* Передаёте JSON в теле запроса, передав Python-объект (например: `dict`) через именованный параметр `json`. +* Если же Вам необходимо отправить *форму с данными* вместо JSON, то используйте параметр `data` вместо `json`. +* Для передачи *заголовков*, передайте объект `dict` через параметр `headers`. +* Для передачи *cookies* также передайте `dict`, но через параметр `cookies`. + +Для получения дополнительной информации о передаче данных на бэкенд с помощью `httpx` или `TestClient` ознакомьтесь с документацией HTTPX. + +!!! info "Информация" + Обратите внимание, что `TestClient` принимает данные, которые можно конвертировать в JSON, но не модели Pydantic. + + Если в Ваших тестах есть модели Pydantic и Вы хотите отправить их в тестируемое приложение, то можете использовать функцию `jsonable_encoder`, описанную на странице [Кодировщик совместимый с JSON](encoder.md){.internal-link target=_blank}. + +## Запуск тестов + +Далее Вам нужно установить `pytest`: + +
+ +```console +$ pip install pytest + +---> 100% +``` + +
+ +Он автоматически найдёт все файлы и тесты, выполнит их и предоставит Вам отчёт о результатах тестирования. + +Запустите тесты командой `pytest` и увидите результат: + +
+ +```console +$ pytest + +================ test session starts ================ +platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 +rootdir: /home/user/code/superawesome-cli/app +plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 +collected 6 items + +---> 100% + +test_main.py ...... [100%] + +================= 1 passed in 0.03s ================= +``` + +
diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index f35ee968c9..de18856f44 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -1,155 +1 @@ -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 - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - 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: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- Учебник - руководство пользователя: - - tutorial/background-tasks.md -- async.md -- Развёртывание: - - deployment/index.md - - deployment/versions.md -- external-links.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs/sq/docs/index.md b/docs/sq/docs/index.md deleted file mode 100644 index cff2c28043..0000000000 --- a/docs/sq/docs/index.md +++ /dev/null @@ -1,466 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**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. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## 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.7+ - -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[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**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="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -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 HTTPX 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: - -* httpx - Required if you want to use the `TestClient`. -* 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/sq/mkdocs.yml b/docs/sq/mkdocs.yml deleted file mode 100644 index b07f3bc637..0000000000 --- a/docs/sq/mkdocs.yml +++ /dev/null @@ -1,145 +0,0 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/sq/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/sv/docs/index.md b/docs/sv/docs/index.md deleted file mode 100644 index 23143a96fb..0000000000 --- a/docs/sv/docs/index.md +++ /dev/null @@ -1,468 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**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. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## 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.7+ - -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[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**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="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -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). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on HTTPX 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: - -* httpx - Required if you want to use the `TestClient`. -* 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). -* 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/sv/mkdocs.yml b/docs/sv/mkdocs.yml deleted file mode 100644 index 3332d232d6..0000000000 --- a/docs/sv/mkdocs.yml +++ /dev/null @@ -1,145 +0,0 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/sv/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: sv -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/tr/docs/external-links.md b/docs/tr/docs/external-links.md new file mode 100644 index 0000000000..78eaf1729d --- /dev/null +++ b/docs/tr/docs/external-links.md @@ -0,0 +1,33 @@ +# Harici Bağlantılar ve Makaleler + +**FastAPI** sürekli büyüyen harika bir topluluğa sahiptir. + +**FastAPI** ile alakalı birçok yazı, makale, araç ve proje bulunmaktadır. + +Bunlardan bazılarının tamamlanmamış bir listesi aşağıda bulunmaktadır. + +!!! tip "İpucu" + Eğer **FastAPI** ile alakalı henüz burada listelenmemiş bir makale, proje, araç veya başka bir şeyiniz varsa, bunu eklediğiniz bir Pull Request oluşturabilirsiniz. + +{% for section_name, section_content in external_links.items() %} + +## {{ section_name }} + +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* {{ item.title }} by {{ item.author }}. + +{% endfor %} +{% endfor %} +{% endfor %} + +## Projeler + +`fastapi` konulu en son GitHub projeleri: + +
+
diff --git a/docs/tr/docs/features.md b/docs/tr/docs/features.md index f8220fb58f..8b143ffe7b 100644 --- a/docs/tr/docs/features.md +++ b/docs/tr/docs/features.md @@ -27,7 +27,7 @@ OpenAPI standartlarına dayalı olan bir framework olarak, geliştiricilerin bir ### Sadece modern Python -Tamamiyle standartlar **Python 3.6**'nın type hintlerine dayanıyor (Pydantic'in sayesinde). Yeni bir syntax öğrenmene gerek yok. Sadece modern Python. +Tamamiyle standartlar **Python 3.8**'nın type hintlerine dayanıyor (Pydantic'in sayesinde). Yeni bir syntax öğrenmene gerek yok. Sadece modern Python. Eğer Python type hintlerini bilmiyorsan veya bir hatırlatmaya ihtiyacın var ise(FastAPI kullanmasan bile) şu iki dakikalık küçük bilgilendirici içeriğe bir göz at: [Python Types](python-types.md){.internal-link target=_blank}. diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index 6bd30d7091..ac8830880b 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -1,50 +1,48 @@ - -{!../../../docs/missing-translation.md!} - -

FastAPI

- FastAPI framework, yüksek performanslı, öğrenmesi kolay, geliştirmesi hızlı, kullanıma sunulmaya hazır. + FastAPI framework, yüksek performanslı, öğrenmesi oldukça kolay, kodlaması hızlı, kullanıma hazır

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

--- -**dokümantasyon**: https://fastapi.tiangolo.com +**Dokümantasyon**: https://fastapi.tiangolo.com -**Kaynak kodu**: https://github.com/tiangolo/fastapi +**Kaynak Kod**: https://github.com/tiangolo/fastapi --- -FastAPI, Python 3.6+'nın standart type hintlerine dayanan modern ve hızlı (yüksek performanslı) API'lar oluşturmak için kullanılabilecek web framework'ü. +FastAPI, Python 3.8+'nin standart tip belirteçlerine dayalı, modern ve hızlı (yüksek performanslı) API'lar oluşturmak için kullanılabilecek web framework'tür. -Ana özellikleri: +Temel özellikleri şunlardır: -* **Hızlı**: çok yüksek performanslı, **NodeJS** ve **Go** ile eşdeğer seviyede performans sağlıyor, (Starlette ve Pydantic sayesinde.) [Python'un en hızlı frameworklerinden bir tanesi.](#performans). -* **Kodlaması hızlı**: Yeni özellikler geliştirmek neredeyse %200 - %300 daha hızlı. * -* **Daha az bug**: Geliştirici (insan) kaynaklı hatalar neredeyse %40 azaltıldı. * -* **Sezgileri güçlü**: Editor (otomatik-tamamlama) desteği harika. Otomatik tamamlama her yerde. Debuglamak ile daha az zaman harcayacaksınız. -* **Kolay**: Öğrenmesi ve kullanması kolay olacak şekilde. Doküman okumak için harcayacağınız süre azaltıldı. -* **Kısa**: Kod tekrarını minimuma indirdik. Fonksiyon parametrelerinin tiplerini belirtmede farklı yollar sunarak karşılaşacağınız bug'ları azalttık. -* **Güçlü**: Otomatik dokümantasyon ile beraber, kullanıma hazır kod yaz. +* **Hızlı**: Çok yüksek performanslı, **NodeJS** ve **Go** ile eşit düzeyde (Starlette ve Pydantic sayesinde). [En hızlı Python framework'lerinden bir tanesidir](#performans). +* **Kodlaması Hızlı**: Geliştirme hızını yaklaşık %200 ile %300 aralığında arttırır. * +* **Daha az hata**: İnsan (geliştirici) kaynaklı hataları yaklaşık %40 azaltır. * +* **Sezgisel**: Muhteşem bir editör desteği. Her yerde otomatik tamamlama. Hata ayıklama ile daha az zaman harcayacaksınız. +* **Kolay**: Öğrenmesi ve kullanması kolay olacak şekilde tasarlandı. Doküman okuma ile daha az zaman harcayacaksınız. +* **Kısa**: Kod tekrarı minimize edildi. Her parametre tanımlamasında birden fazla özellik ve daha az hatayla karşılaşacaksınız. +* **Güçlü**: Otomatik ve etkileşimli dokümantasyon ile birlikte, kullanıma hazır kod elde edebilirsiniz. +* **Standard öncelikli**: API'lar için açık standartlara dayalı (ve tamamen uyumlu); OpenAPI (eski adıyla Swagger) ve JSON Schema. -* **Standartlar belirli**: Tamamiyle API'ların açık standartlara bağlı ve (tam uyumlululuk içerisinde); OpenAPI (eski adıyla Swagger) ve JSON Schema. +* ilgili kanılar, dahili geliştirme ekibinin geliştirdikleri ürünlere yaptıkları testlere dayanmaktadır. -* Bahsi geçen rakamsal ifadeler tamamiyle, geliştirme takımının kendi sundukları ürünü geliştirirken yaptıkları testlere dayanmakta. - -## Sponsors +## Sponsorlar @@ -59,74 +57,72 @@ Ana özellikleri: -Other sponsors +Diğer Sponsorlar ## Görüşler - -"_[...] Bugünlerde **FastAPI**'ı çok fazla kullanıyorum [...] Aslına bakarsanız **Microsoft'taki Machine Learning servislerimizin** hepsinde kullanmayı düşünüyorum. FastAPI ile geliştirdiğimiz servislerin bazıları çoktan **Windows**'un ana ürünlerine ve **Office** ürünlerine entegre edilmeye başlandı bile._" +"_[...] Bugünlerde **FastAPI**'ı çok fazla kullanıyorum. [...] Aslında bunu ekibimin **Microsoft'taki Machine Learning servislerinin** tamamında kullanmayı planlıyorum. Bunlardan bazıları **Windows**'un ana ürünlerine ve **Office** ürünlerine entegre ediliyor._"
Kabir Khan - Microsoft (ref)
--- - -"_**FastAPI**'ı **tahminlerimiz**'i sorgulanabilir hale getirmek için **REST** mimarisı ile beraber server üzerinde kullanmaya başladık._" - +"_**FastAPI**'ı **tahminlerimiz**'i sorgulanabilir hale getirecek bir **REST** sunucu oluşturmak için benimsedik/kullanmaya başladık._"
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
--- - -"_**Netflix** **kriz yönetiminde** orkestrasyon yapabilmek için geliştirdiği yeni framework'ü **Dispatch**'in, açık kaynak versiyonunu paylaşmaktan gurur duyuyor. [**FastAPI** ile yapıldı.]_" +"_**Netflix**, **kriz yönetiminde** orkestrasyon yapabilmek için geliştirdiği yeni framework'ü **Dispatch**'in, açık kaynak sürümünü paylaşmaktan gurur duyuyor. [**FastAPI** ile yapıldı.]_"
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
--- - "_**FastAPI** için ayın üzerindeymişcesine heyecanlıyım. Çok eğlenceli!_" -
Brian Okken - Python Bytes podcast host (ref)
--- -"_Dürüst olmak gerekirse, geliştirdiğin şey bir çok açıdan çok sağlam ve parlak gözüküyor. Açıkcası benim **Hug**'ı tasarlarken yapmaya çalıştığım şey buydu - bunu birisinin başardığını görmek gerçekten çok ilham verici._" +"_Dürüst olmak gerekirse, inşa ettiğiniz şey gerçekten sağlam ve profesyonel görünüyor. Birçok açıdan **Hug**'ın olmasını istediğim şey tam da bu - böyle bir şeyi inşa eden birini görmek gerçekten ilham verici._" -
Timothy Crosley - Hug'ın Yaratıcısı (ref)
+
Timothy Crosley - Hug'ın Yaratıcısı (ref)
--- -"_Eğer REST API geliştirmek için **modern bir framework** öğrenme arayışında isen, **FastAPI**'a bir göz at [...] Hızlı, kullanımı ve öğrenmesi kolay. [...]_" - -"_Biz **API** servislerimizi **FastAPI**'a geçirdik [...] Sizin de beğeneceğinizi düşünüyoruz. [...]_" - +"_Eğer REST API geliştirmek için **modern bir framework** öğrenme arayışında isen, **FastAPI**'a bir göz at [...] Hızlı, kullanımı ve öğrenmesi kolay. [...]_" +"_**API** servislerimizi **FastAPI**'a taşıdık [...] Sizin de beğeneceğinizi düşünüyoruz. [...]_"
Ines Montani - Matthew Honnibal - Explosion AI kurucuları - spaCy yaratıcıları (ref) - (ref)
--- -## **Typer**, komut satırı uygulamalarının FastAPI'ı +"_Python ile kullanıma hazır bir API oluşturmak isteyen herhangi biri için, **FastAPI**'ı şiddetle tavsiye ederim. **Harika tasarlanmış**, **kullanımı kolay** ve **yüksek ölçeklenebilir**, API odaklı geliştirme stratejimizin **ana bileşeni** haline geldi ve Virtual TAC Engineer gibi birçok otomasyon ve servisi yönetiyor._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## Komut Satırı Uygulamalarının FastAPI'ı: **Typer** -Eğer API yerine komut satırı uygulaması geliştiriyor isen **Typer**'a bir göz at. +Eğer API yerine, terminalde kullanılmak üzere bir komut satırı uygulaması geliştiriyorsanız **Typer**'a göz atabilirsiniz. -**Typer** kısaca FastAPI'ın küçük kız kardeşi. Komut satırı uygulamalarının **FastAPI'ı** olması hedeflendi. ⌨️ 🚀 +**Typer** kısaca FastAPI'ın küçük kardeşi. Ve hedefi komut satırı uygulamalarının **FastAPI'ı** olmak. ⌨️ 🚀 ## Gereksinimler -Python 3.7+ +Python 3.8+ FastAPI iki devin omuzları üstünde duruyor: * Web tarafı için Starlette. * Data tarafı için Pydantic. -## Yükleme +## Kurulum
@@ -138,7 +134,7 @@ $ pip install fastapi
-Uygulamanı kullanılabilir hale getirmek için Uvicorn ya da Hypercorn gibi bir ASGI serverına ihtiyacın olacak. +Uygulamamızı kullanılabilir hale getirmek için Uvicorn ya da Hypercorn gibi bir ASGI sunucusuna ihtiyacımız olacak.
@@ -152,9 +148,9 @@ $ pip install "uvicorn[standard]" ## Örnek -### Şimdi dene +### Kodu Oluşturalım -* `main.py` adında bir dosya oluştur : +* `main.py` adında bir dosya oluşturup içine şu kodu yapıştıralım: ```Python from typing import Union @@ -177,9 +173,9 @@ def read_item(item_id: int, q: Union[str, None] = None):
Ya da async def... -Eğer kodunda `async` / `await` var ise, `async def` kullan: +Eğer kodunuzda `async` / `await` varsa, `async def` kullanalım: -```Python hl_lines="9 14" +```Python hl_lines="9 14" from typing import Union from fastapi import FastAPI @@ -199,13 +195,13 @@ async def read_item(item_id: int, q: Union[str, None] = None): **Not**: -Eğer ne olduğunu bilmiyor isen _"Acelen mi var?"_ kısmını oku `async` ve `await`. +Eğer bu konu hakkında bilginiz yoksa `async` ve `await` dokümantasyonundaki _"Aceleniz mi var?"_ kısmını kontrol edebilirsiniz.
-### Çalıştır +### Kodu Çalıştıralım -Serverı aşağıdaki komut ile çalıştır: +Sunucuyu aşağıdaki komutla çalıştıralım:
@@ -222,56 +218,56 @@ INFO: Application startup complete.
-Çalıştırdığımız uvicorn main:app --reload hakkında... +uvicorn main:app --reload komutuyla ilgili... -`uvicorn main:app` şunları ifade ediyor: +`uvicorn main:app` komutunu şu şekilde açıklayabiliriz: * `main`: dosya olan `main.py` (yani Python "modülü"). -* `app`: ise `main.py` dosyasının içerisinde oluşturduğumuz `app = FastAPI()` 'a denk geliyor. -* `--reload`: ise kodda herhangi bir değişiklik yaptığımızda serverın yapılan değişiklerileri algılayıp, değişiklikleri siz herhangi bir şey yapmadan uygulamasını sağlıyor. +* `app`: ise `main.py` dosyasının içerisinde `app = FastAPI()` satırında oluşturduğumuz `FastAPI` nesnesi. +* `--reload`: kod değişikliklerinin ardından sunucuyu otomatik olarak yeniden başlatır. Bu parameteyi sadece geliştirme aşamasında kullanmalıyız.
-### Dokümantasyonu kontrol et +### Şimdi de Kontrol Edelim -Browserını aç ve şu linke git http://127.0.0.1:8000/items/5?q=somequery. +Tarayıcımızda şu bağlantıyı açalım http://127.0.0.1:8000/items/5?q=somequery. -Bir JSON yanıtı göreceksin: +Aşağıdaki gibi bir JSON yanıtıyla karşılaşacağız: ```JSON {"item_id": 5, "q": "somequery"} ``` -Az önce oluşturduğun API: +Az önce oluşturduğumuz API: -* `/` ve `/items/{item_id}` adreslerine HTTP talebi alabilir hale geldi. -* İki _adresde_ `GET` operasyonlarını (HTTP _metodları_ olarakta bilinen) yapabilir hale geldi. -* `/items/{item_id}` _adresi_ ayrıca bir `item_id` _adres parametresine_ sahip ve bu bir `int` olmak zorunda. -* `/items/{item_id}` _adresi_ opsiyonel bir `str` _sorgu paramtersine_ sahip bu da `q`. +* `/` ve `/items/{item_id}` _yollarına_ HTTP isteği alabilir. +* İki _yolda_ `GET` operasyonlarını (HTTP _metodları_ olarak da bilinen) kabul ediyor. +* `/items/{item_id}` _yolu_ `item_id` adında bir _yol parametresine_ sahip ve bu parametre `int` değer almak zorundadır. +* `/items/{item_id}` _yolu_ `q` adında bir _yol parametresine_ sahip ve bu parametre opsiyonel olmakla birlikte, `str` değer almak zorundadır. -### İnteraktif API dokümantasyonu +### Etkileşimli API Dokümantasyonu -Şimdi http://127.0.0.1:8000/docs adresine git. +Şimdi http://127.0.0.1:8000/docs bağlantısını açalım. -Senin için otomatik oluşturulmuş(Swagger UI tarafından sağlanan) interaktif bir API dokümanı göreceksin: +Swagger UI tarafından sağlanan otomatik etkileşimli bir API dokümantasyonu göreceğiz: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternatif API dokümantasyonu +### Alternatif API Dokümantasyonu -Şimdi http://127.0.0.1:8000/redoc adresine git. +Şimdi http://127.0.0.1:8000/redoc bağlantısını açalım. -Senin için alternatif olarak (ReDoc tarafından sağlanan) bir API dokümantasyonu daha göreceksin: +ReDoc tarafından sağlanan otomatik dokümantasyonu göreceğiz: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Örnek bir değişiklik +## Örneği Güncelleyelim -Şimdi `main.py` dosyasını değiştirelim ve body ile `PUT` talebi alabilir hale getirelim. +Şimdi `main.py` dosyasını, `PUT` isteğiyle birlikte bir gövde alacak şekilde değiştirelim. -Şimdi Pydantic sayesinde, Python'un standart tiplerini kullanarak bir body tanımlayacağız. +Gövdeyi Pydantic sayesinde standart python tiplerini kullanarak tanımlayalım. -```Python hl_lines="4 9 10 11 12 25 26 27" +```Python hl_lines="4 9-12 25-27" from typing import Union from fastapi import FastAPI @@ -301,41 +297,41 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -Server otomatik olarak yeniden başlamalı (çünkü yukarıda `uvicorn`'u çalıştırırken `--reload` parametresini kullandık.). +Sunucu otomatik olarak yeniden başlamış olmalı (çünkü yukarıda `uvicorn` komutuyla birlikte `--reload` parametresini kullandık). -### İnteraktif API dokümantasyonu'nda değiştirme yapmak +### Etkileşimli API Dokümantasyonundaki Değişimi Görelim -Şimdi http://127.0.0.1:8000/docs bağlantısına tekrar git. +Şimdi http://127.0.0.1:8000/docs bağlantısına tekrar gidelim. -* İnteraktif API dokümantasyonu, yeni body ile beraber çoktan yenilenmiş olması lazım: +* Etkileşimli API dokümantasyonu, yeni gövdede dahil olmak üzere otomatik olarak güncellenmiş olacak: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* "Try it out"a tıkla, bu senin API parametleri üzerinde deneme yapabilmene izin veriyor: +* "Try it out" butonuna tıklayalım, bu işlem API parametleri üzerinde değişiklik yapmamıza ve doğrudan API ile etkileşime geçmemize imkan sağlayacak: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Şimdi "Execute" butonuna tıkla, kullanıcı arayüzü otomatik olarak API'ın ile bağlantı kurarak ona bu parametreleri gönderecek ve sonucu karşına getirecek. +* Şimdi "Execute" butonuna tıklayalım, kullanıcı arayüzü API'ımız ile bağlantı kurup parametreleri gönderecek ve sonucu ekranımıza getirecek: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Alternatif API dokümantasyonunda değiştirmek +### Alternatif API Dokümantasyonundaki Değişimi Görelim -Şimdi ise http://127.0.0.1:8000/redoc adresine git. +Şimdi ise http://127.0.0.1:8000/redoc bağlantısına tekrar gidelim. -* Alternatif dokümantasyonda koddaki değişimler ile beraber kendini yeni query ve body ile güncelledi. +* Alternatif dokümantasyonda yaptığımız değişiklikler ile birlikte yeni sorgu parametresi ve gövde bilgisi ile güncelemiş olacak: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) ### Özet -Özetleyecek olursak, URL, sorgu veya request body'deki parametrelerini fonksiyon parametresi olarak kullanıyorsun. Bu parametrelerin veri tiplerini bir kere belirtmen yeterli. +Özetlemek gerekirse, parametrelerin, gövdenin, vb. veri tiplerini fonksiyon parametreleri olarak **bir kere** tanımlıyoruz. -Type-hinting işlemini Python dilindeki standart veri tipleri ile yapabilirsin +Bu işlemi standart modern Python tipleriyle yapıyoruz. -Yeni bir syntax'e alışmana gerek yok, metodlar ve classlar zaten spesifik kütüphanelere ait. +Yeni bir sözdizimi yapısını, bir kütüphane özel metod veya sınıfları öğrenmeye gerek yoktur. -Sadece standart **Python 3.6+**. +Hepsi sadece **Python 3.8+** standartlarına dayalıdır. Örnek olarak, `int` tanımlamak için: @@ -343,64 +339,64 @@ Sadece standart **Python 3.6+**. item_id: int ``` -ya da daha kompleks `Item` tipi: +ya da daha kompleks herhangi bir python modelini tanımlayabiliriz, örneğin `Item` modeli için: ```Python item: Item ``` -...sadece kısa bir parametre tipi belirtmekle beraber, sahip olacakların: +...ve sadece kısa bir parametre tipi belirterek elde ettiklerimiz: -* Editör desteği dahil olmak üzere: +* Editör desteğiyle birlikte: * Otomatik tamamlama. - * Tip sorguları. -* Datanın tipe uyumunun sorgulanması: - * Eğer data geçersiz ise, otomatik olarak hataları ayıklar. - * Çok derin JSON objelerinde bile veri tipi sorgusu yapar. -* Gelen verinin dönüşümünü aşağıdaki veri tiplerini kullanarak gerçekleştirebiliyor. + * Tip kontrolü. +* Veri Doğrulama: + * Veri geçerli değilse, otomatik olarak açıklayıcı hatalar gösterir. + * Çok derin JSON nesnelerinde bile doğrulama yapar. +* Gelen verinin dönüşümünü aşağıdaki veri tiplerini kullanarak gerçekleştirir: * JSON. - * Path parametreleri. - * Query parametreleri. - * Cookies. + * Yol parametreleri. + * Sorgu parametreleri. + * Çerezler. * Headers. - * Forms. - * Files. -* Giden verinin dönüşümünü aşağıdaki veri tiplerini kullanarak gerçekleştirebiliyor (JSON olarak): - * Python tiplerinin (`str`, `int`, `float`, `bool`, `list`, vs) çevirisi. - * `datetime` objesi. - * `UUID` objesi. + * Formlar. + * Dosyalar. +* Giden verinin dönüşümünü aşağıdaki veri tiplerini kullanarak gerçekleştirir (JSON olarak): + * Python tiplerinin (`str`, `int`, `float`, `bool`, `list`, vb) dönüşümü. + * `datetime` nesnesi. + * `UUID` nesnesi. * Veritabanı modelleri. - * ve daha fazlası... -* 2 alternatif kullanıcı arayüzü dahil olmak üzere, otomatik interaktif API dokümanu: + * ve çok daha fazlası... +* 2 alternatif kullanıcı arayüzü dahil olmak üzere, otomatik etkileşimli API dokümantasyonu sağlar: * Swagger UI. * ReDoc. --- -Az önceki kod örneğine geri dönelim, **FastAPI**'ın yapacaklarına bir bakış atalım: +Az önceki örneğe geri dönelim, **FastAPI**'ın yapacaklarına bir bakış atalım: -* `item_id`'nin `GET` ve `PUT` talepleri içinde olup olmadığının doğruluğunu kontol edecek. -* `item_id`'nin tipinin `int` olduğunu `GET` ve `PUT` talepleri içinde olup olmadığının doğruluğunu kontol edecek. - * Eğer `GET` ve `PUT` içinde yok ise ve `int` değil ise, sebebini belirten bir hata mesajı gösterecek -* Opsiyonel bir `q` parametresinin `GET` talebi için (`http://127.0.0.1:8000/items/foo?q=somequery` içinde) olup olmadığını kontrol edecek +* `item_id`'nin `GET` ve `PUT` istekleri için, yolda olup olmadığının kontol edecek. +* `item_id`'nin `GET` ve `PUT` istekleri için, tipinin `int` olduğunu doğrulayacak. + * Eğer değilse, sebebini belirten bir hata mesajı gösterecek. +* Opsiyonel bir `q` parametresinin `GET` isteği içinde (`http://127.0.0.1:8000/items/foo?q=somequery` gibi) olup olmadığını kontrol edecek * `q` parametresini `= None` ile oluşturduğumuz için, opsiyonel bir parametre olacak. - * Eğer `None` olmasa zorunlu bir parametre olacak idi (bu yüzden body'de `PUT` parametresi var). -* `PUT` talebi için `/items/{item_id}`'nin body'sini, JSON olarak okuyor: - * `name` adında bir parametetre olup olmadığını ve var ise onun `str` olup olmadığını kontol ediyor. - * `price` adında bir parametetre olup olmadığını ve var ise onun `float` olup olmadığını kontol ediyor. - * `is_offer` adında bir parametetre olup olmadığını ve var ise onun `bool` olup olmadığını kontol ediyor. - * Bunların hepsini en derin JSON modellerinde bile yapacaktır. -* Bütün veri tiplerini otomatik olarak JSON'a çeviriyor veya tam tersi. -* Her şeyi dokümanlayıp, çeşitli yerlerde: - * İnteraktif dokümantasyon sistemleri. - * Otomatik alıcı kodu üretim sistemlerinde ve çeşitli dillerde. -* İki ayrı web arayüzüyle direkt olarak interaktif bir dokümantasyon sunuyor. + * Eğer `None` olmasa zorunlu bir parametre olacaktı (`PUT` metodunun gövdesinde olduğu gibi). +* `PUT` isteği için `/items/{item_id}`'nin gövdesini, JSON olarak doğrulayıp okuyacak: + * `name` adında zorunlu bir parametre olup olmadığını ve varsa tipinin `str` olup olmadığını kontol edecek. + * `price` adında zorunlu bir parametre olup olmadığını ve varsa tipinin `float` olup olmadığını kontol edecek. + * `is_offer` adında opsiyonel bir parametre olup olmadığını ve varsa tipinin `float` olup olmadığını kontol edecek. + * Bunların hepsi en derin JSON nesnelerinde bile çalışacak. +* Verilerin JSON'a ve JSON'ın python nesnesine dönüşümü otomatik olarak yapılacak. +* Her şeyi OpenAPI ile uyumlu bir şekilde otomatik olarak dokümanlayacak ve bunlarda aşağıdaki gibi kullanılabilecek: + * Etkileşimli dokümantasyon sistemleri. + * Bir çok programlama dili için otomatik istemci kodu üretim sistemleri. +* İki ayrı etkileşimli dokümantasyon arayüzünü doğrudan sağlayacak. --- -Henüz yüzeysel bir bakış attık, fakat sen çoktan çalışma mantığını anladın. +Daha yeni başladık ama çalışma mantığını çoktan anlamış oldunuz. -Şimdi aşağıdaki satırı değiştirmeyi dene: +Şimdi aşağıdaki satırı değiştirmeyi deneyin: ```Python return {"item_name": item.name, "item_id": item_id} @@ -418,22 +414,22 @@ Henüz yüzeysel bir bakış attık, fakat sen çoktan çalışma mantığını ... "item_price": item.price ... ``` -...şimdi editör desteğinin nasıl veri tiplerini bildiğini ve otomatik tamamladığını gör: +...ve editörünün veri tiplerini bildiğini ve otomatik tamamladığını göreceksiniz: ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -Daha fazla örnek ve özellik için Tutorial - User Guide sayfasını git. +Daha fazal özellik içeren, daha eksiksiz bir örnek için Öğretici - Kullanıcı Rehberi sayfasını ziyaret edebilirsin. -**Spoiler**: Öğretici - Kullanıcı rehberi şunları içeriyor: +**Spoiler**: Öğretici - Kullanıcı rehberi şunları içerir: -* **Parameterlerini** nasıl **headers**, **cookies**, **form fields** ve **files** olarak deklare edebileceğini. -* `maximum_length` ya da `regex` gibi şeylerle nasıl **doğrulama** yapabileceğini. -* Çok güçlü ve kullanımı kolay **Zorunluluk Entegrasyonu** oluşturmayı. -* Güvenlik ve kimlik doğrulama, **JWT tokenleri**'yle beraber **OAuth2** desteği, ve **HTTP Basic** doğrulaması. -* İleri seviye fakat ona göre oldukça basit olan **derince oluşturulmuş JSON modelleri** (Pydantic sayesinde). +* **Parameterlerin**, **headers**, **çerezler**, **form alanları** ve **dosyalar** olarak tanımlanması. +* `maximum_length` ya da `regex` gibi **doğrulama kısıtlamalarının** nasıl yapılabileceği. +* Çok güçlü ve kullanımı kolay **Bağımlılık Enjeksiyonu** sistemi oluşturmayı. +* Güvenlik ve kimlik doğrulama, **JWT tokenleri** ile **OAuth2** desteği, ve **HTTP Basic** doğrulaması. +* İleri seviye fakat bir o kadarda basit olan **çok derin JSON modelleri** (Pydantic sayesinde). +* **GraphQL** entegrasyonu: Strawberry ve diğer kütüphaneleri kullanarak. * Diğer ekstra özellikler (Starlette sayesinde): - * **WebSockets** - * **GraphQL** + * **WebSocketler** * HTTPX ve `pytest` sayesinde aşırı kolay testler. * **CORS** * **Cookie Sessions** @@ -441,34 +437,34 @@ Daha fazla örnek ve özellik için Python'un en hızlı frameworklerinden birisi , sadece Starlette ve Uvicorn'dan daha yavaş ki FastAPI bunların üzerine kurulu. +Bağımsız TechEmpower kıyaslamaları gösteriyor ki, Uvicorn ile çalıştırılan **FastAPI** uygulamaları en hızlı Python framework'lerinden birisi, sadece Starlette ve Uvicorn'dan yavaş, ki FastAPI bunların üzerine kurulu bir kütüphanedir. -Daha fazla bilgi için, bu bölüme bir göz at Benchmarks. +Daha fazla bilgi için, bu bölüme bir göz at Kıyaslamalar. -## Opsiyonel gereksinimler +## Opsiyonel Gereksinimler Pydantic tarafında kullanılan: -* ujson - daha hızlı JSON "dönüşümü" için. * email_validator - email doğrulaması için. +* pydantic-settings - ayar yönetimi için. +* pydantic-extra-types - Pydantic ile birlikte kullanılabilecek ek tipler için. Starlette tarafında kullanılan: -* httpx - Eğer `TestClient` kullanmak istiyorsan gerekli. -* jinja2 - Eğer kendine ait template konfigürasyonu oluşturmak istiyorsan gerekli -* python-multipart - Form kullanmak istiyorsan gerekli ("dönüşümü"). +* httpx - Eğer `TestClient` yapısını kullanacaksanız gereklidir. +* jinja2 - Eğer varsayılan template konfigürasyonunu kullanacaksanız gereklidir. +* python-multipart - Eğer `request.form()` ile form dönüşümü desteğini kullanacaksanız gereklidir. * itsdangerous - `SessionMiddleware` desteği için gerekli. * pyyaml - `SchemaGenerator` desteği için gerekli (Muhtemelen FastAPI kullanırken ihtiyacınız olmaz). -* graphene - `GraphQLApp` desteği için gerekli. -* ujson - `UJSONResponse` kullanmak istiyorsan gerekli. +* ujson - `UJSONResponse` kullanacaksanız gerekli. Hem FastAPI hem de Starlette tarafından kullanılan: -* uvicorn - oluşturduğumuz uygulamayı bir web sunucusuna servis etmek için gerekli -* orjson - `ORJSONResponse` kullanmak istiyor isen gerekli. +* uvicorn - oluşturduğumuz uygulamayı servis edecek web sunucusu görevini üstlenir. +* orjson - `ORJSONResponse` kullanacaksanız gereklidir. Bunların hepsini `pip install fastapi[all]` ile yükleyebilirsin. ## Lisans -Bu proje, MIT lisansı şartlarına göre lisanslanmıştır. +Bu proje, MIT lisansı şartları altında lisanslanmıştır. diff --git a/docs/tr/docs/newsletter.md b/docs/tr/docs/newsletter.md new file mode 100644 index 0000000000..22ca1b1e29 --- /dev/null +++ b/docs/tr/docs/newsletter.md @@ -0,0 +1,5 @@ +# FastAPI ve Arkadaşları Bülteni + + + + diff --git a/docs/tr/docs/tutorial/first_steps.md b/docs/tr/docs/tutorial/first_steps.md new file mode 100644 index 0000000000..b39802f5d6 --- /dev/null +++ b/docs/tr/docs/tutorial/first_steps.md @@ -0,0 +1,336 @@ +# İlk Adımlar + +En basit FastAPI dosyası şu şekildedir: + +```Python +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Bunu bir `main.py` dosyasına kopyalayın. + +Projeyi çalıştırın: + +
+ +```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` komutu şunu ifade eder: + + * `main`: `main.py` dosyası (the Python "module"). + * `app`: `main.py` dosyası içerisinde `app = FastAPI()` satırıyla oluşturulan nesne. + * `--reload`: Kod değişikliği sonrasında sunucunun yeniden başlatılmasını sağlar. Yalnızca geliştirme için kullanın. + +Çıktıda şu şekilde bir satır vardır: + +```hl_lines="4" +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +Bu satır, yerel makinenizde uygulamanızın sunulduğu URL'yi gösterir. + +### Kontrol Et + +Tarayıcınızda http://127.0.0.1:8000 adresini açın. + +Bir JSON yanıtı göreceksiniz: + +```JSON +{"message": "Hello World"} +``` + +### İnteraktif API dokümantasyonu + +http://127.0.0.1:8000/docs adresine gidin. + +Otomatik oluşturulmuş( Swagger UI tarafından sağlanan) interaktif bir API dokümanı göreceksiniz: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternatif API dokümantasyonu + +Şimdi, http://127.0.0.1:8000/redoc adresine gidin. + +Otomatik oluşturulmuş(ReDoc tarafından sağlanan) bir API dokümanı göreceksiniz: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +### OpenAPI + +**FastAPI**, **OpenAPI** standardını kullanarak tüm API'lerinizi açıklayan bir "şema" oluşturur. + +#### "Şema" + +Bir "şema", bir şeyin tanımı veya açıklamasıdır. Soyut bir açıklamadır, uygulayan kod değildir. + +#### API "şemaları" + +Bu durumda, OpenAPI, API şemasını nasıl tanımlayacağınızı belirten şartnamelerdir. + +Bu şema tanımı, API yollarınızı, aldıkları olası parametreleri vb. içerir. + +#### Data "şema" + +"Şema" terimi, JSON içeriği gibi bazı verilerin şeklini de ifade edebilir. + +Bu durumda, JSON öznitelikleri ve sahip oldukları veri türleri vb. anlamına gelir. + +#### OpenAPI and JSON Şema + +OpenAPI, API'niz için bir API şeması tanımlar. Ve bu şema, JSON veri şemaları standardı olan **JSON Şema** kullanılarak API'niz tarafından gönderilen ve alınan verilerin tanımlarını (veya "şemalarını") içerir. + +#### `openapi.json` kontrol et + +OpenAPI şemasının nasıl göründüğünü merak ediyorsanız, FastAPI otomatik olarak tüm API'nizin açıklamalarını içeren bir JSON (şema) oluşturur. + +Doğrudan şu adreste görebilirsiniz: http://127.0.0.1:8000/openapi.json. + +Aşağıdaki gibi bir şeyle başlayan bir JSON gösterecektir: + +```JSON +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + + + +... +``` + +#### OpenAPI ne içindir? + +OpenAPI şeması, dahili olarak bulunan iki etkileşimli dokümantasyon sistemine güç veren şeydir. + +Ve tamamen OpenAPI'ye dayalı düzinelerce alternatif vardır. **FastAPI** ile oluşturulmuş uygulamanıza bu alternatiflerden herhangi birini kolayca ekleyebilirsiniz. + +API'nizle iletişim kuran istemciler için otomatik olarak kod oluşturmak için de kullanabilirsiniz. Örneğin, frontend, mobil veya IoT uygulamaları. + +## Adım adım özet + +### Adım 1: `FastAPI`yi içe aktarın + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI`, API'niz için tüm fonksiyonları sağlayan bir Python sınıfıdır. + +!!! note "Teknik Detaylar" + `FastAPI` doğrudan `Starlette` kalıtım alan bir sınıftır. + + Tüm Starlette fonksiyonlarını `FastAPI` ile de kullanabilirsiniz. + +### Adım 2: Bir `FastAPI` örneği oluşturun + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Burada `app` değişkeni `FastAPI` sınıfının bir örneği olacaktır. + +Bu tüm API'yi oluşturmak için ana etkileşim noktası olacaktır. + +`uvicorn` komutunda atıfta bulunulan `app` ile aynıdır. + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Uygulamanızı aşağıdaki gibi oluşturursanız: + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial002.py!} +``` + +Ve bunu `main.py` dosyasına koyduktan sonra `uvicorn` komutunu şu şekilde çağırabilirsiniz: + +
+ +```console +$ uvicorn main:my_awesome_api --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Adım 3: *Path işlemleri* oluşturmak + +#### Path + +Burada "Path" URL'de ilk "\" ile başlayan son bölümü ifade eder. + +Yani, şu şekilde bir URL'de: + +``` +https://example.com/items/foo +``` + +... path şöyle olabilir: + +``` +/items/foo +``` + +!!! info + Genellikle bir "path", "endpoint" veya "route" olarak adlandırılabilir. + +Bir API oluştururken, "path", "resource" ile "concern" ayırmanın ana yoludur. + +#### İşlemler + +Burada "işlem" HTTP methodlarından birini ifade eder. + +Onlardan biri: + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +... ve daha egzotik olanları: + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +HTTP protokolünde, bu "methodlardan" birini (veya daha fazlasını) kullanarak her path ile iletişim kurabilirsiniz. + +--- + +API'lerinizi oluştururkan, belirli bir işlemi gerçekleştirirken belirli HTTP methodlarını kullanırsınız. + +Normalde kullanılan: + +* `POST`: veri oluşturmak. +* `GET`: veri okumak. +* `PUT`: veriyi güncellemek. +* `DELETE`: veriyi silmek. + +Bu nedenle, OpenAPI'de HTTP methodlarından her birine "işlem" denir. + +Bizde onlara "**işlemler**" diyeceğiz. + +#### Bir *Path işlem decoratorleri* tanımlanmak + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`@app.get("/")` **FastAPI'ye** aşağıdaki fonksiyonun adresine giden istekleri işlemekten sorumlu olduğunu söyler: + +* path `/` +* get işlemi kullanılarak + + +!!! info "`@decorator` Bilgisi" + Python `@something` şeklinde ifadeleri "decorator" olarak adlandırır. + + Decoratoru bir fonksiyonun üzerine koyarsınız. Dekoratif bir şapka gibi (Sanırım terim buradan gelmektedir). + + Bir "decorator" fonksiyonu alır ve bazı işlemler gerçekleştir. + + Bizim durumumzda decarator **FastAPI'ye** fonksiyonun bir `get` işlemi ile `/` pathine geldiğini söyler. + + Bu **path işlem decoratordür** + +Ayrıca diğer işlemleri de kullanabilirsiniz: + +* `@app.post()` +* `@app.put()` +* `@app.delete()` + +Ve daha egzotik olanları: + +* `@app.options()` +* `@app.head()` +* `@app.patch()` +* `@app.trace()` + +!!! tip + Her işlemi (HTTP method) istediğiniz gibi kullanmakta özgürsünüz. + + **FastAPI** herhangi bir özel anlamı zorlamaz. + + Buradaki bilgiler bir gereklilik değil, bir kılavuz olarak sunulmaktadır. + + Örneğin, GraphQL kullanırkan normalde tüm işlemleri yalnızca `POST` işlemini kullanarak gerçekleştirirsiniz. + +### Adım 4: **path işlem fonksiyonunu** tanımlayın + +Aşağıdakiler bizim **path işlem fonksiyonlarımızdır**: + +* **path**: `/` +* **işlem**: `get` +* **function**: "decorator"ün altındaki fonksiyondur (`@app.get("/")` altında). + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Bu bir Python fonksiyonudur. + +Bir `GET` işlemi kullanarak "`/`" URL'sine bir istek geldiğinde **FastAPI** tarafından çağrılır. + +Bu durumda bir `async` fonksiyonudur. + +--- + +Bunu `async def` yerine normal bir fonksiyon olarakta tanımlayabilirsiniz. + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note + + Eğer farkı bilmiyorsanız, [Async: *"Acelesi var?"*](../async.md#in-a-hurry){.internal-link target=_blank} kontrol edebilirsiniz. + +### Adım 5: İçeriği geri döndürün + + +```Python hl_lines="8" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Bir `dict`, `list` döndürebilir veya `str`, `int` gibi tekil değerler döndürebilirsiniz. + +Ayrıca, Pydantic modellerini de döndürebilirsiniz. (Bununla ilgili daha sonra ayrıntılı bilgi göreceksiniz.) + +Otomatik olarak JSON'a dönüştürülecek(ORM'ler vb. dahil) başka birçok nesne ve model vardır. En beğendiklerinizi kullanmayı deneyin, yüksek ihtimalle destekleniyordur. + +## Özet + +* `FastAPI`'yi içe aktarın. +* Bir `app` örneği oluşturun. +* **path işlem decorator** yazın. (`@app.get("/")` gibi) +* **path işlem fonksiyonu** yazın. (`def root(): ...` gibi) +* Development sunucunuzu çalıştırın. (`uvicorn main:app --reload` gibi) diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index e29d259365..de18856f44 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -1,148 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/tr/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: tr -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs/uk/docs/alternatives.md b/docs/uk/docs/alternatives.md new file mode 100644 index 0000000000..e712579769 --- /dev/null +++ b/docs/uk/docs/alternatives.md @@ -0,0 +1,412 @@ +# Альтернативи, натхнення та порівняння + +Що надихнуло на створення **FastAPI**, який він у порінянні з іншими альтернативами та чого він у них навчився. + +## Вступ + +**FastAPI** не існувало б, якби не попередні роботи інших. + +Раніше було створено багато інструментів, які надихнули на його створення. + +Я кілька років уникав створення нового фреймворку. Спочатку я спробував вирішити всі функції, охоплені **FastAPI**, використовуючи багато різних фреймворків, плагінів та інструментів. + +Але в якийсь момент не було іншого виходу, окрім створення чогось, що надавало б усі ці функції, взявши найкращі ідеї з попередніх інструментів і поєднавши їх найкращим чином, використовуючи мовні функції, які навіть не були доступні раніше (Python 3.6+ підказки типів). + +## Попередні інструменти + +### Django + +Це найпопулярніший фреймворк Python, який користується широкою довірою. Він використовується для створення таких систем, як Instagram. + +Він відносно тісно пов’язаний з реляційними базами даних (наприклад, MySQL або PostgreSQL), тому мати базу даних NoSQL (наприклад, Couchbase, MongoDB, Cassandra тощо) як основний механізм зберігання не дуже просто. + +Він був створений для створення HTML у серверній частині, а не для створення API, які використовуються сучасним інтерфейсом (як-от React, Vue.js і Angular) або іншими системами (як-от IoT пристрої), які спілкуються з ним. + +### Django REST Framework + +Фреймворк Django REST був створений як гнучкий інструментарій для створення веб-інтерфейсів API використовуючи Django в основі, щоб покращити його можливості API. + +Його використовують багато компаній, включаючи Mozilla, Red Hat і Eventbrite. + +Це був один із перших прикладів **автоматичної документації API**, і саме це була одна з перших ідей, яка надихнула на «пошук» **FastAPI**. + +!!! Примітка + Django REST Framework створив Том Крісті. Той самий творець Starlette і Uvicorn, на яких базується **FastAPI**. + + +!!! Перегляньте "Надихнуло **FastAPI** на" + Мати автоматичний веб-інтерфейс документації API. + +### Flask + +Flask — це «мікрофреймворк», він не включає інтеграцію бази даних, а також багато речей, які за замовчуванням є в Django. + +Ця простота та гнучкість дозволяють використовувати бази даних NoSQL як основну систему зберігання даних. + +Оскільки він дуже простий, він порівняно легкий та інтуїтивний для освоєння, хоча в деяких моментах документація стає дещо технічною. + +Він також зазвичай використовується для інших програм, яким не обов’язково потрібна база даних, керування користувачами або будь-яка з багатьох функцій, які є попередньо вбудованими в Django. Хоча багато з цих функцій можна додати за допомогою плагінів. + +Відокремлення частин було ключовою особливістю, яку я хотів зберегти, при цьому залишаючись «мікрофреймворком», який можна розширити, щоб охопити саме те, що потрібно. + +Враховуючи простоту Flask, він здавався хорошим підходом для створення API. Наступним, що знайшов, був «Django REST Framework» для Flask. + +!!! Переглянте "Надихнуло **FastAPI** на" + Бути мікрофреймоворком. Зробити легким комбінування та поєднання необхідних інструментів та частин. + + Мати просту та легку у використанні систему маршрутизації. + + +### Requests + +**FastAPI** насправді не є альтернативою **Requests**. Сфера їх застосування дуже різна. + +Насправді цілком звична річ використовувати Requests *всередині* програми FastAPI. + +Але все ж FastAPI черпав натхнення з Requests. + +**Requests** — це бібліотека для *взаємодії* з API (як клієнт), а **FastAPI** — це бібліотека для *створення* API (як сервер). + +Вони більш-менш знаходяться на протилежних кінцях, доповнюючи одна одну. + +Requests мають дуже простий та інтуїтивно зрозумілий дизайн, дуже простий у використанні, з розумними параметрами за замовчуванням. Але в той же час він дуже потужний і налаштовується. + +Ось чому, як сказано на офіційному сайті: + +> Requests є одним із найбільш завантажуваних пакетів Python усіх часів + +Використовувати його дуже просто. Наприклад, щоб виконати запит `GET`, ви повинні написати: + +```Python +response = requests.get("http://example.com/some/url") +``` + +Відповідна операція *роуту* API FastAPI може виглядати так: + +```Python hl_lines="1" +@app.get("/some/url") +def read_url(): + return {"message": "Hello World"} +``` + +Зверніть увагу на схожість у `requests.get(...)` і `@app.get(...)`. + +!!! Перегляньте "Надихнуло **FastAPI** на" + * Майте простий та інтуїтивно зрозумілий API. + * Використовуйте імена (операції) методів HTTP безпосередньо, простим та інтуїтивно зрозумілим способом. + * Розумні параметри за замовчуванням, але потужні налаштування. + + +### Swagger / OpenAPI + +Головною функцією, яку я хотів від Django REST Framework, була автоматична API документація. + +Потім я виявив, що існує стандарт для документування API з використанням JSON (або YAML, розширення JSON) під назвою Swagger. + +І вже був створений веб-інтерфейс користувача для Swagger API. Отже, можливість генерувати документацію Swagger для API дозволить використовувати цей веб-інтерфейс автоматично. + +У якийсь момент Swagger було передано Linux Foundation, щоб перейменувати його на OpenAPI. + +Тому, коли говорять про версію 2.0, прийнято говорити «Swagger», а про версію 3+ «OpenAPI». + +!!! Перегляньте "Надихнуло **FastAPI** на" + Прийняти і використовувати відкритий стандарт для специфікацій API замість спеціальної схеми. + + Інтегрувати інструменти інтерфейсу на основі стандартів: + + * Інтерфейс Swagger + * ReDoc + + Ці два було обрано через те, що вони досить популярні та стабільні, але, виконавши швидкий пошук, ви можете знайти десятки додаткових альтернативних інтерфейсів для OpenAPI (які можна використовувати з **FastAPI**). + +### Фреймворки REST для Flask + +Існує кілька фреймворків Flask REST, але, витративши час і роботу на їх дослідження, я виявив, що багато з них припинено або залишено, з кількома постійними проблемами, які зробили їх непридатними. + +### Marshmallow + +Однією з головних функцій, необхідних для систем API, є "серіалізація", яка бере дані з коду (Python) і перетворює їх на щось, що можна надіслати через мережу. Наприклад, перетворення об’єкта, що містить дані з бази даних, на об’єкт JSON. Перетворення об’єктів `datetime` на строки тощо. + +Іншою важливою функцією, необхідною для API, є перевірка даних, яка забезпечує дійсність даних за певними параметрами. Наприклад, що деяке поле є `int`, а не деяка випадкова строка. Це особливо корисно для вхідних даних. + +Без системи перевірки даних вам довелося б виконувати всі перевірки вручну, у коді. + +Marshmallow створено для забезпечення цих функцій. Це чудова бібліотека, і я часто нею користувався раніше. + +Але він був створений до того, як існували підказки типу Python. Отже, щоб визначити кожну схему, вам потрібно використовувати спеціальні утиліти та класи, надані Marshmallow. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Використовувати код для автоматичного визначення "схем", які надають типи даних і перевірку. + +### Webargs + +Іншою важливою функцією, необхідною для API, є аналіз даних із вхідних запитів. + +Webargs — це інструмент, створений, щоб забезпечити це поверх кількох фреймворків, включаючи Flask. + +Він використовує Marshmallow в основі для перевірки даних. І створений тими ж розробниками. + +Це чудовий інструмент, і я також часто використовував його, перш ніж створити **FastAPI**. + +!!! Інформація + Webargs був створений тими ж розробниками Marshmallow. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Мати автоматичну перевірку даних вхідного запиту. + +### APISpec + +Marshmallow і Webargs забезпечують перевірку, аналіз і серіалізацію як плагіни. + +Але документація досі відсутня. Потім було створено APISpec. + +Це плагін для багатьох фреймворків (також є плагін для Starlette). + +Принцип роботи полягає в тому, що ви пишете визначення схеми, використовуючи формат YAML, у docstring кожної функції, що обробляє маршрут. + +І він генерує схеми OpenAPI. + +Так це працює у Flask, Starlette, Responder тощо. + +Але потім ми знову маємо проблему наявності мікросинтаксису всередині Python строки (великий YAML). + +Редактор тут нічим не може допомогти. І якщо ми змінимо параметри чи схеми Marshmallow і забудемо також змінити цю строку документа YAML, згенерована схема буде застарілою. + +!!! Інформація + APISpec був створений тими ж розробниками Marshmallow. + + +!!! Перегляньте "Надихнуло **FastAPI** на" + Підтримувати відкритий стандарт API, OpenAPI. + +### Flask-apispec + +Це плагін Flask, який об’єднує Webargs, Marshmallow і APISpec. + +Він використовує інформацію з Webargs і Marshmallow для автоматичного створення схем OpenAPI за допомогою APISpec. + +Це чудовий інструмент, дуже недооцінений. Він має бути набагато популярнішим, ніж багато плагінів Flask. Це може бути пов’язано з тим, що його документація надто стисла й абстрактна. + +Це вирішило необхідність писати YAML (інший синтаксис) всередині рядків документів Python. + +Ця комбінація Flask, Flask-apispec із Marshmallow і Webargs була моїм улюбленим бекенд-стеком до створення **FastAPI**. + +Їі використання призвело до створення кількох генераторів повного стека Flask. Це основний стек, який я (та кілька зовнішніх команд) використовував досі: + +* https://github.com/tiangolo/full-stack +* https://github.com/tiangolo/full-stack-flask-couchbase +* https://github.com/tiangolo/full-stack-flask-couchdb + +І ці самі генератори повного стеку були основою [**FastAPI** генераторів проектів](project-generation.md){.internal-link target=_blank}. + +!!! Інформація + Flask-apispec був створений тими ж розробниками Marshmallow. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Створення схеми OpenAPI автоматично з того самого коду, який визначає серіалізацію та перевірку. + +### NestJS (та Angular) + +Це навіть не Python, NestJS — це фреймворк NodeJS JavaScript (TypeScript), натхненний Angular. + +Це досягає чогось подібного до того, що можна зробити з Flask-apispec. + +Він має інтегровану систему впровадження залежностей, натхненну Angular two. Він потребує попередньої реєстрації «injectables» (як і всі інші системи впровадження залежностей, які я знаю), тому це збільшує багатослівність та повторення коду. + +Оскільки параметри описані за допомогою типів TypeScript (подібно до підказок типу Python), підтримка редактора досить хороша. + +Але оскільки дані TypeScript не зберігаються після компіляції в JavaScript, вони не можуть покладатися на типи для визначення перевірки, серіалізації та документації одночасно. Через це та деякі дизайнерські рішення, щоб отримати перевірку, серіалізацію та автоматичну генерацію схеми, потрібно додати декоратори в багатьох місцях. Таким чином код стає досить багатослівним. + +Він не дуже добре обробляє вкладені моделі. Отже, якщо тіло JSON у запиті є об’єктом JSON із внутрішніми полями, які, у свою чергу, є вкладеними об’єктами JSON, його неможливо належним чином задокументувати та перевірити. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Використовувати типи Python, щоб мати чудову підтримку редактора. + + Мати потужну систему впровадження залежностей. Знайдіть спосіб звести до мінімуму повторення коду. + +### Sanic + +Це був один із перших надзвичайно швидких фреймворків Python на основі `asyncio`. Він був дуже схожий на Flask. + +!!! Примітка "Технічні деталі" + Він використовував `uvloop` замість стандартного циклу Python `asyncio`. Ось що зробило його таким швидким. + + Це явно надихнуло Uvicorn і Starlette, які зараз швидші за Sanic у відкритих тестах. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Знайти спосіб отримати божевільну продуктивність. + + Ось чому **FastAPI** базується на Starlette, оскільки це найшвидша доступна структура (перевірена тестами сторонніх розробників). + +### Falcon + +Falcon — ще один високопродуктивний фреймворк Python, він розроблений як мінімальний і працює як основа інших фреймворків, таких як Hug. + +Він розроблений таким чином, щоб мати функції, які отримують два параметри, один «запит» і один «відповідь». Потім ви «читаєте» частини запиту та «записуєте» частини у відповідь. Через такий дизайн неможливо оголосити параметри запиту та тіла за допомогою стандартних підказок типу Python як параметри функції. + +Таким чином, перевірка даних, серіалізація та документація повинні виконуватися в коді, а не автоматично. Або вони повинні бути реалізовані як фреймворк поверх Falcon, як Hug. Така сама відмінність спостерігається в інших фреймворках, натхненних дизайном Falcon, що мають один об’єкт запиту та один об’єкт відповіді як параметри. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Знайти способи отримати чудову продуктивність. + + Разом із Hug (оскільки Hug базується на Falcon) надихнув **FastAPI** оголосити параметр `response` у функціях. + + Хоча у FastAPI це необов’язково, і використовується в основному для встановлення заголовків, файлів cookie та альтернативних кодів стану. + +### Molten + +Я відкрив для себе Molten на перших етапах створення **FastAPI**. І він має досить схожі ідеї: + +* Базується на підказках типу Python. +* Перевірка та документація цих типів. +* Система впровадження залежностей. + +Він не використовує перевірку даних, серіалізацію та бібліотеку документації сторонніх розробників, як Pydantic, він має свою власну. Таким чином, ці визначення типів даних не можна було б використовувати повторно так легко. + +Це вимагає трохи більш докладних конфігурацій. І оскільки він заснований на WSGI (замість ASGI), він не призначений для використання високопродуктивних інструментів, таких як Uvicorn, Starlette і Sanic. + +Система впровадження залежностей вимагає попередньої реєстрації залежностей, і залежності вирішуються на основі оголошених типів. Отже, неможливо оголосити більше ніж один «компонент», який надає певний тип. + +Маршрути оголошуються в одному місці з використанням функцій, оголошених в інших місцях (замість використання декораторів, які можна розмістити безпосередньо поверх функції, яка обробляє кінцеву точку). Це ближче до того, як це робить Django, ніж до Flask (і Starlette). Він розділяє в коді речі, які відносно тісно пов’язані. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Визначити додаткові перевірки для типів даних, використовуючи значення "за замовчуванням" атрибутів моделі. Це покращує підтримку редактора, а раніше вона була недоступна в Pydantic. + + Це фактично надихнуло оновити частини Pydantic, щоб підтримувати той самий стиль оголошення перевірки (всі ці функції вже доступні в Pydantic). + +### Hug + +Hug був одним із перших фреймворків, який реалізував оголошення типів параметрів API за допомогою підказок типу Python. Це була чудова ідея, яка надихнула інші інструменти зробити те саме. + +Він використовував спеціальні типи у своїх оголошеннях замість стандартних типів Python, але це все одно був величезний крок вперед. + +Це також був один із перших фреймворків, який генерував спеціальну схему, що оголошувала весь API у JSON. + +Він не базувався на таких стандартах, як OpenAPI та JSON Schema. Тому було б непросто інтегрувати його з іншими інструментами, як-от Swagger UI. Але знову ж таки, це була дуже інноваційна ідея. + +Він має цікаву незвичайну функцію: використовуючи ту саму структуру, можна створювати API, а також CLI. + +Оскільки він заснований на попередньому стандарті для синхронних веб-фреймворків Python (WSGI), він не може працювати з Websockets та іншими речами, хоча він також має високу продуктивність. + +!!! Інформація + Hug створив Тімоті Крослі, той самий творець `isort`, чудовий інструмент для автоматичного сортування імпорту у файлах Python. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Hug надихнув частину APIStar і був одним із найбільш перспективних інструментів, поряд із APIStar. + + Hug надихнув **FastAPI** на використання підказок типу Python для оголошення параметрів і автоматичного створення схеми, що визначає API. + + Hug надихнув **FastAPI** оголосити параметр `response` у функціях для встановлення заголовків і файлів cookie. + +### APIStar (<= 0,5) + +Безпосередньо перед тим, як вирішити створити **FastAPI**, я знайшов сервер **APIStar**. Він мав майже все, що я шукав, і мав чудовий дизайн. + +Це була одна з перших реалізацій фреймворку, що використовує підказки типу Python для оголошення параметрів і запитів, яку я коли-небудь бачив (до NestJS і Molten). Я знайшов його більш-менш одночасно з Hug. Але APIStar використовував стандарт OpenAPI. + +Він мав автоматичну перевірку даних, серіалізацію даних і генерацію схеми OpenAPI на основі підказок того самого типу в кількох місцях. + +Визначення схеми тіла не використовували ті самі підказки типу Python, як Pydantic, воно було трохи схоже на Marshmallow, тому підтримка редактора була б не такою хорошою, але все ж APIStar був найкращим доступним варіантом. + +Він мав найкращі показники продуктивності на той час (перевершив лише Starlette). + +Спочатку він не мав автоматичного веб-інтерфейсу документації API, але я знав, що можу додати до нього інтерфейс користувача Swagger. + +Він мав систему введення залежностей. Він вимагав попередньої реєстрації компонентів, як і інші інструменти, розглянуті вище. Але все одно це була чудова функція. + +Я ніколи не міг використовувати його в повноцінному проекті, оскільки він не мав інтеграції безпеки, тому я не міг замінити всі функції, які мав, генераторами повного стеку на основі Flask-apispec. У моїх невиконаних проектах я мав створити запит на вилучення, додавши цю функцію. + +Але потім фокус проекту змінився. + +Це вже не був веб-фреймворк API, оскільки творцю потрібно було зосередитися на Starlette. + +Тепер APIStar — це набір інструментів для перевірки специфікацій OpenAPI, а не веб-фреймворк. + +!!! Інформація + APIStar створив Том Крісті. Той самий хлопець, який створив: + + * Django REST Framework + * Starlette (на якому базується **FastAPI**) + * Uvicorn (використовується Starlette і **FastAPI**) + +!!! Перегляньте "Надихнуло **FastAPI** на" + Існувати. + + Ідею оголошення кількох речей (перевірки даних, серіалізації та документації) за допомогою тих самих типів Python, які в той же час забезпечували чудову підтримку редактора, я вважав геніальною ідеєю. + + І після тривалого пошуку подібної структури та тестування багатьох різних альтернатив, APIStar став найкращим доступним варіантом. + + Потім APIStar перестав існувати як сервер, і було створено Starlette, який став новою кращою основою для такої системи. Це стало останнім джерелом натхнення для створення **FastAPI**. Я вважаю **FastAPI** «духовним спадкоємцем» APIStar, удосконалюючи та розширюючи функції, систему введення тексту та інші частини на основі досвіду, отриманого від усіх цих попередніх інструментів. + +## Використовується **FastAPI** + +### Pydantic + +Pydantic — це бібліотека для визначення перевірки даних, серіалізації та документації (за допомогою схеми JSON) на основі підказок типу Python. + +Це робить його надзвичайно інтуїтивним. + +Його можна порівняти з Marshmallow. Хоча він швидший за Marshmallow у тестах. Оскільки він базується на тих самих підказках типу Python, підтримка редактора чудова. + +!!! Перегляньте "**FastAPI** використовує його для" + Виконання перевірки всіх даних, серіалізації даних і автоматичної документацію моделі (на основі схеми JSON). + + Потім **FastAPI** бере ці дані схеми JSON і розміщує їх у OpenAPI, окремо від усіх інших речей, які він робить. + +### Starlette + +Starlette — це легкий фреймворк/набір інструментів ASGI, який ідеально підходить для створення високопродуктивних asyncio сервісів. + +Він дуже простий та інтуїтивно зрозумілий. Його розроблено таким чином, щоб його можна було легко розширювати та мати модульні компоненти. + +Він має: + +* Серйозно вражаючу продуктивність. +* Підтримку WebSocket. +* Фонові завдання в процесі. +* Події запуску та завершення роботи. +* Тестового клієнта, побудований на HTTPX. +* CORS, GZip, статичні файли, потокові відповіді. +* Підтримку сеансів і файлів cookie. +* 100% покриття тестом. +* 100% анотовану кодову базу. +* Кілька жорстких залежностей. + +Starlette наразі є найшвидшим фреймворком Python із перевірених. Перевершує лише Uvicorn, який є не фреймворком, а сервером. + +Starlette надає всі основні функції веб-мікрофреймворку. + +Але він не забезпечує автоматичної перевірки даних, серіалізації чи документації. + +Це одна з головних речей, які **FastAPI** додає зверху, все на основі підказок типу Python (з використанням Pydantic). Це, а також система впровадження залежностей, утиліти безпеки, створення схеми OpenAPI тощо. + +!!! Примітка "Технічні деталі" + ASGI — це новий «стандарт», який розробляється членами основної команди Django. Це ще не «стандарт Python» (PEP), хоча вони в процесі цього. + + Тим не менш, він уже використовується як «стандарт» кількома інструментами. Це значно покращує сумісність, оскільки ви можете переключити Uvicorn на будь-який інший сервер ASGI (наприклад, Daphne або Hypercorn), або ви можете додати інструменти, сумісні з ASGI, як-от `python-socketio`. + +!!! Перегляньте "**FastAPI** використовує його для" + Керування всіма основними веб-частинами. Додавання функцій зверху. + + Сам клас `FastAPI` безпосередньо успадковує клас `Starlette`. + + Отже, усе, що ви можете робити зі Starlette, ви можете робити це безпосередньо за допомогою **FastAPI**, оскільки це, по суті, Starlette на стероїдах. + +### Uvicorn + +Uvicorn — це блискавичний сервер ASGI, побудований на uvloop і httptools. + +Це не веб-фреймворк, а сервер. Наприклад, він не надає інструментів для маршрутизації. Це те, що фреймворк на кшталт Starlette (або **FastAPI**) забезпечить поверх нього. + +Це рекомендований сервер для Starlette і **FastAPI**. + +!!! Перегляньте "**FastAPI** рекомендує це як" + Основний веб-сервер для запуску програм **FastAPI**. + + Ви можете поєднати його з Gunicorn, щоб мати асинхронний багатопроцесний сервер. + + Додаткову інформацію див. у розділі [Розгортання](deployment/index.md){.internal-link target=_blank}. + +## Орієнтири та швидкість + +Щоб зрозуміти, порівняти та побачити різницю між Uvicorn, Starlette і FastAPI, перегляньте розділ про [Бенчмарки](benchmarks.md){.internal-link target=_blank}. diff --git a/docs/uk/docs/fastapi-people.md b/docs/uk/docs/fastapi-people.md new file mode 100644 index 0000000000..b32f0e5cef --- /dev/null +++ b/docs/uk/docs/fastapi-people.md @@ -0,0 +1,178 @@ +# Люди FastAPI + +FastAPI має дивовижну спільноту, яка вітає людей різного походження. + +## Творець – Супроводжувач + +Привіт! 👋 + +Це я: + +{% if people %} +
+{% for user in people.maintainers %} + +
@{{ user.login }}
Answers: {{ user.answers }}
Pull Requests: {{ user.prs }}
+{% endfor %} + +
+{% endif %} + +Я - творець і супроводжувач **FastAPI**. Детальніше про це можна прочитати в [Довідка FastAPI - Отримати довідку - Зв'язатися з автором](help-fastapi.md#connect-with-the-author){.internal-link target=_blank}. + +...Але тут я хочу показати вам спільноту. + +--- + +**FastAPI** отримує велику підтримку від спільноти. І я хочу відзначити їхній внесок. + +Це люди, які: + +* [Допомагають іншим із проблемами (запитаннями) у GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank}. +* [Створюють пул реквести](help-fastapi.md#create-a-pull-request){.internal-link target=_blank}. +* Переглядають пул реквести, [особливо важливо для перекладів](contributing.md#translations){.internal-link target=_blank}. + +Оплески їм. 👏 🙇 + +## Найбільш активні користувачі минулого місяця + +Це користувачі, які [найбільше допомагали іншим із проблемами (запитаннями) у GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} протягом минулого місяця. ☕ + +{% if people %} +
+{% for user in people.last_month_active %} + +
@{{ user.login }}
Issues replied: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## Експерти + +Ось **експерти FastAPI**. 🤓 + +Це користувачі, які [найбільше допомагали іншим із проблемами (запитаннями) у GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} протягом *всього часу*. + +Вони зарекомендували себе як експерти, допомагаючи багатьом іншим. ✨ + +{% if people %} +
+{% for user in people.experts %} + +
@{{ user.login }}
Issues replied: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## Найкращі контрибютори + +Ось **Найкращі контрибютори**. 👷 + +Ці користувачі [створили найбільшу кількість пул реквестів](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} які були *змержені*. + +Вони надали програмний код, документацію, переклади тощо. 📦 + +{% if people %} +
+{% for user in people.top_contributors %} + +
@{{ user.login }}
Pull Requests: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +Є багато інших контрибюторів (більше сотні), їх усіх можна побачити на сторінці FastAPI GitHub Contributors. 👷 + +## Найкращі рецензенти + +Ці користувачі є **Найкращими рецензентами**. 🕵️ + +### Рецензенти на переклади + +Я розмовляю лише кількома мовами (і не дуже добре 😅). Отже, рецензенти – це ті, хто має [**повноваження схвалювати переклади**](contributing.md#translations){.internal-link target=_blank} документації. Без них не було б документації кількома іншими мовами. + +--- + +**Найкращі рецензенти** 🕵️ переглянули більшість пул реквестів від інших, забезпечуючи якість коду, документації і особливо **перекладів**. + +{% if people %} +
+{% for user in people.top_reviewers %} + +
@{{ user.login }}
Reviews: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## Спонсори + +Це **Спонсори**. 😎 + +Вони підтримують мою роботу з **FastAPI** (та іншими), переважно через GitHub Sponsors. + +{% if sponsors %} + +{% if sponsors.gold %} + +### Золоті спонсори + +{% for sponsor in sponsors.gold -%} + +{% endfor %} +{% endif %} + +{% if sponsors.silver %} + +### Срібні спонсори + +{% for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + +{% if sponsors.bronze %} + +### Бронзові спонсори + +{% for sponsor in sponsors.bronze -%} + +{% endfor %} +{% endif %} + +{% endif %} + +### Індивідуальні спонсори + +{% if github_sponsors %} +{% for group in github_sponsors.sponsors %} + +
+ +{% for user in group %} +{% if user.login not in sponsors_badge.logins %} + + + +{% endif %} +{% endfor %} + +
+ +{% endfor %} +{% endif %} + +## Про дані - технічні деталі + +Основна мета цієї сторінки – висвітлити зусилля спільноти, щоб допомогти іншим. + +Особливо враховуючи зусилля, які зазвичай менш помітні, а в багатьох випадках більш важкі, як-от допомога іншим із проблемами та перегляд пул реквестів перекладів. + +Дані розраховуються щомісяця, ви можете ознайомитися з вихідним кодом тут. + +Тут я також підкреслюю внески спонсорів. + +Я також залишаю за собою право оновлювати алгоритми підрахунку, види рейтингів, порогові значення тощо (про всяк випадок 🤷). diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md index cff2c28043..fad693f79d 100644 --- a/docs/uk/docs/index.md +++ b/docs/uk/docs/index.md @@ -1,50 +1,49 @@ - -{!../../../docs/missing-translation.md!} - -

FastAPI

- FastAPI framework, high performance, easy to learn, fast to code, ready for production + Готовий до продакшину, високопродуктивний, простий у вивченні та швидкий для написання коду фреймворк

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

--- -**Documentation**: https://fastapi.tiangolo.com +**Документація**: https://fastapi.tiangolo.com -**Source Code**: https://github.com/tiangolo/fastapi +**Програмний код**: 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. +FastAPI - це сучасний, швидкий (високопродуктивний), вебфреймворк для створення API за допомогою Python 3.8+,в основі якого лежить стандартна анотація типів Python. -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). +* **Швидкий**: Дуже висока продуктивність, на рівні з **NodeJS** та **Go** (завдяки Starlette та Pydantic). [Один із найшвидших фреймворків](#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. +* **Швидке написання коду**: Пришвидшує розробку функціоналу приблизно на 200%-300%. * +* **Менше помилок**: Зменшить кількість помилок спричинених людиною (розробником) на 40%. * +* **Інтуїтивний**: Чудова підтримка редакторами коду. Доповнення всюди. Зменште час на налагодження. +* **Простий**: Спроектований, для легкого використання та навчання. Знадобиться менше часу на читання документації. +* **Короткий**: Зведе до мінімуму дублювання коду. Кожен оголошений параметр може виконувати кілька функцій. +* **Надійний**: Ви матимете стабільний код готовий до продакшину з автоматичною інтерактивною документацією. +* **Стандартизований**: Оснований та повністю сумісний з відкритими стандартами для API: OpenAPI (попередньо відомий як Swagger) та JSON Schema. -* estimation based on tests on an internal development team, building production applications. +* оцінка на основі тестів внутрішньої команди розробників, створення продуктових застосунків. -## Sponsors +## Спонсори @@ -61,7 +60,7 @@ The key features are: Other sponsors -## 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._" @@ -101,24 +100,24 @@ The key features are: --- -## **Typer**, the FastAPI of CLIs +## **Typer**, FastAPI CLI -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. +Створюючи CLI застосунок для використання в терміналі, замість веб-API зверніть увагу на **Typer**. -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 +**Typer** є молодшим братом FastAPI. І це **FastAPI для CLI**. ⌨️ 🚀 -## Requirements +## Вимоги -Python 3.7+ +Python 3.8+ -FastAPI stands on the shoulders of giants: +FastAPI стоїть на плечах гігантів: -* Starlette for the web parts. -* Pydantic for the data parts. +* Starlette для web частини. +* Pydantic для частини даних. -## Installation +## Вставновлення
@@ -130,23 +129,23 @@ $ pip install fastapi
-You will also need an ASGI server, for production such as Uvicorn or Hypercorn. +Вам також знадобиться сервер ASGI для продакшину, наприклад Uvicorn або Hypercorn.
```console -$ pip install "uvicorn[standard]" +$ pip install uvicorn[standard] ---> 100% ```
-## Example +## Приклад -### Create it +### Створіть -* Create a file `main.py` with: +* Створіть файл `main.py` з: ```Python from typing import Union @@ -167,9 +166,9 @@ def read_item(item_id: int, q: Union[str, None] = None): ```
-Or use async def... +Або використайте async def... -If your code uses `async` / `await`, use `async def`: +Якщо ваш код використовує `async` / `await`, скористайтеся `async def`: ```Python hl_lines="9 14" from typing import Union @@ -189,15 +188,15 @@ async def read_item(item_id: int, q: Union[str, None] = 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. +Стикнувшись з проблемами, не зайвим буде ознайомитися з розділом _"In a hurry?"_ про `async` та `await` у документації.
-### Run it +### Запустіть -Run the server with: +Запустіть server з:
@@ -214,54 +213,54 @@ INFO: Application startup complete.
-About the command uvicorn main:app --reload... +Про команди uvicorn main:app --reload... -The command `uvicorn main:app` refers to: +Команда `uvicorn main:app` посилається на: -* `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. +* `main`: файл `main.py` ("Модуль" Python). +* `app`: об’єкт створений усередині `main.py` рядком `app = FastAPI()`. +* `--reload`: перезапускає сервер після зміни коду. Використовуйте виключно для розробки.
-### Check it +### Перевірте -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. +Відкрийте браузер та введіть адресу http://127.0.0.1:8000/items/5?q=somequery. -You will see the JSON response as: +Ви побачите у відповідь подібний JSON: ```JSON {"item_id": 5, "q": "somequery"} ``` -You already created an API that: +Ви вже створили API, який: -* 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`. +* Отримує HTTP запити за _шляхами_ `/` та `/items/{item_id}`. +* Обидва _шляхи_ приймають `GET` операції (також відомі як HTTP _методи_). +* _Шлях_ `/items/{item_id}` містить _параметр шляху_ `item_id` який має бути типу `int`. +* _Шлях_ `/items/{item_id}` містить необовʼязковий `str` _параметр запиту_ `q`. -### Interactive API docs +### Інтерактивні документації API -Now go to http://127.0.0.1:8000/docs. +Перейдемо сюди http://127.0.0.1:8000/docs. -You will see the automatic interactive API documentation (provided by Swagger UI): +Ви побачите автоматичну інтерактивну API документацію (створену завдяки Swagger UI): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternative API docs +### Альтернативні документації API -And now, go to http://127.0.0.1:8000/redoc. +Тепер перейдемо сюди http://127.0.0.1:8000/redoc. -You will see the alternative automatic documentation (provided by ReDoc): +Ви побачите альтернативну автоматичну документацію (створену завдяки 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. +Тепер модифікуйте файл `main.py`, щоб отримати вміст запиту `PUT`. -Declare the body using standard Python types, thanks to Pydantic. +Оголошуйте вміст запиту за допомогою стандартних типів Python завдяки Pydantic. ```Python hl_lines="4 9-12 25-27" from typing import Union @@ -293,174 +292,174 @@ 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). +Сервер повинен автоматично перезавантажуватися (тому що Ви додали `--reload` до `uvicorn` команди вище). -### Interactive API docs upgrade +### Оновлення інтерактивної API документації -Now go to http://127.0.0.1:8000/docs. +Тепер перейдемо сюди http://127.0.0.1:8000/docs. -* The interactive API documentation will be automatically updated, including the new body: +* Інтерактивна документація API буде автоматично оновлена, включаючи новий вміст: ![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: +* Натисніть кнопку "Try it out", це дозволить вам заповнити параметри та безпосередньо взаємодіяти з 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: +* Потім натисніть кнопку "Execute", інтерфейс користувача зв'яжеться з вашим API, надішле параметри, у відповідь отримає результати та покаже їх на екрані: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Alternative API docs upgrade +### Оновлення альтернативної API документації -And now, go to http://127.0.0.1:8000/redoc. +Зараз перейдемо 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. +Ви робите це за допомогою стандартних сучасних типів Python. -You don't have to learn a new syntax, the methods or classes of a specific library, etc. +Вам не потрібно вивчати новий синтаксис, методи чи класи конкретної бібліотеки тощо. -Just standard **Python 3.6+**. +Використовуючи стандартний **Python 3.8+**. -For example, for an `int`: +Наприклад, для `int`: ```Python item_id: int ``` -or for a more complex `Item` model: +або для більш складної моделі `Item`: ```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 з високим рівнем вкладеності. +* Перетворення вхідних даних: з мережі до даних і типів Python. Читання з: * 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: + * Файлів. +* Перетворення вихідних даних: з типів і даних Python до мережевих даних (як JSON): + * Конвертація Python типів (`str`, `int`, `float`, `bool`, `list`, тощо). + * `datetime` об'єкти. + * `UUID` об'єкти. + * Моделі бази даних. + * ...та багато іншого. +* Автоматичну інтерактивну документацію API, включаючи 2 альтернативні інтерфейси користувача: * Swagger UI. * ReDoc. --- -Coming back to the previous code example, **FastAPI** will: +Повертаючись до попереднього прикладу коду, **FastAPI**: -* 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. +* Підтвердить наявність `item_id` у шляху для запитів `GET` та `PUT`. +* Підтвердить, що `item_id` має тип `int` для запитів `GET` and `PUT`. + * Якщо це не так, клієнт побачить корисну, зрозумілу помилку. +* Перевірить, чи є необов'язковий параметр запиту з назвою `q` (а саме `http://127.0.0.1:8000/items/foo?q=somequery`) для запитів `GET`. + * Оскільки параметр `q` оголошено як `= None`, він необов'язковий. + * За відсутності `None` він був би обов'язковим (як і вміст у випадку з `PUT`). +* Для запитів `PUT` із `/items/{item_id}`, читає вміст як JSON: + * Перевірить, чи має обов'язковий атрибут `name` тип `str`. + * Перевірить, чи має обов'язковий атрибут `price` тип `float`. + * Перевірить, чи існує необов'язковий атрибут `is_offer` та чи має він тип `bool`. + * Усе це також працюватиме для глибоко вкладених об'єктів JSON. +* Автоматично конвертує із та в JSON. +* Документує все за допомогою OpenAPI, який може бути використано в: + * Інтерактивних системах документації. + * Системах автоматичної генерації клієнтського коду для багатьох мов. +* Надає безпосередньо 2 вебінтерфейси інтерактивної документації. --- -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: +**Spoiler alert**: туторіал - посібник користувача містить: -* 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: +* Оголошення **параметрів** з інших місць як: **headers**, **cookies**, **form fields** та **files**. +* Як встановити **перевірку обмежень** як `maximum_length` або `regex`. +* Дуже потужна і проста у використанні система **Ін'єкція Залежностей**. +* Безпека та автентифікація, включаючи підтримку **OAuth2** з **JWT tokens** та **HTTP Basic** автентифікацію. +* Досконаліші (але однаково прості) техніки для оголошення **глибоко вкладених моделей JSON** (завдяки Pydantic). +* Багато додаткових функцій (завдяки Starlette) як-от: * **WebSockets** - * **GraphQL** - * extremely easy tests based on HTTPX and `pytest` + * надзвичайно прості тести на основі HTTPX та `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). (*) +Незалежні тести TechEmpower показують що застосунки **FastAPI**, які працюють під керуванням Uvicorn є одними з найшвидших серед доступних фреймворків в Python, поступаючись лише Starlette та Uvicorn (які внутрішньо використовуються в FastAPI). (*) -To understand more about it, see the section Benchmarks. +Щоб дізнатися більше про це, перегляньте розділ Benchmarks. -## Optional Dependencies +## Необов'язкові залежності -Used by Pydantic: +Pydantic використовує: -* ujson - for faster JSON "parsing". -* email_validator - for email validation. +* email_validator - для валідації електронної пошти. +* pydantic-settings - для управління налаштуваннями. +* pydantic-extra-types - для додаткових типів, що можуть бути використані з Pydantic. -Used by Starlette: -* httpx - Required if you want to use the `TestClient`. -* 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`. +Starlette використовує: -Used by FastAPI / Starlette: +* httpx - Необхідно, якщо Ви хочете використовувати `TestClient`. +* jinja2 - Необхідно, якщо Ви хочете використовувати шаблони як конфігурацію за замовчуванням. +* python-multipart - Необхідно, якщо Ви хочете підтримувати "розбір" форми за допомогою `request.form()`. +* itsdangerous - Необхідно для підтримки `SessionMiddleware`. +* pyyaml - Необхідно для підтримки Starlette `SchemaGenerator` (ймовірно, вам це не потрібно з FastAPI). +* ujson - Необхідно, якщо Ви хочете використовувати `UJSONResponse`. -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. +FastAPI / Starlette використовують: -You can install all of these with `pip install fastapi[all]`. +* uvicorn - для сервера, який завантажує та обслуговує вашу програму. +* orjson - Необхідно, якщо Ви хочете використовувати `ORJSONResponse`. -## License +Ви можете встановити все це за допомогою `pip install fastapi[all]`. -This project is licensed under the terms of the MIT license. +## Ліцензія + +Цей проєкт ліцензовано згідно з умовами ліцензії MIT. diff --git a/docs/uk/docs/python-types.md b/docs/uk/docs/python-types.md new file mode 100644 index 0000000000..6c8e290168 --- /dev/null +++ b/docs/uk/docs/python-types.md @@ -0,0 +1,448 @@ +# Вступ до типів Python + +Python підтримує додаткові "підказки типу" ("type hints") (також звані "анотаціями типу" ("type annotations")). + +Ці **"type hints"** є спеціальним синтаксисом, що дозволяє оголошувати тип змінної. + +За допомогою оголошення типів для ваших змінних, редактори та інструменти можуть надати вам кращу підтримку. + +Це просто **швидкий посібник / нагадування** про анотації типів у Python. Він покриває лише мінімум, необхідний щоб використовувати їх з **FastAPI**... що насправді дуже мало. + +**FastAPI** повністю базується на цих анотаціях типів, вони дають йому багато переваг. + +Але навіть якщо ви ніколи не використаєте **FastAPI**, вам буде корисно дізнатись трохи про них. + +!!! note + Якщо ви експерт у Python і ви вже знаєте усе про анотації типів - перейдіть до наступного розділу. + +## Мотивація + +Давайте почнемо з простого прикладу: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +Виклик цієї програми виводить: + +``` +John Doe +``` + +Функція виконує наступне: + +* Бере `first_name` та `last_name`. +* Конвертує кожну літеру кожного слова у верхній регістр за допомогою `title()`. +* Конкатенує їх разом із пробілом по середині. + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### Редагуйте це + +Це дуже проста програма. + +Але тепер уявіть, що ви писали це з нуля. + +У певний момент ви розпочали б визначення функції, у вас були б готові параметри... + +Але тоді вам потрібно викликати "той метод, який переводить першу літеру у верхній регістр". + +Це буде `upper`? Чи `uppercase`? `first_uppercase`? `capitalize`? + +Тоді ви спробуєте давнього друга програміста - автозаповнення редактора коду. + +Ви надрукуєте перший параметр функції, `first_name`, тоді крапку (`.`), а тоді натиснете `Ctrl+Space`, щоб запустити автозаповнення. + +Але, на жаль, ви не отримаєте нічого корисного: + + + +### Додайте типи + +Давайте змінимо один рядок з попередньої версії. + +Ми змінимо саме цей фрагмент, параметри функції, з: + +```Python + first_name, last_name +``` + +на: + +```Python + first_name: str, last_name: str +``` + +Ось і все. + +Це "type hints": + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +Це не те саме, що оголошення значень за замовчуванням, як це було б з: + +```Python + first_name="john", last_name="doe" +``` + +Це зовсім інше. + +Ми використовуємо двокрапку (`:`), не дорівнює (`=`). + +І додавання анотації типу зазвичай не змінює того, що сталось би без них. + +Але тепер, уявіть що ви посеред процесу створення функції, але з анотаціями типів. + +В цей же момент, ви спробуєте викликати автозаповнення з допомогою `Ctrl+Space` і побачите: + + + +Разом з цим, ви можете прокручувати, переглядати опції, допоки ви не знайдете одну, що звучить схоже: + + + +## Більше мотивації + +Перевірте цю функцію, вона вже має анотацію типу: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +Оскільки редактор знає типи змінних, ви не тільки отримаєте автозаповнення, ви також отримаєте перевірку помилок: + + + +Тепер ви знаєте, щоб виправити це, вам потрібно перетворити `age` у строку з допомогою `str(age)`: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## Оголошення типів + +Щойно ви побачили основне місце для оголошення анотацій типу. Як параметри функції. + +Це також основне місце, де ви б їх використовували у **FastAPI**. + +### Прості типи + +Ви можете оголошувати усі стандартні типи у Python, не тільки `str`. + +Ви можете використовувати, наприклад: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### Generic-типи з параметрами типів + +Існують деякі структури даних, які можуть містити інші значення, наприклад `dict`, `list`, `set` та `tuple`. І внутрішні значення також можуть мати свій тип. + +Ці типи, які мають внутрішні типи, називаються "**generic**" типами. І оголосити їх можна навіть із внутрішніми типами. + +Щоб оголосити ці типи та внутрішні типи, ви можете використовувати стандартний модуль Python `typing`. Він існує спеціально для підтримки анотацій типів. + +#### Новіші версії Python + +Синтаксис із використанням `typing` **сумісний** з усіма версіями, від Python 3.6 до останніх, включаючи Python 3.9, Python 3.10 тощо. + +У міру розвитку Python **новіші версії** мають покращену підтримку анотацій типів і в багатьох випадках вам навіть не потрібно буде імпортувати та використовувати модуль `typing` для оголошення анотацій типу. + +Якщо ви можете вибрати новішу версію Python для свого проекту, ви зможете скористатися цією додатковою простотою. Дивіться кілька прикладів нижче. + +#### List (список) + +Наприклад, давайте визначимо змінну, яка буде `list` із `str`. + +=== "Python 3.8 і вище" + + З модуля `typing`, імпортуємо `List` (з великої літери `L`): + + ``` Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + + Оголосимо змінну з тим самим синтаксисом двокрапки (`:`). + + Як тип вкажемо `List`, який ви імпортували з `typing`. + + Оскільки список є типом, який містить деякі внутрішні типи, ви поміщаєте їх у квадратні дужки: + + ```Python hl_lines="4" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + +=== "Python 3.9 і вище" + + Оголосимо змінну з тим самим синтаксисом двокрапки (`:`). + + Як тип вкажемо `list`. + + Оскільки список є типом, який містить деякі внутрішні типи, ви поміщаєте їх у квадратні дужки: + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006_py39.py!} + ``` + +!!! info + Ці внутрішні типи в квадратних дужках називаються "параметрами типу". + + У цьому випадку, `str` це параметр типу переданий у `List` (або `list` у Python 3.9 і вище). + +Це означає: "змінна `items` це `list`, і кожен з елементів у цьому списку - `str`". + +!!! tip + Якщо ви використовуєте Python 3.9 і вище, вам не потрібно імпортувати `List` з `typing`, ви можете використовувати натомість тип `list`. + +Зробивши це, ваш редактор може надати підтримку навіть під час обробки елементів зі списку: + + + +Без типів цього майже неможливо досягти. + +Зверніть увагу, що змінна `item` є одним із елементів у списку `items`. + +І все ж редактор знає, що це `str`, і надає підтримку для цього. + +#### Tuple and Set (кортеж та набір) + +Ви повинні зробити те ж саме, щоб оголосити `tuple` і `set`: + +=== "Python 3.8 і вище" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial007.py!} + ``` + +=== "Python 3.9 і вище" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial007_py39.py!} + ``` + +Це означає: + +* Змінна `items_t` це `tuple` з 3 елементами, `int`, ще `int`, та `str`. +* Змінна `items_s` це `set`, і кожен його елемент типу `bytes`. + +#### Dict (словник) + +Щоб оголосити `dict`, вам потрібно передати 2 параметри типу, розділені комами. + +Перший параметр типу для ключа у `dict`. + +Другий параметр типу для значення у `dict`: + +=== "Python 3.8 і вище" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008.py!} + ``` + +=== "Python 3.9 і вище" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008_py39.py!} + ``` + +Це означає: + +* Змінна `prices` це `dict`: + * Ключі цього `dict` типу `str` (наприклад, назва кожного елементу). + * Значення цього `dict` типу `float` (наприклад, ціна кожного елементу). + +#### Union (об'єднання) + +Ви можете оголосити, що змінна може бути будь-яким із **кількох типів**, наприклад, `int` або `str`. + +У Python 3.6 і вище (включаючи Python 3.10) ви можете використовувати тип `Union` з `typing` і вставляти в квадратні дужки можливі типи, які можна прийняти. + +У Python 3.10 також є **альтернативний синтаксис**, у якому ви можете розділити можливі типи за допомогою вертикальної смуги (`|`). + +=== "Python 3.8 і вище" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008b.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008b_py310.py!} + ``` + +В обох випадках це означає, що `item` може бути `int` або `str`. + +#### Possibly `None` (Optional) + +Ви можете оголосити, що значення може мати тип, наприклад `str`, але також може бути `None`. + +У Python 3.6 і вище (включаючи Python 3.10) ви можете оголосити його, імпортувавши та використовуючи `Optional` з модуля `typing`. + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +Використання `Optional[str]` замість просто `str` дозволить редактору допомогти вам виявити помилки, коли ви могли б вважати, що значенням завжди є `str`, хоча насправді воно також може бути `None`. + +`Optional[Something]` насправді є скороченням для `Union[Something, None]`, вони еквівалентні. + +Це також означає, що в Python 3.10 ви можете використовувати `Something | None`: + +=== "Python 3.8 і вище" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009.py!} + ``` + +=== "Python 3.8 і вище - альтернатива" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009b.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial009_py310.py!} + ``` + +#### Generic типи + +Ці типи, які приймають параметри типу у квадратних дужках, називаються **Generic types** or **Generics**, наприклад: + +=== "Python 3.8 і вище" + + * `List` + * `Tuple` + * `Set` + * `Dict` + * `Union` + * `Optional` + * ...та інші. + +=== "Python 3.9 і вище" + + Ви можете використовувати ті самі вбудовані типи, як generic (з квадратними дужками та типами всередині): + + * `list` + * `tuple` + * `set` + * `dict` + + І те саме, що й у Python 3.8, із модуля `typing`: + + * `Union` + * `Optional` + * ...та інші. + +=== "Python 3.10 і вище" + + Ви можете використовувати ті самі вбудовані типи, як generic (з квадратними дужками та типами всередині): + + * `list` + * `tuple` + * `set` + * `dict` + + І те саме, що й у Python 3.8, із модуля `typing`: + + * `Union` + * `Optional` (так само як у Python 3.8) + * ...та інші. + + У Python 3.10, як альтернатива використанню `Union` та `Optional`, ви можете використовувати вертикальну смугу (`|`) щоб оголосити об'єднання типів. + +### Класи як типи + +Ви також можете оголосити клас як тип змінної. + +Скажімо, у вас є клас `Person` з імʼям: + +```Python hl_lines="1-3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Потім ви можете оголосити змінну типу `Person`: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +І знову ж таки, ви отримуєте всю підтримку редактора: + + + +## Pydantic моделі + +Pydantic це бібліотека Python для валідації даних. + +Ви оголошуєте «форму» даних як класи з атрибутами. + +І кожен атрибут має тип. + +Потім ви створюєте екземпляр цього класу з деякими значеннями, і він перевірить ці значення, перетворить їх у відповідний тип (якщо є потреба) і надасть вам об’єкт з усіма даними. + +І ви отримуєте всю підтримку редактора з цим отриманим об’єктом. + +Приклад з документації Pydantic: + +=== "Python 3.8 і вище" + + ```Python + {!> ../../../docs_src/python_types/tutorial011.py!} + ``` + +=== "Python 3.9 і вище" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py39.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py310.py!} + ``` + +!!! info + Щоб дізнатись більше про Pydantic, перегляньте його документацію. + +**FastAPI** повністю базується на Pydantic. + +Ви побачите набагато більше цього всього на практиці в [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. + +## Анотації типів у **FastAPI** + +**FastAPI** використовує ці підказки для виконання кількох речей. + +З **FastAPI** ви оголошуєте параметри з підказками типу, і отримуєте: + +* **Підтримку редактора**. +* **Перевірку типів**. + +...і **FastAPI** використовує ті самі оголошення для: + +* **Визначення вимог**: з параметрів шляху запиту, параметрів запиту, заголовків, тіл, залежностей тощо. +* **Перетворення даних**: із запиту в необхідний тип. +* **Перевірка даних**: що надходять від кожного запиту: + * Генерування **автоматичних помилок**, що повертаються клієнту, коли дані недійсні. +* **Документування** API за допомогою OpenAPI: + * який потім використовується для автоматичної інтерактивної документації користувальницьких інтерфейсів. + +Все це може здатися абстрактним. Не хвилюйтеся. Ви побачите все це в дії в [Туторіал - Посібник користувача](tutorial/index.md){.internal-link target=_blank}. + +Важливо те, що за допомогою стандартних типів Python в одному місці (замість того, щоб додавати більше класів, декораторів тощо), **FastAPI** зробить багато роботи за вас. + +!!! info + Якщо ви вже пройшли весь навчальний посібник і повернулися, щоб дізнатися більше про типи, ось хороший ресурс "шпаргалка" від `mypy`. diff --git a/docs/uk/docs/tutorial/body-fields.md b/docs/uk/docs/tutorial/body-fields.md new file mode 100644 index 0000000000..eee993cbe3 --- /dev/null +++ b/docs/uk/docs/tutorial/body-fields.md @@ -0,0 +1,116 @@ +# Тіло - Поля + +Так само як ви можете визначати додаткову валідацію та метадані у параметрах *функції обробки шляху* за допомогою `Query`, `Path` та `Body`, ви можете визначати валідацію та метадані всередині моделей Pydantic за допомогою `Field` від Pydantic. + +## Імпорт `Field` + +Спочатку вам потрібно імпортувати це: + +=== "Python 3.10+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Варто користуватись `Annotated` версією, якщо це можливо. + + ```Python hl_lines="2" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Варто користуватись `Annotated` версією, якщо це можливо. + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +!!! warning + Зверніть увагу, що `Field` імпортується прямо з `pydantic`, а не з `fastapi`, як всі інші (`Query`, `Path`, `Body` тощо). + +## Оголошення атрибутів моделі + +Ви можете використовувати `Field` з атрибутами моделі: + +=== "Python 3.10+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12-15" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Варто користуватись `Annotated` версією, якщо це можливо.. + + ```Python hl_lines="9-12" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Варто користуватись `Annotated` версією, якщо це можливо.. + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +`Field` працює так само, як `Query`, `Path` і `Body`, у нього такі самі параметри тощо. + +!!! note "Технічні деталі" + Насправді, `Query`, `Path` та інші, що ви побачите далі, створюють об'єкти підкласів загального класу `Param`, котрий сам є підкласом класу `FieldInfo` з Pydantic. + + І `Field` від Pydantic також повертає екземпляр `FieldInfo`. + + `Body` також безпосередньо повертає об'єкти підкласу `FieldInfo`. І є інші підкласи, які ви побачите пізніше, що є підкласами класу Body. + + Пам'ятайте, що коли ви імпортуєте 'Query', 'Path' та інше з 'fastapi', вони фактично є функціями, які повертають спеціальні класи. + +!!! tip + Зверніть увагу, що кожен атрибут моделі із типом, значенням за замовчуванням та `Field` має ту саму структуру, що й параметр *функції обробки шляху*, з `Field` замість `Path`, `Query` і `Body`. + +## Додавання додаткової інформації + +Ви можете визначити додаткову інформацію у `Field`, `Query`, `Body` тощо. І вона буде включена у згенеровану JSON схему. + +Ви дізнаєтеся більше про додавання додаткової інформації пізніше у документації, коли вивчатимете визначення прикладів. + +!!! warning + Додаткові ключі, передані в `Field`, також будуть присутні у згенерованій схемі OpenAPI для вашого додатка. + Оскільки ці ключі не обов'язково можуть бути частиною специфікації OpenAPI, деякі інструменти OpenAPI, наприклад, [OpenAPI валідатор](https://validator.swagger.io/), можуть не працювати з вашою згенерованою схемою. + +## Підсумок + +Ви можете використовувати `Field` з Pydantic для визначення додаткових перевірок та метаданих для атрибутів моделі. + +Ви також можете використовувати додаткові іменовані аргументи для передачі додаткових метаданих JSON схеми. diff --git a/docs/uk/docs/tutorial/body.md b/docs/uk/docs/tutorial/body.md new file mode 100644 index 0000000000..9759e7f450 --- /dev/null +++ b/docs/uk/docs/tutorial/body.md @@ -0,0 +1,213 @@ +# Тіло запиту + +Коли вам потрібно надіслати дані з клієнта (скажімо, браузера) до вашого API, ви надсилаєте їх як **тіло запиту**. + +Тіло **запиту** — це дані, надіслані клієнтом до вашого API. Тіло **відповіді** — це дані, які ваш API надсилає клієнту. + +Ваш API майже завжди має надсилати тіло **відповіді**. Але клієнтам не обов’язково потрібно постійно надсилати тіла **запитів**. + +Щоб оголосити тіло **запиту**, ви використовуєте Pydantic моделі з усією їх потужністю та перевагами. + +!!! info + Щоб надіслати дані, ви повинні використовувати один із: `POST` (більш поширений), `PUT`, `DELETE` або `PATCH`. + + Надсилання тіла із запитом `GET` має невизначену поведінку в специфікаціях, проте воно підтримується FastAPI лише для дуже складних/екстремальних випадків використання. + + Оскільки це не рекомендується, інтерактивна документація з Swagger UI не відображатиме документацію для тіла запиту під час використання `GET`, і проксі-сервери в середині можуть не підтримувати її. + +## Імпортуйте `BaseModel` від Pydantic + +Спочатку вам потрібно імпортувати `BaseModel` з `pydantic`: + +=== "Python 3.8 і вище" + + ```Python hl_lines="4" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="2" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +## Створіть свою модель даних + +Потім ви оголошуєте свою модель даних як клас, який успадковується від `BaseModel`. + +Використовуйте стандартні типи Python для всіх атрибутів: + +=== "Python 3.8 і вище" + + ```Python hl_lines="7-11" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="5-9" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +Так само, як і при оголошенні параметрів запиту, коли атрибут моделі має значення за замовчуванням, він не є обов’язковим. В іншому випадку це потрібно. Використовуйте `None`, щоб зробити його необов'язковим. + +Наприклад, ця модель вище оголошує JSON "`об'єкт`" (або Python `dict`), як: + +```JSON +{ + "name": "Foo", + "description": "An optional description", + "price": 45.2, + "tax": 3.5 +} +``` + +...оскільки `description` і `tax` є необов'язковими (зі значенням за замовчуванням `None`), цей JSON "`об'єкт`" також буде дійсним: + +```JSON +{ + "name": "Foo", + "price": 45.2 +} +``` + +## Оголоси її як параметр + +Щоб додати модель даних до вашої *операції шляху*, оголосіть її так само, як ви оголосили параметри шляху та запиту: + +=== "Python 3.8 і вище" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +...і вкажіть її тип як модель, яку ви створили, `Item`. + +## Результати + +Лише з цим оголошенням типу Python **FastAPI** буде: + +* Читати тіло запиту як JSON. +* Перетворювати відповідні типи (якщо потрібно). +* Валідувати дані. + * Якщо дані недійсні, він поверне гарну та чітку помилку, вказуючи, де саме і які дані були неправильними. +* Надавати отримані дані у параметрі `item`. + * Оскільки ви оголосили його у функції як тип `Item`, ви також матимете всю підтримку редактора (автозаповнення, тощо) для всіх атрибутів та їх типів. +* Генерувати JSON Schema визначення для вашої моделі, ви також можете використовувати їх де завгодно, якщо це має сенс для вашого проекту. +* Ці схеми будуть частиною згенерованої схеми OpenAPI і використовуватимуться автоматичною документацією інтерфейсу користувача. + +## Автоматична документація + +Схеми JSON ваших моделей будуть частиною вашої схеми, згенерованої OpenAPI, і будуть показані в інтерактивній API документації: + + + +А також використовуватимуться в API документації всередині кожної *операції шляху*, якій вони потрібні: + + + +## Підтримка редактора + +У вашому редакторі, всередині вашої функції, ви будете отримувати підказки типу та завершення скрізь (це б не сталося, якби ви отримали `dict` замість моделі Pydantic): + + + +Ви також отримуєте перевірку помилок на наявність операцій з неправильним типом: + + + +Це не випадково, весь каркас був побудований навколо цього дизайну. + +І він був ретельно перевірений на етапі проектування, перед будь-яким впровадженням, щоб переконатися, що він працюватиме з усіма редакторами. + +Були навіть деякі зміни в самому Pydantic, щоб підтримати це. + +Попередні скріншоти були зроблені у Visual Studio Code. + +Але ви отримаєте ту саму підтримку редактора у PyCharm та більшість інших редакторів Python: + + + +!!! tip + Якщо ви використовуєте PyCharm як ваш редактор, ви можете використати Pydantic PyCharm Plugin. + + Він покращує підтримку редакторів для моделей Pydantic за допомогою: + + * автозаповнення + * перевірки типу + * рефакторингу + * пошуку + * інспекції + +## Використовуйте модель + +Усередині функції ви можете отримати прямий доступ до всіх атрибутів об’єкта моделі: + +=== "Python 3.8 і вище" + + ```Python hl_lines="21" + {!> ../../../docs_src/body/tutorial002.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="19" + {!> ../../../docs_src/body/tutorial002_py310.py!} + ``` + +## Тіло запиту + параметри шляху + +Ви можете одночасно оголошувати параметри шляху та тіло запиту. + +**FastAPI** розпізнає, що параметри функції, які відповідають параметрам шляху, мають бути **взяті з шляху**, а параметри функції, які оголошуються як моделі Pydantic, **взяті з тіла запиту**. + +=== "Python 3.8 і вище" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="15-16" + {!> ../../../docs_src/body/tutorial003_py310.py!} + ``` + +## Тіло запиту + шлях + параметри запиту + +Ви також можете оголосити параметри **тіло**, **шлях** і **запит** одночасно. + +**FastAPI** розпізнає кожен з них і візьме дані з потрібного місця. + +=== "Python 3.8 і вище" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial004_py310.py!} + ``` + +Параметри функції будуть розпізнаватися наступним чином: + +* Якщо параметр також оголошено в **шляху**, він використовуватиметься як параметр шляху. +* Якщо параметр має **сингулярний тип** (наприклад, `int`, `float`, `str`, `bool` тощо), він буде інтерпретуватися як параметр **запиту**. +* Якщо параметр оголошується як тип **Pydantic моделі**, він інтерпретується як **тіло** запиту. + +!!! note + FastAPI буде знати, що значення "q" не є обов'язковим через значення за замовчуванням "= None". + + `Optional` у `Optional[str]` не використовується FastAPI, але дозволить вашому редактору надати вам кращу підтримку та виявляти помилки. + +## Без Pydantic + +Якщо ви не хочете використовувати моделі Pydantic, ви також можете використовувати параметри **Body**. Перегляньте документацію для [Тіло – Кілька параметрів: сингулярні значення в тілі](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. diff --git a/docs/uk/docs/tutorial/cookie-params.md b/docs/uk/docs/tutorial/cookie-params.md new file mode 100644 index 0000000000..199b938397 --- /dev/null +++ b/docs/uk/docs/tutorial/cookie-params.md @@ -0,0 +1,96 @@ +# Параметри Cookie + +Ви можете визначити параметри Cookie таким же чином, як визначаються параметри `Query` і `Path`. + +## Імпорт `Cookie` + +Спочатку імпортуйте `Cookie`: + +=== "Python 3.10+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="1" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +## Визначення параметрів `Cookie` + +Потім визначте параметри cookie, використовуючи таку ж конструкцію як для `Path` і `Query`. + +Перше значення це значення за замовчуванням, ви можете також передати всі додаткові параметри валідації чи анотації: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="7" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +!!! note "Технічні Деталі" + `Cookie` це "сестра" класів `Path` і `Query`. Вони наслідуються від одного батьківського класу `Param`. + Але пам'ятайте, що коли ви імпортуєте `Query`, `Path`, `Cookie` та інше з `fastapi`, це фактично функції, що повертають спеціальні класи. + +!!! info + Для визначення cookies ви маєте використовувати `Cookie`, тому що в іншому випадку параметри будуть інтерпритовані, як параметри запиту. + +## Підсумки + +Визначайте cookies за допомогою `Cookie`, використовуючи той же спільний шаблон, що і `Query` та `Path`. diff --git a/docs/uk/docs/tutorial/encoder.md b/docs/uk/docs/tutorial/encoder.md new file mode 100644 index 0000000000..b6583341f3 --- /dev/null +++ b/docs/uk/docs/tutorial/encoder.md @@ -0,0 +1,42 @@ +# JSON Compatible Encoder + +Існують випадки, коли вам може знадобитися перетворити тип даних (наприклад, модель Pydantic) в щось сумісне з JSON (наприклад, `dict`, `list`, і т. д.). + +Наприклад, якщо вам потрібно зберегти це в базі даних. + +Для цього, **FastAPI** надає `jsonable_encoder()` функцію. + +## Використання `jsonable_encoder` + +Давайте уявимо, що у вас є база даних `fake_db`, яка приймає лише дані, сумісні з JSON. + +Наприклад, вона не приймає об'єкти типу `datetime`, оскільки вони не сумісні з JSON. + +Отже, об'єкт типу `datetime` потрібно перетворити в рядок `str`, який містить дані в ISO форматі. + +Тим самим способом ця база даних не прийматиме об'єкт типу Pydantic model (об'єкт з атрибутами), а лише `dict`. + +Ви можете використовувати `jsonable_encoder` для цього. + +Вона приймає об'єкт, такий як Pydantic model, і повертає його версію, сумісну з JSON: + +=== "Python 3.10+" + + ```Python hl_lines="4 21" + {!> ../../../docs_src/encoder/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + +У цьому прикладі вона конвертує Pydantic model у `dict`, а `datetime` у `str`. + +Результат виклику цієї функції - це щось, що можна кодувати з використанням стандарту Python `json.dumps()`. + +Вона не повертає велику строку `str`, яка містить дані у форматі JSON (як строка). Вона повертає стандартну структуру даних Python (наприклад `dict`) із значеннями та підзначеннями, які є сумісними з JSON. + +!!! Примітка + `jsonable_encoder` фактично використовується **FastAPI** внутрішньо для перетворення даних. Проте вона корисна в багатьох інших сценаріях. diff --git a/docs/uk/docs/tutorial/extra-data-types.md b/docs/uk/docs/tutorial/extra-data-types.md new file mode 100644 index 0000000000..ec5ec0d18f --- /dev/null +++ b/docs/uk/docs/tutorial/extra-data-types.md @@ -0,0 +1,130 @@ +# Додаткові типи даних + +До цього часу, ви використовували загальнопоширені типи даних, такі як: + +* `int` +* `float` +* `str` +* `bool` + +Але можна також використовувати більш складні типи даних. + +І ви все ще матимете ті ж можливості, які були показані до цього: + +* Чудова підтримка редактора. +* Конвертація даних з вхідних запитів. +* Конвертація даних для відповіді. +* Валідація даних. +* Автоматична анотація та документація. + +## Інші типи даних + +Ось додаткові типи даних для використання: + +* `UUID`: + * Стандартний "Універсальний Унікальний Ідентифікатор", який часто використовується як ідентифікатор у багатьох базах даних та системах. + * У запитах та відповідях буде представлений як `str`. +* `datetime.datetime`: + * Пайтонівський `datetime.datetime`. + * У запитах та відповідях буде представлений як `str` в форматі ISO 8601, як: `2008-09-15T15:53:00+05:00`. +* `datetime.date`: + * Пайтонівський `datetime.date`. + * У запитах та відповідях буде представлений як `str` в форматі ISO 8601, як: `2008-09-15`. +* `datetime.time`: + * Пайтонівський `datetime.time`. + * У запитах та відповідях буде представлений як `str` в форматі ISO 8601, як: `14:23:55.003`. +* `datetime.timedelta`: + * Пайтонівський `datetime.timedelta`. + * У запитах та відповідях буде представлений як `float` загальної кількості секунд. + * Pydantic також дозволяє представляти це як "ISO 8601 time diff encoding", більше інформації дивись у документації. +* `frozenset`: + * У запитах і відповідях це буде оброблено так само, як і `set`: + * У запитах список буде зчитано, дублікати будуть видалені та він буде перетворений на `set`. + * У відповідях, `set` буде перетворений на `list`. + * Згенерована схема буде вказувати, що значення `set` є унікальними (з використанням JSON Schema's `uniqueItems`). +* `bytes`: + * Стандартний Пайтонівський `bytes`. + * У запитах і відповідях це буде оброблено як `str`. + * Згенерована схема буде вказувати, що це `str` з "форматом" `binary`. +* `Decimal`: + * Стандартний Пайтонівський `Decimal`. + * У запитах і відповідях це буде оброблено так само, як і `float`. +* Ви можете перевірити всі дійсні типи даних Pydantic тут: типи даних Pydantic. + +## Приклад + +Ось приклад *path operation* з параметрами, використовуючи деякі з вищезазначених типів. + +=== "Python 3.10+" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 3 13-17" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="1 2 11-15" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="1 2 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +Зверніть увагу, що параметри всередині функції мають свій звичайний тип даних, і ви можете, наприклад, виконувати звичайні маніпуляції з датами, такі як: + +=== "Python 3.10+" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="19-20" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="17-18" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` diff --git a/docs/uk/docs/tutorial/index.md b/docs/uk/docs/tutorial/index.md new file mode 100644 index 0000000000..e5bae74bc9 --- /dev/null +++ b/docs/uk/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Туторіал - Посібник користувача + +У цьому посібнику показано, як користуватися **FastAPI** з більшістю його функцій, крок за кроком. + +Кожен розділ поступово надбудовується на попередні, але він структурований на окремі теми, щоб ви могли перейти безпосередньо до будь-якої конкретної, щоб вирішити ваші конкретні потреби API. + +Він також створений як довідник для роботи у майбутньому. + +Тож ви можете повернутися і побачити саме те, що вам потрібно. + +## Запустіть код + +Усі блоки коду можна скопіювати та використовувати безпосередньо (це фактично перевірені файли Python). + +Щоб запустити будь-який із прикладів, скопіюйте код у файл `main.py` і запустіть `uvicorn` за допомогою: + +
+ +```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. +``` + +
+ +**ДУЖЕ радимо** написати або скопіювати код, відредагувати його та запустити локально. + +Використання його у своєму редакторі – це те, що дійсно показує вам переваги FastAPI, бачите, як мало коду вам потрібно написати, всі перевірки типів, автозаповнення тощо. + +--- + +## Встановлення FastAPI + +Першим кроком є встановлення FastAPI. + +Для туторіалу ви можете встановити його з усіма необов’язковими залежностями та функціями: + +
+ +```console +$ pip install "fastapi[all]" + +---> 100% +``` + +
+ +...який також включає `uvicorn`, який ви можете використовувати як сервер, який запускає ваш код. + +!!! note + Ви також можете встановити його частина за частиною. + + Це те, що ви, ймовірно, зробили б, коли захочете розгорнути свою програму у виробничому середовищі: + + ``` + pip install fastapi + ``` + + Також встановіть `uvicorn`, щоб він працював як сервер: + + ``` + pip install "uvicorn[standard]" + ``` + + І те саме для кожної з опціональних залежностей, які ви хочете використовувати. + +## Розширений посібник користувача + +Існує також **Розширений посібник користувача**, який ви зможете прочитати пізніше після цього **Туторіал - Посібник користувача**. + +**Розширений посібник користувача** засновано на цьому, використовує ті самі концепції та навчає вас деяким додатковим функціям. + +Але вам слід спочатку прочитати **Туторіал - Посібник користувача** (те, що ви зараз читаєте). + +Він розроблений таким чином, що ви можете створити повну програму лише за допомогою **Туторіал - Посібник користувача**, а потім розширити її різними способами, залежно від ваших потреб, використовуючи деякі з додаткових ідей з **Розширеного посібника користувача** . diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 7113287710..de18856f44 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -1,145 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/uk/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: uk -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs/ur/docs/benchmarks.md b/docs/ur/docs/benchmarks.md new file mode 100644 index 0000000000..9fc793e6fc --- /dev/null +++ b/docs/ur/docs/benchmarks.md @@ -0,0 +1,52 @@ +# بینچ مارکس + +انڈیپنڈنٹ ٹیک امپور بینچ مارک **FASTAPI** Uvicorn کے تحت چلنے والی ایپلی کیشنز کو ایک تیز رفتار Python فریم ورک میں سے ایک ، صرف Starlette اور Uvicorn کے نیچے ( FASTAPI کے ذریعہ اندرونی طور پر استعمال کیا جاتا ہے ) (*) + +لیکن جب بینچ مارک اور موازنہ کی جانچ پڑتال کرتے ہو تو آپ کو مندرجہ ذیل بات ذہن میں رکھنی چاہئے. + +## بینچ مارک اور رفتار + +جب آپ بینچ مارک کی جانچ کرتے ہیں تو ، مساوی کے مقابلے میں مختلف اقسام کے متعدد اوزار دیکھنا عام ہے. + +خاص طور پر ، Uvicorn, Starlette اور FastAPI کو دیکھنے کے لئے ( بہت سے دوسرے ٹولز ) کے ساتھ موازنہ کیا گیا. + +ٹول کے ذریعہ حل ہونے والا آسان مسئلہ ، اس کی بہتر کارکردگی ہوگی. اور زیادہ تر بینچ مارک ٹول کے ذریعہ فراہم کردہ اضافی خصوصیات کی جانچ نہیں کرتے ہیں. + +درجہ بندی کی طرح ہے: + +
    +
  • ASGI :Uvicorn سرور
  • +
      +
    • Starlette: (Uvicorn استعمال کرتا ہے) ایک ویب مائیکرو فریم ورک
    • +
        +
      • FastAPI: (Starlette کا استعمال کرتا ہے) ایک API مائکرو فریم ورک جس میں APIs بنانے کے لیے کئی اضافی خصوصیات ہیں، ڈیٹا کی توثیق وغیرہ کے ساتھ۔
      • +
      +
    +
+ +
    +
  • Uvicorn:
  • +
      +
    • بہترین کارکردگی ہوگی، کیونکہ اس میں سرور کے علاوہ زیادہ اضافی کوڈ نہیں ہے۔
    • +
    • آپ براہ راست Uvicorn میں درخواست نہیں لکھیں گے۔ اس کا مطلب یہ ہوگا کہ آپ کے کوڈ میں کم و بیش، کم از کم، Starlette (یا FastAPI) کی طرف سے فراہم کردہ تمام کوڈ شامل کرنا ہوں گے۔ اور اگر آپ نے ایسا کیا تو، آپ کی حتمی ایپلیکیشن کا وہی اوور ہیڈ ہوگا جیسا کہ ایک فریم ورک استعمال کرنے اور آپ کے ایپ کوڈ اور کیڑے کو کم سے کم کرنا۔
    • +
    • اگر آپ Uvicorn کا موازنہ کر رہے ہیں تو اس کا موازنہ Daphne، Hypercorn، uWSGI وغیرہ ایپلیکیشن سرورز سے کریں۔
    • +
    +
+
    +
  • Starlette:
  • +
      +
    • Uvicorn کے بعد اگلی بہترین کارکردگی ہوگی۔ درحقیقت، Starlette چلانے کے لیے Uvicorn کا استعمال کرتی ہے۔ لہذا، یہ شاید زیادہ کوڈ پر عمل درآمد کرکے Uvicorn سے "سست" ہوسکتا ہے۔
    • +
    • لیکن یہ آپ کو آسان ویب ایپلیکیشنز بنانے کے لیے ٹولز فراہم کرتا ہے، راستوں پر مبنی روٹنگ کے ساتھ، وغیرہ۔
    • +
    • اگر آپ سٹارلیٹ کا موازنہ کر رہے ہیں تو اس کا موازنہ Sanic، Flask، Django وغیرہ سے کریں۔ ویب فریم ورکس (یا مائیکرو فریم ورکس)
    • +
    +
+
    +
  • FastAPI:
  • +
      +
    • جس طرح سے Uvicorn Starlette کا استعمال کرتا ہے اور اس سے تیز نہیں ہو سکتا، Starlette FastAPI کا استعمال کرتا ہے، اس لیے یہ اس سے تیز نہیں ہو سکتا۔
    • +
    • Starlette FastAPI کے اوپری حصے میں مزید خصوصیات فراہم کرتا ہے۔ وہ خصوصیات جن کی آپ کو APIs بناتے وقت تقریباً ہمیشہ ضرورت ہوتی ہے، جیسے ڈیٹا کی توثیق اور سیریلائزیشن۔ اور اسے استعمال کرنے سے، آپ کو خودکار دستاویزات مفت میں مل جاتی ہیں (خودکار دستاویزات چلنے والی ایپلی کیشنز میں اوور ہیڈ کو بھی شامل نہیں کرتی ہیں، یہ اسٹارٹ اپ پر تیار ہوتی ہیں)۔
    • +
    • اگر آپ نے FastAPI کا استعمال نہیں کیا ہے اور Starlette کو براہ راست استعمال کیا ہے (یا کوئی دوسرا ٹول، جیسے Sanic، Flask، Responder، وغیرہ) آپ کو تمام ڈیٹا کی توثیق اور سیریلائزیشن کو خود نافذ کرنا ہوگا۔ لہذا، آپ کی حتمی ایپلیکیشن اب بھی وہی اوور ہیڈ ہوگی جیسا کہ اسے FastAPI کا استعمال کرتے ہوئے بنایا گیا تھا۔ اور بہت سے معاملات میں، یہ ڈیٹا کی توثیق اور سیریلائزیشن ایپلی کیشنز میں لکھے گئے کوڈ کی سب سے بڑی مقدار ہے۔
    • +
    • لہذا، FastAPI کا استعمال کرکے آپ ترقیاتی وقت، Bugs، کوڈ کی لائنوں کی بچت کر رہے ہیں، اور شاید آپ کو وہی کارکردگی (یا بہتر) ملے گی اگر آپ اسے استعمال نہیں کرتے (جیسا کہ آپ کو یہ سب اپنے کوڈ میں لاگو کرنا ہوگا۔ )
    • +
    • اگر آپ FastAPI کا موازنہ کر رہے ہیں، تو اس کا موازنہ ویب ایپلیکیشن فریم ورک (یا ٹولز کے سیٹ) سے کریں جو ڈیٹا کی توثیق، سیریلائزیشن اور دستاویزات فراہم کرتا ہے، جیسے Flask-apispec، NestJS، Molten، وغیرہ۔ مربوط خودکار ڈیٹا کی توثیق، سیریلائزیشن اور دستاویزات کے ساتھ فریم ورک۔
    • +
    +
diff --git a/docs/ur/mkdocs.yml b/docs/ur/mkdocs.yml new file mode 100644 index 0000000000..de18856f44 --- /dev/null +++ b/docs/ur/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml diff --git a/docs/vi/docs/features.md b/docs/vi/docs/features.md new file mode 100644 index 0000000000..306aeb3595 --- /dev/null +++ b/docs/vi/docs/features.md @@ -0,0 +1,197 @@ +# Tính năng + +## Tính năng của FastAPI + +**FastAPI** cho bạn những tính năng sau: + +### Dựa trên những tiêu chuẩn mở + +* OpenAPI cho việc tạo API, bao gồm những khai báo về đường dẫn các toán tử, tham số, body requests, cơ chế bảo mật, etc. +* Tự động tài liệu hóa data model theo JSON Schema (OpenAPI bản thân nó được dựa trên JSON Schema). +* Được thiết kế xung quanh các tiêu chuẩn này sau khi nghiên cứu tỉ mỉ thay vì chỉ suy nghĩ đơn giản và sơ xài. +* Điều này cho phép tự động hóa **trình sinh code client** cho nhiều ngôn ngữ lập trình khác nhau. + +### Tự động hóa tài liệu + + +Tài liệu tương tác API và web giao diện người dùng. Là một framework được dựa trên OpenAPI do đó có nhiều tùy chọn giao diện cho tài liệu API, 2 giao diện bên dưới là mặc định. + +* Swagger UI, với giao diện khám phá, gọi và kiểm thử API trực tiếp từ trình duyệt. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Thay thế với tài liệu API với ReDoc. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Chỉ cần phiên bản Python hiện đại + +Tất cả được dựa trên khai báo kiểu dữ liệu chuẩn của **Python 3.8** (cảm ơn Pydantic). Bạn không cần học cú pháp mới, chỉ cần biết chuẩn Python hiện đại. + +Nếu bạn cần 2 phút để làm mới lại cách sử dụng các kiểu dữ liệu mới của Python (thậm chí nếu bạn không sử dụng FastAPI), xem hướng dẫn ngắn: [Kiểu dữ liệu Python](python-types.md){.internal-link target=_blank}. + +Bạn viết chuẩn Python với kiểu dữ liệu như sau: + +```Python +from datetime import date + +from pydantic import BaseModel + +# Declare a variable as a str +# and get editor support inside the function +def main(user_id: str): + return user_id + + +# A Pydantic model +class User(BaseModel): + id: int + name: str + joined: date +``` + +Sau đó có thể được sử dụng: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +!!! info + `**second_user_data` nghĩa là: + + Truyền các khóa và giá trị của dict `second_user_data` trực tiếp như các tham số kiểu key-value, tương đương với: `User(id=4, name="Mary", joined="2018-11-30")` + +### Được hỗ trợ từ các trình soạn thảo + + +Toàn bộ framework được thiết kế để sử dụng dễ dàng và trực quan, toàn bộ quyết định đã được kiểm thử trên nhiều trình soạn thảo thậm chí trước khi bắt đầu quá trình phát triển, để chắc chắn trải nghiệm phát triển là tốt nhất. + +Trong lần khảo sát cuối cùng dành cho các lập trình viên Python, đã rõ ràng rằng đa số các lập trình viên sử dụng tính năng "autocompletion". + +Toàn bộ framework "FastAPI" phải đảm bảo rằng: autocompletion hoạt động ở mọi nơi. Bạn sẽ hiếm khi cần quay lại để đọc tài liệu. + +Đây là các trình soạn thảo có thể giúp bạn: + +* trong Visual Studio Code: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* trong PyCharm: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +Bạn sẽ có được auto-completion trong code, thậm chí trước đó là không thể. Như trong ví dụ, khóa `price` bên trong một JSON (đó có thể được lồng nhau) đến từ một request. + +Không còn nhập sai tên khóa, quay đi quay lại giữa các tài liệu hoặc cuộn lên cuộn xuống để tìm xem cuối cùng bạn đã sử dụng `username` hay `user_name`. + +### Ngắn gọn + +FastAPI có các giá trị mặc định hợp lý cho mọi thứ, với các cấu hình tùy chọn ở mọi nơi. Tất cả các tham số có thể được tinh chỉnh để thực hiện những gì bạn cần và để định nghĩa API bạn cần. + +Nhưng mặc định, tất cả **đều hoạt động**. + +### Validation + +* Validation cho đa số (hoặc tất cả?) **các kiểu dữ liệu** Python, bao gồm: + * JSON objects (`dict`). + * Mảng JSON (`list`) định nghĩa kiểu dữ liệu từng phần tử. + * Xâu (`str`), định nghĩa độ dài lớn nhất, nhỏ nhất. + * Số (`int`, `float`) với các giá trị lớn nhất, nhỏ nhất, etc. + +* Validation cho nhiều kiểu dữ liệu bên ngoài như: + * URL. + * Email. + * UUID. + * ...và nhiều cái khác. + +Tất cả validation được xử lí bằng những thiết lập tốt và mạnh mẽ của **Pydantic**. + +### Bảo mật và xác thực + +Bảo mật và xác thực đã tích hợp mà không làm tổn hại tới cơ sở dữ liệu hoặc data models. + +Tất cả cơ chế bảo mật định nghĩa trong OpenAPI, bao gồm: + +* HTTP Basic. +* **OAuth2** (với **JWT tokens**). Xem hướng dẫn [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* API keys in: + * Headers. + * Các tham số trong query string. + * Cookies, etc. + +Cộng với tất cả các tính năng bảo mật từ Starlette (bao gồm **session cookies**). + +Tất cả được xây dựng dưới dạng các công cụ và thành phần có thể tái sử dụng, dễ dàng tích hợp với hệ thống, kho lưu trữ dữ liệu, cơ sở dữ liệu quan hệ và NoSQL của bạn,... + +### Dependency Injection + +FastAPI bao gồm một hệ thống Dependency Injection vô cùng dễ sử dụng nhưng vô cùng mạnh mẽ. + +* Thậm chí, các dependency có thể có các dependency khác, tạo thành một phân cấp hoặc **"một đồ thị" của các dependency**. +* Tất cả **được xử lí tự động** bởi framework. +* Tất cả các dependency có thể yêu cầu dữ liệu từ request và **tăng cường các ràng buộc từ đường dẫn** và tự động tài liệu hóa. +* **Tự động hóa validation**, thậm chí với các tham số *đường dẫn* định nghĩa trong các dependency. +* Hỗ trợ hệ thống xác thực người dùng phức tạp, **các kết nối cơ sở dữ liệu**,... +* **Không làm tổn hại** cơ sở dữ liệu, frontends,... Nhưng dễ dàng tích hợp với tất cả chúng. + +### Không giới hạn "plug-ins" + +Hoặc theo một cách nào khác, không cần chúng, import và sử dụng code bạn cần. + +Bất kì tích hợp nào được thiết kế để sử dụng đơn giản (với các dependency), đến nỗi bạn có thể tạo một "plug-in" cho ứng dụng của mình trong 2 dòng code bằng cách sử dụng cùng một cấu trúc và cú pháp được sử dụng cho *path operations* của bạn. + +### Đã được kiểm thử + +* 100% test coverage. +* 100% type annotated code base. +* Được sử dụng cho các ứng dụng sản phẩm. + +## Tính năng của Starlette + +`FastAPI` is thực sự là một sub-class của `Starlette`. Do đó, nếu bạn đã biết hoặc đã sử dụng Starlette, đa số các chức năng sẽ làm việc giống như vậy. + +Với **FastAPI**, bạn có được tất cả những tính năng của **Starlette**: + +* Hiệu năng thực sự ấn tượng. Nó là một trong nhưng framework Python nhanh nhất, khi so sánh với **NodeJS** và **Go**. +* Hỗ trợ **WebSocket**. +* In-process background tasks. +* Startup and shutdown events. +* Client cho kiểm thử xây dựng trên HTTPX. +* **CORS**, GZip, Static Files, Streaming responses. +* Hỗ trợ **Session and Cookie**. +* 100% test coverage. +* 100% type annotated codebase. + +## Tính năng của Pydantic + +**FastAPI** tương thích đầy đủ với (và dựa trên) Pydantic. Do đó, bất kì code Pydantic nào bạn thêm vào cũng sẽ hoạt động. + +Bao gồm các thư viện bên ngoài cũng dựa trên Pydantic, như ORMs, ODMs cho cơ sở dữ liệu. + +Nó cũng có nghĩa là trong nhiều trường hợp, bạn có thể truyền cùng object bạn có từ một request **trực tiếp cho cơ sở dữ liệu**, vì mọi thứ được validate tự động. + +Điều tương tự áp dụng cho các cách khác nhau, trong nhiều trường hợp, bạn có thể chỉ truyền object từ cơ sở dữ liêu **trực tiếp tới client**. + +Với **FastAPI**, bạn có tất cả những tính năng của **Pydantic** (FastAPI dựa trên Pydantic cho tất cả những xử lí về dữ liệu): + +* **Không gây rối não**: + * Không cần học ngôn ngữ mô tả cấu trúc mới. + * Nếu bạn biết kiểu dữ liệu Python, bạn biết cách sử dụng Pydantic. +* Sử dụng tốt với **IDE/linter/não của bạn**: + + * Bởi vì các cấu trúc dữ liệu của Pydantic chỉ là các instances của class bạn định nghĩa; auto-completion, linting, mypy và trực giác của bạn nên làm việc riêng biệt với những dữ liệu mà bạn đã validate. +* Validate **các cấu trúc phức tạp**: + * Sử dụng các models Pydantic phân tầng, `List` và `Dict` của Python `typing`,... + * Và các validators cho phép các cấu trúc dữ liệu phức tạp trở nên rõ ràng và dễ dàng để định nghĩa, kiểm tra và tài liệu hóa thành JSON Schema. + * Bạn có thể có các object **JSON lồng nhau** và tất cả chúng đã validate và annotated. +* **Có khả năng mở rộng**: + * Pydantic cho phép bạn tùy chỉnh kiểu dữ liệu bằng việc định nghĩa hoặc bạn có thể mở rộng validation với các decorator trong model. +* 100% test coverage. diff --git a/docs/vi/docs/index.md b/docs/vi/docs/index.md new file mode 100644 index 0000000000..3f416dbece --- /dev/null +++ b/docs/vi/docs/index.md @@ -0,0 +1,472 @@ +

+ FastAPI +

+

+ FastAPI framework, hiệu năng cao, dễ học, dễ code, sẵn sàng để tạo ra sản phẩm +

+

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

+ +--- + +**Tài liệu**: https://fastapi.tiangolo.com + +**Mã nguồn**: https://github.com/tiangolo/fastapi + +--- + +FastAPI là một web framework hiện đại, hiệu năng cao để xây dựng web APIs với Python 3.8+ dựa trên tiêu chuẩn Python type hints. + +Những tính năng như: + +* **Nhanh**: Hiệu năng rất cao khi so sánh với **NodeJS** và **Go** (cảm ơn Starlette và Pydantic). [Một trong những Python framework nhanh nhất](#performance). +* **Code nhanh**: Tăng tốc độ phát triển tính năng từ 200% tới 300%. * +* **Ít lỗi hơn**: Giảm khoảng 40% những lỗi phát sinh bởi con người (nhà phát triển). * +* **Trực giác tốt hơn**: Được các trình soạn thảo hỗ tuyệt vời. Completion mọi nơi. Ít thời gian gỡ lỗi. +* **Dễ dàng**: Được thiết kế để dễ dàng học và sử dụng. Ít thời gian đọc tài liệu. +* **Ngắn**: Tối thiểu code bị trùng lặp. Nhiều tính năng được tích hợp khi định nghĩa tham số. Ít lỗi hơn. +* **Tăng tốc**: Có được sản phẩm cùng với tài liệu (được tự động tạo) có thể tương tác. +* **Được dựa trên các tiêu chuẩn**: Dựa trên (và hoàn toàn tương thích với) các tiêu chuẩn mở cho APIs : OpenAPI (trước đó được biết đến là Swagger) và JSON Schema. + +* ước tính được dựa trên những kiểm chứng trong nhóm phát triển nội bộ, xây dựng các ứng dụng sản phẩm. + +## Nhà tài trợ + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +Những nhà tài trợ khác + +## Ý kiến đánh giá + +"_[...] Tôi đang sử dụng **FastAPI** vô cùng nhiều vào những ngày này. [...] Tôi thực sự đang lên kế hoạch sử dụng nó cho tất cả các nhóm **dịch vụ ML tại Microsoft**. Một vài trong số đó đang tích hợp vào sản phẩm lõi của **Window** và một vài sản phẩm cho **Office**._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_Chúng tôi tích hợp thư viện **FastAPI** để sinh ra một **REST** server, nó có thể được truy vấn để thu được những **dự đoán**._ [bởi Ludwid] " + +
Piero Molino, Yaroslav Dudin, và Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** vui mừng thông báo việc phát hành framework mã nguồn mở của chúng tôi cho *quản lí khủng hoảng* tập trung: **Dispatch**! [xây dựng với **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_Tôi vô cùng hào hứng về **FastAPI**. Nó rất thú vị_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Thành thật, những gì bạn đã xây dựng nhìn siêu chắc chắn và bóng bẩy. Theo nhiều cách, nó là những gì tôi đã muốn Hug trở thành - thật sự truyền cảm hứng để thấy ai đó xây dựng nó._" + +
Timothy Crosley - người tạo ra Hug (ref)
+ +--- + +"_Nếu bạn đang tìm kiếm một **framework hiện đại** để xây dựng một REST APIs, thử xem xét **FastAPI** [...] Nó nhanh, dễ dùng và dễ học [...]_" + +"_Chúng tôi đã chuyển qua **FastAPI cho **APIs** của chúng tôi [...] Tôi nghĩ bạn sẽ thích nó [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+
Ines Montani - Matthew Honnibal - nhà sáng lập Explosion AI - người tạo ra spaCy (ref) - (ref)
+ +--- + +"_Nếu ai đó đang tìm cách xây dựng sản phẩm API bằng Python, tôi sẽ đề xuất **FastAPI**. Nó **được thiết kế đẹp đẽ**, **sử dụng đơn giản** và **có khả năng mở rộng cao**, nó đã trở thành một **thành phần quan trọng** trong chiến lược phát triển API của chúng tôi và đang thúc đẩy nhiều dịch vụ và mảng tự động hóa như Kỹ sư TAC ảo của chúng tôi._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## **Typer**, giao diện dòng lệnh của FastAPI + + + +Nếu bạn đang xây dựng một CLI - ứng dụng được sử dụng trong giao diện dòng lệnh, xem về **Typer**. + +**Typer** là một người anh em của FastAPI. Và nó được dự định trở thành **giao diện dòng lệnh cho FastAPI**. ⌨️ 🚀 + +## Yêu cầu + +Python 3.8+ + +FastAPI đứng trên vai những người khổng lồ: + +* Starlette cho phần web. +* Pydantic cho phần data. + +## Cài đặt + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +Bạn cũng sẽ cần một ASGI server cho production như Uvicorn hoặc Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## Ví dụ + +### Khởi tạo + +* Tạo một tệp tin `main.py` như sau: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+Hoặc sử dụng async def... + +Nếu code của bạn sử dụng `async` / `await`, hãy sử dụng `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Lưu ý**: + +Nếu bạn không biết, xem phần _"In a hurry?"_ về `async` và `await` trong tài liệu này. + +
+ +### Chạy ứng dụng + +Chạy server như sau: + +
+ +```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. +``` + +
+ +
+Về lệnh uvicorn main:app --reload... + +Lệnh `uvicorn main:app` tham chiếu tới những thành phần sau: + +* `main`: tệp tin `main.py` (một Python "module"). +* `app`: object được tạo trong tệp tin `main.py` tại dòng `app = FastAPI()`. +* `--reload`: chạy lại server sau khi code thay đổi. Chỉ sử dụng trong quá trình phát triển. + +
+ +### Kiểm tra + +Mở trình duyệt của bạn tại http://127.0.0.1:8000/items/5?q=somequery. + +Bạn sẽ thấy một JSON response: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +Bạn đã sẵn sàng để tạo một API như sau: + +* Nhận HTTP request với _đường dẫn_ `/` và `/items/{item_id}`. +* Cả hai _đường dẫn_ sử dụng toán tử `GET` (cũng đươc biết đến là _phương thức_ HTTP). +* _Đường dẫn_ `/items/{item_id}` có một _tham số đường dẫn_ `item_id`, nó là một tham số kiểu `int`. +* _Đường dẫn_ `/items/{item_id}` có một _tham số query string_ `q`, nó là một tham số tùy chọn kiểu `str`. + +### Tài liệu tương tác API + +Truy cập http://127.0.0.1:8000/docs. + +Bạn sẽ thấy tài liệu tương tác API được tạo tự động (cung cấp bởi Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Tài liệu API thay thế + +Và bây giờ, hãy truy cập tới http://127.0.0.1:8000/redoc. + +Bạn sẽ thấy tài liệu được thay thế (cung cấp bởi ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Nâng cấp ví dụ + +Bây giờ sửa tệp tin `main.py` để nhận body từ một request `PUT`. + +Định nghĩa của body sử dụng kiểu dữ liệu chuẩn của Python, cảm ơn Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +Server nên tự động chạy lại (bởi vì bạn đã thêm `--reload` trong lệnh `uvicorn` ở trên). + +### Nâng cấp tài liệu API + +Bây giờ truy cập tới http://127.0.0.1:8000/docs. + +* Tài liệu API sẽ được tự động cập nhật, bao gồm body mới: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Click vào nút "Try it out", nó cho phép bạn điền những tham số và tương tác trực tiếp với API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Sau khi click vào nút "Execute", giao diện người dùng sẽ giao tiếp với API của bạn bao gồm: gửi các tham số, lấy kết quả và hiển thị chúng trên màn hình: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Nâng cấp tài liệu API thay thế + +Và bây giờ truy cập tới http://127.0.0.1:8000/redoc. + +* Tài liệu thay thế cũng sẽ phản ánh tham số và body mới: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Tóm lại + +Bạn khai báo **một lần** kiểu dữ liệu của các tham số, body, etc là các tham số của hàm số. + +Bạn định nghĩa bằng cách sử dụng các kiểu dữ liệu chuẩn của Python. + +Bạn không phải học một cú pháp mới, các phương thức và class của một thư viện cụ thể nào. + +Chỉ cần sử dụng các chuẩn của **Python 3.8+**. + +Ví dụ, với một tham số kiểu `int`: + +```Python +item_id: int +``` + +hoặc với một model `Item` phức tạp hơn: + +```Python +item: Item +``` + +...và với định nghĩa đơn giản đó, bạn có được: + +* Sự hỗ trợ từ các trình soạn thảo, bao gồm: + * Completion. + * Kiểm tra kiểu dữ liệu. +* Kiểm tra kiểu dữ liệu: + * Tự động sinh lỗi rõ ràng khi dữ liệu không hợp lệ . + * Kiểm tra JSON lồng nhau . +* Chuyển đổi dữ liệu đầu vào: tới từ network sang dữ liệu kiểu Python. Đọc từ: + * JSON. + * Các tham số trong đường dẫn. + * Các tham số trong query string. + * Cookies. + * Headers. + * Forms. + * Files. +* Chuyển đổi dữ liệu đầu ra: chuyển đổi từ kiểu dữ liệu Python sang dữ liệu network (như JSON): + * Chuyển đổi kiểu dữ liệu Python (`str`, `int`, `float`, `bool`, `list`,...). + * `datetime` objects. + * `UUID` objects. + * Database models. + * ...và nhiều hơn thế. +* Tự động tạo tài liệu tương tác API, bao gồm 2 giao diện người dùng: + * Swagger UI. + * ReDoc. + +--- + +Quay trở lại ví dụ trước, **FastAPI** sẽ thực hiện: + +* Kiểm tra xem có một `item_id` trong đường dẫn với các request `GET` và `PUT` không? +* Kiểm tra xem `item_id` có phải là kiểu `int` trong các request `GET` và `PUT` không? + * Nếu không, client sẽ thấy một lỗi rõ ràng và hữu ích. +* Kiểm tra xem nếu có một tham số `q` trong query string (ví dụ như `http://127.0.0.1:8000/items/foo?q=somequery`) cho request `GET`. + * Tham số `q` được định nghĩa `= None`, nó là tùy chọn. + * Nếu không phải `None`, nó là bắt buộc (như body trong trường hợp của `PUT`). +* Với request `PUT` tới `/items/{item_id}`, đọc body như JSON: + * Kiểm tra xem nó có một thuộc tính bắt buộc kiểu `str` là `name` không? + * Kiểm tra xem nó có một thuộc tính bắt buộc kiểu `float` là `price` không? + * Kiểm tra xem nó có một thuộc tính tùy chọn là `is_offer` không? Nếu có, nó phải có kiểu `bool`. + * Tất cả những kiểm tra này cũng được áp dụng với các JSON lồng nhau. +* Chuyển đổi tự động các JSON object đến và JSON object đi. +* Tài liệu hóa mọi thứ với OpenAPI, tài liệu đó có thể được sử dụng bởi: + + * Các hệ thống tài liệu có thể tương tác. + * Hệ thống sinh code tự động, cho nhiều ngôn ngữ lập trình. +* Cung cấp trực tiếp 2 giao diện web cho tài liệu tương tác + +--- + +Chúng tôi chỉ trình bày những thứ cơ bản bên ngoài, nhưng bạn đã hiểu cách thức hoạt động của nó. + +Thử thay đổi dòng này: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...từ: + +```Python + ... "item_name": item.name ... +``` + +...sang: + +```Python + ... "item_price": item.price ... +``` + +...và thấy trình soạn thảo của bạn nhận biết kiểu dữ liệu và gợi ý hoàn thiện các thuộc tính. + +![trình soạn thảo hỗ trợ](https://fastapi.tiangolo.com/img/vscode-completion.png) + +Ví dụ hoàn chỉnh bao gồm nhiều tính năng hơn, xem Tutorial - User Guide. + + +**Cảnh báo tiết lỗ**: Tutorial - User Guide: + +* Định nghĩa **tham số** từ các nguồn khác nhau như: **headers**, **cookies**, **form fields** và **files**. +* Cách thiết lập **các ràng buộc cho validation** như `maximum_length` hoặc `regex`. +* Một hệ thống **Dependency Injection vô cùng mạnh mẽ và dễ dàng sử dụng. +* Bảo mật và xác thực, hỗ trợ **OAuth2**(với **JWT tokens**) và **HTTP Basic**. +* Những kĩ thuật nâng cao hơn (nhưng tương đối dễ) để định nghĩa **JSON models lồng nhau** (cảm ơn Pydantic). +* Tích hợp **GraphQL** với Strawberry và các thư viện khác. +* Nhiều tính năng mở rộng (cảm ơn Starlette) như: + * **WebSockets** + * kiểm thử vô cùng dễ dàng dựa trên HTTPX và `pytest` + * **CORS** + * **Cookie Sessions** + * ...và nhiều hơn thế. + +## Hiệu năng + +Independent TechEmpower benchmarks cho thấy các ứng dụng **FastAPI** chạy dưới Uvicorn là một trong những Python framework nhanh nhất, chỉ đứng sau Starlette và Uvicorn (được sử dụng bên trong FastAPI). (*) + +Để hiểu rõ hơn, xem phần Benchmarks. + +## Các dependency tùy chọn + +Sử dụng bởi Pydantic: + +* ujson - "Parse" JSON nhanh hơn. +* email_validator - cho email validation. + +Sử dụng Starlette: + +* httpx - Bắt buộc nếu bạn muốn sử dụng `TestClient`. +* jinja2 - Bắt buộc nếu bạn muốn sử dụng cấu hình template engine mặc định. +* python-multipart - Bắt buộc nếu bạn muốn hỗ trợ "parsing", form với `request.form()`. +* itsdangerous - Bắt buộc để hỗ trợ `SessionMiddleware`. +* pyyaml - Bắt buộc để hỗ trợ `SchemaGenerator` cho Starlette (bạn có thể không cần nó trong FastAPI). +* ujson - Bắt buộc nếu bạn muốn sử dụng `UJSONResponse`. + +Sử dụng bởi FastAPI / Starlette: + +* uvicorn - Server để chạy ứng dụng của bạn. +* orjson - Bắt buộc nếu bạn muốn sử dụng `ORJSONResponse`. + +Bạn có thể cài đặt tất cả những dependency trên với `pip install "fastapi[all]"`. + +## Giấy phép + +Dự án này được cấp phép dưới những điều lệ của giấy phép MIT. diff --git a/docs/vi/docs/python-types.md b/docs/vi/docs/python-types.md new file mode 100644 index 0000000000..4999caac34 --- /dev/null +++ b/docs/vi/docs/python-types.md @@ -0,0 +1,545 @@ +# Giới thiệu kiểu dữ liệu Python + +Python hỗ trợ tùy chọn "type hints" (còn được gọi là "type annotations"). + +Những **"type hints"** hay chú thích là một cú pháp đặc biệt cho phép khai báo kiểu dữ liệu của một biến. + +Bằng việc khai báo kiểu dữ liệu cho các biến của bạn, các trình soạn thảo và các công cụ có thể hỗ trợ bạn tốt hơn. + +Đây chỉ là một **hướng dẫn nhanh** về gợi ý kiểu dữ liệu trong Python. Nó chỉ bao gồm những điều cần thiết tối thiểu để sử dụng chúng với **FastAPI**... đó thực sự là rất ít. + +**FastAPI** hoàn toàn được dựa trên những gợi ý kiểu dữ liệu, chúng mang đến nhiều ưu điểm và lợi ích. + +Nhưng thậm chí nếu bạn không bao giờ sử dụng **FastAPI**, bạn sẽ được lợi từ việc học một ít về chúng. + +!!! note + Nếu bạn là một chuyên gia về Python, và bạn đã biết mọi thứ về gợi ý kiểu dữ liệu, bỏ qua và đi tới chương tiếp theo. + +## Động lực + +Hãy bắt đầu với một ví dụ đơn giản: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +Kết quả khi gọi chương trình này: + +``` +John Doe +``` + +Hàm thực hiện như sau: + +* Lấy một `first_name` và `last_name`. +* Chuyển đổi kí tự đầu tiên của mỗi biến sang kiểu chữ hoa với `title()`. +* Nối chúng lại với nhau bằng một kí tự trắng ở giữa. + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### Sửa đổi + +Nó là một chương trình rất đơn giản. + +Nhưng bây giờ hình dung rằng bạn đang viết nó từ đầu. + +Tại một vài thời điểm, bạn sẽ bắt đầu định nghĩa hàm, bạn có các tham số... + +Nhưng sau đó bạn phải gọi "phương thức chuyển đổi kí tự đầu tiên sang kiểu chữ hoa". + +Có phải là `upper`? Có phải là `uppercase`? `first_uppercase`? `capitalize`? + +Sau đó, bạn thử hỏi người bạn cũ của mình, autocompletion của trình soạn thảo. + +Bạn gõ tham số đầu tiên của hàm, `first_name`, sau đó một dấu chấm (`.`) và sau đó ấn `Ctrl+Space` để kích hoạt bộ hoàn thành. + +Nhưng đáng buồn, bạn không nhận được điều gì hữu ích cả: + + + +### Thêm kiểu dữ liệu + +Hãy sửa một dòng từ phiên bản trước. + +Chúng ta sẽ thay đổi chính xác đoạn này, tham số của hàm, từ: + +```Python + first_name, last_name +``` + +sang: + +```Python + first_name: str, last_name: str +``` + +Chính là nó. + +Những thứ đó là "type hints": + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +Đó không giống như khai báo những giá trị mặc định giống như: + +```Python + first_name="john", last_name="doe" +``` + +Nó là một thứ khác. + +Chúng ta sử dụng dấu hai chấm (`:`), không phải dấu bằng (`=`). + +Và việc thêm gợi ý kiểu dữ liệu không làm thay đổi những gì xảy ra so với khi chưa thêm chúng. + +But now, imagine you are again in the middle of creating that function, but with type hints. + +Tại cùng một điểm, bạn thử kích hoạt autocomplete với `Ctrl+Space` và bạn thấy: + + + +Với cái đó, bạn có thể cuộn, nhìn thấy các lựa chọn, cho đến khi bạn tìm thấy một "tiếng chuông": + + + +## Động lực nhiều hơn + +Kiểm tra hàm này, nó đã có gợi ý kiểu dữ liệu: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +Bởi vì trình soạn thảo biết kiểu dữ liệu của các biến, bạn không chỉ có được completion, bạn cũng được kiểm tra lỗi: + + + +Bây giờ bạn biết rằng bạn phải sửa nó, chuyển `age` sang một xâu với `str(age)`: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## Khai báo các kiểu dữ liệu + +Bạn mới chỉ nhìn thấy những nơi chủ yếu để đặt khai báo kiểu dữ liệu. Như là các tham số của hàm. + +Đây cũng là nơi chủ yếu để bạn sử dụng chúng với **FastAPI**. + +### Kiểu dữ liệu đơn giản + +Bạn có thể khai báo tất cả các kiểu dữ liệu chuẩn của Python, không chỉ là `str`. + +Bạn có thể sử dụng, ví dụ: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### Các kiểu dữ liệu tổng quát với tham số kiểu dữ liệu + +Có một vài cấu trúc dữ liệu có thể chứa các giá trị khác nhau như `dict`, `list`, `set` và `tuple`. Và những giá trị nội tại cũng có thể có kiểu dữ liệu của chúng. + +Những kiểu dữ liệu nội bộ này được gọi là những kiểu dữ liệu "**tổng quát**". Và có khả năng khai báo chúng, thậm chí với các kiểu dữ liệu nội bộ của chúng. + +Để khai báo những kiểu dữ liệu và những kiểu dữ liệu nội bộ đó, bạn có thể sử dụng mô đun chuẩn của Python là `typing`. Nó có hỗ trợ những gợi ý kiểu dữ liệu này. + +#### Những phiên bản mới hơn của Python + +Cú pháp sử dụng `typing` **tương thích** với tất cả các phiên bản, từ Python 3.6 tới những phiên bản cuối cùng, bao gồm Python 3.9, Python 3.10,... + +As Python advances, **những phiên bản mới** mang tới sự hỗ trợ được cải tiến cho những chú thích kiểu dữ liệu và trong nhiều trường hợp bạn thậm chí sẽ không cần import và sử dụng mô đun `typing` để khai báo chú thích kiểu dữ liệu. + +Nếu bạn có thể chọn một phiên bản Python gần đây hơn cho dự án của bạn, ban sẽ có được những ưu điểm của những cải tiến đơn giản đó. + +Trong tất cả các tài liệu tồn tại những ví dụ tương thích với mỗi phiên bản Python (khi có một sự khác nhau). + +Cho ví dụ "**Python 3.6+**" có nghĩa là nó tương thích với Python 3.7 hoặc lớn hơn (bao gồm 3.7, 3.8, 3.9, 3.10,...). và "**Python 3.9+**" nghĩa là nó tương thích với Python 3.9 trở lên (bao gồm 3.10,...). + +Nếu bạn có thể sử dụng **phiên bản cuối cùng của Python**, sử dụng những ví dụ cho phiên bản cuối, những cái đó sẽ có **cú pháp đơn giản và tốt nhât**, ví dụ, "**Python 3.10+**". + +#### List + +Ví dụ, hãy định nghĩa một biến là `list` các `str`. + +=== "Python 3.9+" + + Khai báo biến với cùng dấu hai chấm (`:`). + + Tương tự kiểu dữ liệu `list`. + + Như danh sách là một kiểu dữ liệu chứa một vài kiểu dữ liệu có sẵn, bạn đặt chúng trong các dấu ngoặc vuông: + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006_py39.py!} + ``` + +=== "Python 3.8+" + + Từ `typing`, import `List` (với chữ cái `L` viết hoa): + + ``` Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + + Khai báo biến với cùng dấu hai chấm (`:`). + + Tương tự như kiểu dữ liệu, `List` bạn import từ `typing`. + + Như danh sách là một kiểu dữ liệu chứa các kiểu dữ liệu có sẵn, bạn đặt chúng bên trong dấu ngoặc vuông: + + ```Python hl_lines="4" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + +!!! info + Các kiểu dữ liệu có sẵn bên trong dấu ngoặc vuông được gọi là "tham số kiểu dữ liệu". + + Trong trường hợp này, `str` là tham số kiểu dữ liệu được truyền tới `List` (hoặc `list` trong Python 3.9 trở lên). + +Có nghĩa là: "biến `items` là một `list`, và mỗi phần tử trong danh sách này là một `str`". + +!!! tip + Nếu bạn sử dụng Python 3.9 hoặc lớn hơn, bạn không phải import `List` từ `typing`, bạn có thể sử dụng `list` để thay thế. + +Bằng cách này, trình soạn thảo của bạn có thể hỗ trợ trong khi xử lí các phần tử trong danh sách: + + + +Đa phần đều không thể đạt được nếu không có các kiểu dữ liệu. + +Chú ý rằng, biến `item` là một trong các phần tử trong danh sách `items`. + +Và do vậy, trình soạn thảo biết nó là một `str`, và cung cấp sự hỗ trợ cho nó. + +#### Tuple and Set + +Bạn sẽ làm điều tương tự để khai báo các `tuple` và các `set`: + +=== "Python 3.9+" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial007_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial007.py!} + ``` + +Điều này có nghĩa là: + +* Biến `items_t` là một `tuple` với 3 phần tử, một `int`, một `int` nữa, và một `str`. +* Biến `items_s` là một `set`, và mỗi phần tử của nó có kiểu `bytes`. + +#### Dict + +Để định nghĩa một `dict`, bạn truyền 2 tham số kiểu dữ liệu, phân cách bởi dấu phẩy. + +Tham số kiểu dữ liệu đầu tiên dành cho khóa của `dict`. + +Tham số kiểu dữ liệu thứ hai dành cho giá trị của `dict`. + +=== "Python 3.9+" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008.py!} + ``` + +Điều này có nghĩa là: + +* Biến `prices` là một `dict`: + * Khóa của `dict` này là kiểu `str` (đó là tên của mỗi vật phẩm). + * Giá trị của `dict` này là kiểu `float` (đó là giá của mỗi vật phẩm). + +#### Union + +Bạn có thể khai báo rằng một biến có thể là **một vài kiểu dữ liệu" bất kì, ví dụ, một `int` hoặc một `str`. + +Trong Python 3.6 hoặc lớn hơn (bao gồm Python 3.10) bạn có thể sử dụng kiểu `Union` từ `typing` và đặt trong dấu ngoặc vuông những giá trị được chấp nhận. + +In Python 3.10 there's also a **new syntax** where you can put the possible types separated by a vertical bar (`|`). + +Trong Python 3.10 cũng có một **cú pháp mới** mà bạn có thể đặt những kiểu giá trị khả thi phân cách bởi một dấu sổ dọc (`|`). + + +=== "Python 3.10+" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008b_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008b.py!} + ``` + +Trong cả hai trường hợp có nghĩa là `item` có thể là một `int` hoặc `str`. + +#### Khả năng `None` + +Bạn có thể khai báo một giá trị có thể có một kiểu dữ liệu, giống như `str`, nhưng nó cũng có thể là `None`. + +Trong Python 3.6 hoặc lớn hơn (bao gồm Python 3.10) bạn có thể khai báo nó bằng các import và sử dụng `Optional` từ mô đun `typing`. + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +Sử dụng `Optional[str]` thay cho `str` sẽ cho phép trình soạn thảo giúp bạn phát hiện các lỗi mà bạn có thể gặp như một giá trị luôn là một `str`, trong khi thực tế nó rất có thể là `None`. + +`Optional[Something]` là một cách viết ngắn gọn của `Union[Something, None]`, chúng là tương đương nhau. + +Điều này cũng có nghĩa là trong Python 3.10, bạn có thể sử dụng `Something | None`: + +=== "Python 3.10+" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial009_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009.py!} + ``` + +=== "Python 3.8+ alternative" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009b.py!} + ``` + +#### Sử dụng `Union` hay `Optional` + +If you are using a Python version below 3.10, here's a tip from my very **subjective** point of view: + +Nếu bạn đang sử dụng phiên bản Python dưới 3.10, đây là một mẹo từ ý kiến rất "chủ quan" của tôi: + +* 🚨 Tránh sử dụng `Optional[SomeType]` +* Thay vào đó ✨ **sử dụng `Union[SomeType, None]`** ✨. + +Cả hai là tương đương và bên dưới chúng giống nhau, nhưng tôi sẽ đễ xuất `Union` thay cho `Optional` vì từ "**tùy chọn**" có vẻ ngầm định giá trị là tùy chọn, và nó thực sự có nghĩa rằng "nó có thể là `None`", do đó nó không phải là tùy chọn và nó vẫn được yêu cầu. + +Tôi nghĩ `Union[SomeType, None]` là rõ ràng hơn về ý nghĩa của nó. + +Nó chỉ là về các từ và tên. Nhưng những từ đó có thể ảnh hưởng cách bạn và những đồng đội của bạn suy nghĩ về code. + +Cho một ví dụ, hãy để ý hàm này: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c.py!} +``` + +Tham số `name` được định nghĩa là `Optional[str]`, nhưng nó **không phải là tùy chọn**, bạn không thể gọi hàm mà không có tham số: + +```Python +say_hi() # Oh, no, this throws an error! 😱 +``` + +Tham số `name` **vẫn được yêu cầu** (không phải là *tùy chọn*) vì nó không có giá trị mặc định. Trong khi đó, `name` chấp nhận `None` như là giá trị: + +```Python +say_hi(name=None) # This works, None is valid 🎉 +``` + +Tin tốt là, khi bạn sử dụng Python 3.10, bạn sẽ không phải lo lắng về điều đó, bạn sẽ có thể sử dụng `|` để định nghĩa hợp của các kiểu dữ liệu một cách đơn giản: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c_py310.py!} +``` + +Và sau đó, bạn sẽ không phải lo rằng những cái tên như `Optional` và `Union`. 😎 + + +#### Những kiểu dữ liệu tổng quát + +Những kiểu dữ liệu này lấy tham số kiểu dữ liệu trong dấu ngoặc vuông được gọi là **Kiểu dữ liệu tổng quát**, cho ví dụ: + +=== "Python 3.10+" + + Bạn có thể sử dụng các kiểu dữ liệu có sẵn như là kiểu dữ liệu tổng quát (với ngoặc vuông và kiểu dữ liệu bên trong): + + * `list` + * `tuple` + * `set` + * `dict` + + Và tương tự với Python 3.6, từ mô đun `typing`: + + * `Union` + * `Optional` (tương tự như Python 3.6) + * ...và các kiểu dữ liệu khác. + + Trong Python 3.10, thay vì sử dụng `Union` và `Optional`, bạn có thể sử dụng sổ dọc ('|') để khai báo hợp của các kiểu dữ liệu, điều đó tốt hơn và đơn giản hơn nhiều. + +=== "Python 3.9+" + + Bạn có thể sử dụng các kiểu dữ liệu có sẵn tương tự như (với ngoặc vuông và kiểu dữ liệu bên trong): + + * `list` + * `tuple` + * `set` + * `dict` + + Và tương tự với Python 3.6, từ mô đun `typing`: + + * `Union` + * `Optional` + * ...and others. + +=== "Python 3.8+" + + * `List` + * `Tuple` + * `Set` + * `Dict` + * `Union` + * `Optional` + * ...và các kiểu khác. + +### Lớp như kiểu dữ liệu + +Bạn cũng có thể khai báo một lớp như là kiểu dữ liệu của một biến. + +Hãy nói rằng bạn muốn có một lớp `Person` với một tên: + +```Python hl_lines="1-3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Sau đó bạn có thể khai báo một biến có kiểu là `Person`: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Và lại một lần nữa, bạn có được tất cả sự hỗ trợ từ trình soạn thảo: + + + +Lưu ý rằng, điều này có nghĩa rằng "`one_person`" là một **thực thể** của lớp `Person`. + +Nó không có nghĩa "`one_person`" là một **lớp** gọi là `Person`. + +## Pydantic models + +Pydantic là một thư viện Python để validate dữ liệu hiệu năng cao. + +Bạn có thể khai báo "hình dạng" của dữa liệu như là các lớp với các thuộc tính. + +Và mỗi thuộc tính có một kiểu dữ liệu. + +Sau đó bạn tạo một thực thể của lớp đó với một vài giá trị và nó sẽ validate các giá trị, chuyển đổi chúng sang kiểu dữ liệu phù hợp (nếu đó là trường hợp) và cho bạn một object với toàn bộ dữ liệu. + +Và bạn nhận được tất cả sự hỗ trợ của trình soạn thảo với object kết quả đó. + +Một ví dụ từ tài liệu chính thức của Pydantic: + +=== "Python 3.10+" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python + {!> ../../../docs_src/python_types/tutorial011.py!} + ``` + +!!! info + Để học nhiều hơn về Pydantic, tham khảo tài liệu của nó. + +**FastAPI** được dựa hoàn toàn trên Pydantic. + +Bạn sẽ thấy nhiều ví dụ thực tế hơn trong [Hướng dẫn sử dụng](tutorial/index.md){.internal-link target=_blank}. + +!!! tip + Pydantic có một hành vi đặc biệt khi bạn sử dụng `Optional` hoặc `Union[Something, None]` mà không có giá trị mặc dịnh, bạn có thể đọc nhiều hơn về nó trong tài liệu của Pydantic về Required Optional fields. + + +## Type Hints với Metadata Annotations + +Python cũng có một tính năng cho phép đặt **metadata bổ sung** trong những gợi ý kiểu dữ liệu này bằng cách sử dụng `Annotated`. + +=== "Python 3.9+" + + Trong Python 3.9, `Annotated` là một phần của thư viện chuẩn, do đó bạn có thể import nó từ `typing`. + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial013_py39.py!} + ``` + +=== "Python 3.8+" + + Ở phiên bản dưới Python 3.9, bạn import `Annotated` từ `typing_extensions`. + + Nó đã được cài đặt sẵng cùng với **FastAPI**. + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial013.py!} + ``` + +Python bản thân nó không làm bất kì điều gì với `Annotated`. Với các trình soạn thảo và các công cụ khác, kiểu dữ liệu vẫn là `str`. + +Nhưng bạn có thể sử dụng `Annotated` để cung cấp cho **FastAPI** metadata bổ sung về cách mà bạn muốn ứng dụng của bạn xử lí. + +Điều quan trọng cần nhớ là ***tham số kiểu dữ liệu* đầu tiên** bạn truyền tới `Annotated` là **kiểu giá trị thực sự**. Phần còn lại chỉ là metadata cho các công cụ khác. + +Bây giờ, bạn chỉ cần biết rằng `Annotated` tồn tại, và nó là tiêu chuẩn của Python. 😎 + + +Sau đó, bạn sẽ thấy sự **mạnh mẽ** mà nó có thể làm. + +!!! tip + Thực tế, cái này là **tiêu chuẩn của Python**, nghĩa là bạn vẫn sẽ có được **trải nghiệm phát triển tốt nhất có thể** với trình soạn thảo của bạn, với các công cụ bạn sử dụng để phân tích và tái cấu trúc code của bạn, etc. ✨ + + Và code của bạn sẽ tương thích với nhiều công cụ và thư viện khác của Python. 🚀 + +## Các gợi ý kiểu dữ liệu trong **FastAPI** + +**FastAPI** lấy các ưu điểm của các gợi ý kiểu dữ liệu để thực hiện một số thứ. + +Với **FastAPI**, bạn khai báo các tham số với gợi ý kiểu và bạn có được: + +* **Sự hỗ trợ từ các trình soạn thảo**. +* **Kiểm tra kiểu dữ liệu (type checking)**. + +...và **FastAPI** sử dụng các khia báo để: + +* **Định nghĩa các yêu cầu**: từ tham số đường dẫn của request, tham số query, headers, bodies, các phụ thuộc (dependencies),... +* **Chuyển dổi dữ liệu*: từ request sang kiểu dữ liệu được yêu cầu. +* **Kiểm tra tính đúng đắn của dữ liệu**: tới từ mỗi request: + * Sinh **lỗi tự động** để trả về máy khác khi dữ liệu không hợp lệ. +* **Tài liệu hóa** API sử dụng OpenAPI: + * cái mà sau được được sử dụng bởi tài liệu tương tác người dùng. + +Điều này có thể nghe trừu tượng. Đừng lo lắng. Bạn sẽ thấy tất cả chúng trong [Hướng dẫn sử dụng](tutorial/index.md){.internal-link target=_blank}. + +Điều quan trọng là bằng việc sử dụng các kiểu dữ liệu chuẩn của Python (thay vì thêm các lớp, decorators,...), **FastAPI** sẽ thực hiện nhiều công việc cho bạn. + +!!! info + Nếu bạn đã đi qua toàn bộ các hướng dẫn và quay trở lại để tìm hiểu nhiều hơn về các kiểu dữ liệu, một tài nguyên tốt như "cheat sheet" từ `mypy`. diff --git a/docs/vi/docs/tutorial/first-steps.md b/docs/vi/docs/tutorial/first-steps.md new file mode 100644 index 0000000000..712f00852f --- /dev/null +++ b/docs/vi/docs/tutorial/first-steps.md @@ -0,0 +1,333 @@ +# Những bước đầu tiên + +Tệp tin FastAPI đơn giản nhất có thể trông như này: + +```Python +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Sao chép sang một tệp tin `main.py`. + +Chạy live server: + +
+ +```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 + Câu lệnh `uvicorn main:app` được giải thích như sau: + + * `main`: tệp tin `main.py` (một Python "mô đun"). + * `app`: một object được tạo ra bên trong `main.py` với dòng `app = FastAPI()`. + * `--reload`: làm server khởi động lại sau mỗi lần thay đổi. Chỉ sử dụng trong môi trường phát triển. + +Trong output, có một dòng giống như: + +```hl_lines="4" +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +Dòng đó cho thấy URL, nơi mà app của bạn đang được chạy, trong máy local của bạn. + +### Kiểm tra + +Mở trình duyệt của bạn tại http://127.0.0.1:8000. + +Bạn sẽ thấy một JSON response như: + +```JSON +{"message": "Hello World"} +``` + +### Tài liệu tương tác API + +Bây giờ tới http://127.0.0.1:8000/docs. + +Bạn sẽ thấy một tài liệu tương tác API (cung cấp bởi Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Phiên bản thay thế của tài liệu API + +Và bây giờ tới http://127.0.0.1:8000/redoc. + +Bạn sẽ thấy một bản thay thế của tài liệu (cung cấp bởi ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +### OpenAPI + +**FastAPI** sinh một "schema" với tất cả API của bạn sử dụng tiêu chuẩn **OpenAPI** cho định nghĩa các API. + +#### "Schema" + +Một "schema" là một định nghĩa hoặc mô tả thứ gì đó. Không phải code triển khai của nó, nhưng chỉ là một bản mô tả trừu tượng. + +#### API "schema" + +Trong trường hợp này, OpenAPI là một bản mô tả bắt buộc cơ chế định nghĩa API của bạn. + +Định nghĩa cấu trúc này bao gồm những đường dẫn API của bạn, các tham số có thể có,... + +#### "Cấu trúc" dữ liệu + +Thuật ngữ "cấu trúc" (schema) cũng có thể được coi như là hình dạng của dữ liệu, tương tự như một JSON content. + +Trong trường hợp đó, nó có nghĩa là các thuộc tính JSON và các kiểu dữ liệu họ có,... + +#### OpenAPI và JSON Schema + +OpenAPI định nghĩa một cấu trúc API cho API của bạn. Và cấu trúc đó bao gồm các dịnh nghĩa (or "schema") về dữ liệu được gửi đi và nhận về bởi API của bạn, sử dụng **JSON Schema**, một tiêu chuẩn cho cấu trúc dữ liệu JSON. + +#### Kiểm tra `openapi.json` + +Nếu bạn tò mò về việc cấu trúc OpenAPI nhìn như thế nào thì FastAPI tự động sinh một JSON (schema) với các mô tả cho tất cả API của bạn. + +Bạn có thể thấy nó trực tiếp tại: http://127.0.0.1:8000/openapi.json. + +Nó sẽ cho thấy một JSON bắt đầu giống như: + +```JSON +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + + + +... +``` + +#### OpenAPI dùng để làm gì? + +Cấu trúc OpenAPI là sức mạnh của tài liệu tương tác. + +Và có hàng tá các bản thay thế, tất cả đều dựa trên OpenAPI. Bạn có thể dễ dàng thêm bất kì bản thay thế bào cho ứng dụng của bạn được xây dựng với **FastAPI**. + +Bạn cũng có thể sử dụng nó để sinh code tự động, với các client giao viết qua API của bạn. Ví dụ, frontend, mobile hoặc các ứng dụng IoT. + +## Tóm lại, từng bước một + +### Bước 1: import `FastAPI` + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI` là một Python class cung cấp tất cả chức năng cho API của bạn. + +!!! note "Chi tiết kĩ thuật" + `FastAPI` là một class kế thừa trực tiếp `Starlette`. + + Bạn cũng có thể sử dụng tất cả Starlette chức năng với `FastAPI`. + +### Bước 2: Tạo một `FastAPI` "instance" + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Biến `app` này là một "instance" của class `FastAPI`. + +Đây sẽ là điểm cốt lõi để tạo ra tất cả API của bạn. + +`app` này chính là điều được nhắc tới bởi `uvicorn` trong câu lệnh: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Nếu bạn tạo ứng dụng của bạn giống như: + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial002.py!} +``` + +Và đặt nó trong một tệp tin `main.py`, sau đó bạn sẽ gọi `uvicorn` giống như: + +
+ +```console +$ uvicorn main:my_awesome_api --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Bước 3: tạo một *đường dẫn toán tử* + +#### Đường dẫn + +"Đường dẫn" ở đây được nhắc tới là phần cuối cùng của URL bắt đầu từ `/`. + +Do đó, trong một URL nhìn giống như: + +``` +https://example.com/items/foo +``` + +...đường dẫn sẽ là: + +``` +/items/foo +``` + +!!! info + Một đường dẫn cũng là một cách gọi chung cho một "endpoint" hoặc một "route". + +Trong khi xây dựng một API, "đường dẫn" là các chính để phân tách "mối quan hệ" và "tài nguyên". + +#### Toán tử (Operation) + +"Toán tử" ở đây được nhắc tới là một trong các "phương thức" HTTP. + +Một trong những: + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +...và một trong những cái còn lại: + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +Trong giao thức HTTP, bạn có thể giao tiếp trong mỗi đường dẫn sử dụng một (hoặc nhiều) trong các "phương thức này". + +--- + +Khi xây dựng các API, bạn thường sử dụng cụ thể các phương thức HTTP này để thực hiện một hành động cụ thể. + +Thông thường, bạn sử dụng + +* `POST`: để tạo dữ liệu. +* `GET`: để đọc dữ liệu. +* `PUT`: để cập nhật dữ liệu. +* `DELETE`: để xóa dữ liệu. + +Do đó, trong OpenAPI, mỗi phương thức HTTP được gọi là một "toán tử (operation)". + +Chúng ta cũng sẽ gọi chúng là "**các toán tử**". + +#### Định nghĩa moojt *decorator cho đường dẫn toán tử* + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`@app.get("/")` nói **FastAPI** rằng hàm bên dưới có trách nhiệm xử lí request tới: + +* đường dẫn `/` +* sử dụng một toán tửget + +!!! info Thông tin về "`@decorator`" + Cú pháp `@something` trong Python được gọi là một "decorator". + + Bạn đặt nó trên một hàm. Giống như một chiếc mũ xinh xắn (Tôi ddonas đó là lí do mà thuật ngữ này ra đời). + + Một "decorator" lấy một hàm bên dưới và thực hiện một vài thứ với nó. + + Trong trường hợp của chúng ta, decorator này nói **FastAPI** rằng hàm bên dưới ứng với **đường dẫn** `/` và một **toán tử** `get`. + + Nó là một "**decorator đường dẫn toán tử**". + +Bạn cũng có thể sử dụng với các toán tử khác: + +* `@app.post()` +* `@app.put()` +* `@app.delete()` + +Và nhiều hơn với các toán tử còn lại: + +* `@app.options()` +* `@app.head()` +* `@app.patch()` +* `@app.trace()` + +!!! tip + Bạn thoải mái sử dụng mỗi toán tử (phương thức HTTP) như bạn mơ ước. + + **FastAPI** không bắt buộc bất kì ý nghĩa cụ thể nào. + + Thông tin ở đây được biểu thị như là một chỉ dẫn, không phải là một yêu cầu bắt buộc. + + Ví dụ, khi sử dụng GraphQL bạn thông thường thực hiện tất cả các hành động chỉ bằng việc sử dụng các toán tử `POST`. + +### Step 4: Định nghĩa **hàm cho đường dẫn toán tử** + +Đây là "**hàm cho đường dẫn toán tử**": + +* **đường dẫn**: là `/`. +* **toán tử**: là `get`. +* **hàm**: là hàm bên dưới "decorator" (bên dưới `@app.get("/")`). + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Đây là một hàm Python. + +Nó sẽ được gọi bởi **FastAPI** bất cứ khi nào nó nhận một request tới URL "`/`" sử dụng một toán tử `GET`. + +Trong trường hợp này, nó là một hàm `async`. + +--- + +Bạn cũng có thể định nghĩa nó như là một hàm thông thường thay cho `async def`: + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note + Nếu bạn không biết sự khác nhau, kiểm tra [Async: *"Trong khi vội vàng?"*](../async.md#in-a-hurry){.internal-link target=_blank}. + +### Bước 5: Nội dung trả về + +```Python hl_lines="8" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Bạn có thể trả về một `dict`, `list`, một trong những giá trị đơn như `str`, `int`,... + +Bạn cũng có thể trả về Pydantic model (bạn sẽ thấy nhiều hơn về nó sau). + +Có nhiều object và model khác nhau sẽ được tự động chuyển đổi sang JSON (bao gồm cả ORM,...). Thử sử dụng loại ưa thích của bạn, nó có khả năng cao đã được hỗ trợ. + +## Tóm lại + +* Import `FastAPI`. +* Tạo một `app` instance. +* Viết một **decorator cho đường dẫn toán tử** (giống như `@app.get("/")`). +* Viết một **hàm cho đường dẫn toán tử** (giống như `def root(): ...` ở trên). +* Chạy server trong môi trường phát triển (giống như `uvicorn main:app --reload`). diff --git a/docs/vi/docs/tutorial/index.md b/docs/vi/docs/tutorial/index.md new file mode 100644 index 0000000000..e8a93fe408 --- /dev/null +++ b/docs/vi/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Hướng dẫn sử dụng + +Hướng dẫn này cho bạn thấy từng bước cách sử dụng **FastAPI** đa số các tính năng của nó. + +Mỗi phần được xây dựng từ những phần trước đó, nhưng nó được cấu trúc thành các chủ đề riêng biệt, do đó bạn có thể xem trực tiếp từng phần cụ thể bất kì để giải quyết những API cụ thể mà bạn cần. + +Nó cũng được xây dựng để làm việc như một tham chiếu trong tương lai. + +Do đó bạn có thể quay lại và tìm chính xác những gì bạn cần. + +## Chạy mã + +Tất cả các code block có thể được sao chép và sử dụng trực tiếp (chúng thực chất là các tệp tin Python đã được kiểm thử). + +Để chạy bất kì ví dụ nào, sao chép code tới tệp tin `main.py`, và bắt đầu `uvicorn` với: + +
+ +```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. +``` + +
+ +**Khuyến khích** bạn viết hoặc sao chép code, sửa và chạy nó ở local. + +Sử dụng nó trong trình soạn thảo của bạn thực sự cho bạn thấy những lợi ích của FastAPI, thấy được cách bạn viết code ít hơn, tất cả đều được type check, autocompletion,... + +--- + +## Cài đặt FastAPI + +Bước đầu tiên là cài đặt FastAPI. + +Với hướng dẫn này, bạn có thể muốn cài đặt nó với tất cả các phụ thuộc và tính năng tùy chọn: + +
+ +```console +$ pip install "fastapi[all]" + +---> 100% +``` + +
+ +...dó cũng bao gồm `uvicorn`, bạn có thể sử dụng như một server để chạy code của bạn. + +!!! note + Bạn cũng có thể cài đặt nó từng phần. + + Đây là những gì bạn có thể sẽ làm một lần duy nhất bạn muốn triển khai ứng dụng của bạn lên production: + + ``` + pip install fastapi + ``` + + Cũng cài đặt `uvicorn` để làm việc như một server: + + ``` + pip install "uvicorn[standard]" + ``` + + Và tương tự với từng phụ thuộc tùy chọn mà bạn muốn sử dụng. + +## Hướng dẫn nâng cao + +Cũng có một **Hướng dẫn nâng cao** mà bạn có thể đọc nó sau **Hướng dẫn sử dụng**. + +**Hướng dẫn sử dụng nâng cao**, xây dựng dựa trên cái này, sử dụng các khái niệm tương tự, và dạy bạn những tính năng mở rộng. + +Nhưng bạn nên đọc **Hướng dẫn sử dụng** đầu tiên (những gì bạn đang đọc). + +Nó được thiết kế do đó bạn có thể xây dựng một ứng dụng hoàn chỉnh chỉ với **Hướng dẫn sử dụng**, và sau đó mở rộng nó theo các cách khác nhau, phụ thuộc vào những gì bạn cần, sử dụng một vài ý tưởng bổ sung từ **Hướng dẫn sử dụng nâng cao**. diff --git a/docs/vi/mkdocs.yml b/docs/vi/mkdocs.yml new file mode 100644 index 0000000000..de18856f44 --- /dev/null +++ b/docs/vi/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml diff --git a/docs/yo/docs/index.md b/docs/yo/docs/index.md new file mode 100644 index 0000000000..101e13b6b5 --- /dev/null +++ b/docs/yo/docs/index.md @@ -0,0 +1,470 @@ +

+ FastAPI +

+

+ Ìlànà wẹ́ẹ́bù FastAPI, iṣẹ́ gíga, ó rọrùn láti kọ̀, o yára láti kóòdù, ó sì ṣetán fún iṣelọpọ ní lílo +

+

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

+ +--- + +**Àkọsílẹ̀**: https://fastapi.tiangolo.com + +**Orisun Kóòdù**: https://github.com/tiangolo/fastapi + +--- + +FastAPI jẹ́ ìgbàlódé, tí ó yára (iṣẹ-giga), ìlànà wẹ́ẹ́bù fún kikọ àwọn API pẹ̀lú Python 3.8+ èyí tí ó da lori àwọn ìtọ́kasí àmì irúfẹ́ Python. + +Àwọn ẹya pàtàkì ni: + +* **Ó yára**: Iṣẹ tí ó ga púpọ̀, tí ó wa ni ibamu pẹ̀lú **NodeJS** àti **Go** (ọpẹ si Starlette àti Pydantic). [Ọkan nínú àwọn ìlànà Python ti o yára jùlọ ti o wa](#performance). +* **Ó yára láti kóòdù**: O mu iyara pọ si láti kọ àwọn ẹya tuntun kóòdù nipasẹ "Igba ìdá ọgọ́rùn-ún" (i.e. 200%) si "ọ̀ọ́dúrún ìdá ọgọ́rùn-ún" (i.e. 300%). +* **Àìtọ́ kékeré**: O n din aṣiṣe ku bi ọgbon ìdá ọgọ́rùn-ún (i.e. 40%) ti eda eniyan (oṣiṣẹ kóòdù) fa. * +* **Ọgbọ́n àti ìmọ̀**: Atilẹyin olootu nla. Ìparí nibi gbogbo. Àkókò díẹ̀ nipa wíwá ibi tí ìṣòro kóòdù wà. +* **Irọrun**: A kọ kí ó le rọrun láti lo àti láti kọ ẹkọ nínú rè. Ó máa fún ọ ní àkókò díẹ̀ látı ka àkọsílẹ. +* **Ó kúkurú ní kikọ**: Ó dín àtúnkọ àti àtúntò kóòdù kù. Ìkéde àṣàyàn kọ̀ọ̀kan nínú rẹ̀ ní ọ̀pọ̀lọpọ̀ àwọn ìlò. O ṣe iranlọwọ láti má ṣe ní ọ̀pọ̀lọpọ̀ àṣìṣe. +* **Ó lágbára**: Ó ń ṣe àgbéjáde kóòdù tí ó ṣetán fún ìṣelọ́pọ̀. Pẹ̀lú àkọsílẹ̀ tí ó máa ṣàlàyé ara rẹ̀ fún ẹ ní ìbáṣepọ̀ aládàáṣiṣẹ́ pẹ̀lú rè. +* **Ajohunše/Ìtọ́kasí**: Ó da lori (àti ibamu ni kikun pẹ̀lú) àwọn ìmọ ajohunše/ìtọ́kasí fún àwọn API: OpenAPI (èyí tí a mọ tẹlẹ si Swagger) àti JSON Schema. + +* iṣiro yi da lori àwọn idanwo tí ẹgbẹ ìdàgbàsókè FastAPI ṣe, nígbàtí wọn kọ àwọn ohun elo iṣelọpọ kóòdù pẹ̀lú rẹ. + +## Àwọn onígbọ̀wọ́ + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +Àwọn onígbọ̀wọ́ míràn + +## Àwọn ero àti èsì + +"_[...] Mò ń lo **FastAPI** púpọ̀ ní lẹ́nu àìpẹ́ yìí. [...] Mo n gbero láti lo o pẹ̀lú àwọn ẹgbẹ mi fún gbogbo iṣẹ **ML wa ni Microsoft**. Diẹ nínú wọn ni afikun ti ifilelẹ àwọn ẹya ara ti ọja **Windows** wa pẹ̀lú àwọn ti **Office**._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_A gba àwọn ohun èlò ìwé afọwọkọ **FastAPI** tí kò yí padà láti ṣẹ̀dá olùpín **REST** tí a lè béèrè lọ́wọ́ rẹ̀ láti gba **àsọtẹ́lẹ̀**. [fún Ludwig]_" + +
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** ni inudidun láti kede itusilẹ orisun kóòdù ti ìlànà iṣọkan **iṣakoso Ìṣòro** wa: **Ìfiránṣẹ́**! [a kọ pẹ̀lú **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_Inú mi dùn púpọ̀ nípa **FastAPI**. Ó mú inú ẹnì dùn púpọ̀!_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Ní tòótọ́, ohun tí o kọ dára ó sì tún dán. Ní ọ̀pọ̀lọpọ̀ ọ̀nà, ohun tí mo fẹ́ kí **Hug** jẹ́ nìyẹn - ó wúni lórí gan-an láti rí ẹnìkan tí ó kọ́ nǹkan bí èyí._" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"_Ti o ba n wa láti kọ ọkan **ìlànà igbalode** fún kikọ àwọn REST API, ṣayẹwo **FastAPI** [...] Ó yára, ó rọrùn láti lò, ó sì rọrùn láti kọ́[...]_" + +"_A ti yipada si **FastAPI** fún **APIs** wa [...] Mo lérò pé wà á fẹ́ràn rẹ̀ [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+ +--- + +"_Ti ẹnikẹni ba n wa láti kọ iṣelọpọ API pẹ̀lú Python, èmi yóò ṣe'dúró fún **FastAPI**. Ó jẹ́ ohun tí **àgbékalẹ̀ rẹ̀ lẹ́wà**, **ó rọrùn láti lò** àti wipe ó ni **ìwọ̀n gíga**, o tí dí **bọtini paati** nínú alakọkọ API ìdàgbàsókè kikọ fún wa, àti pe o ni ipa lori adaṣiṣẹ àti àwọn iṣẹ gẹ́gẹ́ bíi Onímọ̀-ẹ̀rọ TAC tí órí Íńtánẹ́ẹ̀tì_" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## **Typer**, FastAPI ti CLIs + + + +Ti o ba n kọ ohun èlò CLI láti ṣeé lọ nínú ohun èlò lori ebute kọmputa dipo API, ṣayẹwo **Typer**. + +**Typer** jẹ́ àbúrò ìyá FastAPI kékeré. Àti pé wọ́n kọ́ láti jẹ́ **FastAPI ti CLIs**. ⌨️ 🚀 + +## Èròjà + +Python 3.8+ + +FastAPI dúró lórí àwọn èjìká tí àwọn òmíràn: + +* Starlette fún àwọn ẹ̀yà ayélujára. +* Pydantic fún àwọn ẹ̀yà àkójọf'áyẹ̀wò. + +## Fifi sórí ẹrọ + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+Iwọ yóò tún nílò olupin ASGI, fún iṣelọpọ bii Uvicorn tabi Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## Àpẹẹrẹ + +### Ṣẹ̀dá rẹ̀ + +* Ṣẹ̀dá fáìlì `main.py (èyí tíí ṣe, akọkọ.py)` pẹ̀lú: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+Tàbí lò async def... + +Tí kóòdù rẹ̀ bá ń lò `async` / `await`, lò `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Akiyesi**: + +Tí o kò bá mọ̀, ṣàyẹ̀wò ibi tí a ti ní _"In a hurry?"_ (i.e. _"Ní kíákíá?"_) nípa `async` and `await` nínú àkọsílẹ̀. + +
+ +### Mu ṣiṣẹ + +Mú olupin ṣiṣẹ pẹ̀lú: + +
+ +```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. +``` + +
+ +
+Nipa aṣẹ kóòdù náà uvicorn main:app --reload... + +Àṣẹ `uvicorn main:app` ń tọ́ka sí: + +* `main`: fáìlì náà 'main.py' (Python "module"). +* `app` jẹ object( i.e. nǹkan) tí a ṣẹ̀dá nínú `main.py` pẹ̀lú ilà `app = FastAPI()`. +* `--reload`: èyí yóò jẹ́ ki olupin tún bẹ̀rẹ̀ lẹ́hìn àwọn àyípadà kóòdù. Jọ̀wọ́, ṣe èyí fún ìdàgbàsókè kóòdù nìkan, má ṣe é ṣe lori àgbéjáde kóòdù tabi fún iṣelọpọ kóòdù. + + +
+ +### Ṣayẹwo rẹ + +Ṣii aṣàwákiri kọ̀ǹpútà rẹ ni http://127.0.0.1:8000/items/5?q=somequery. + +Ìwọ yóò sì rí ìdáhùn JSON bíi: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +O tí ṣẹ̀dá API èyí tí yóò: + +* Gbà àwọn ìbéèrè HTTP ni àwọn _ipa ọ̀nà_ `/` àti `/items/{item_id}`. +* Èyí tí àwọn _ipa ọ̀nà_ (i.e. _paths_) méjèèjì gbà àwọn iṣẹ `GET` (a tun mọ si _àwọn ọna_ HTTP). +* Èyí tí _ipa ọ̀nà_ (i.e. _paths_) `/items/{item_id}` ní _àwọn ohun-ini ipa ọ̀nà_ tí ó yẹ kí ó jẹ́ `int` i.e. `ÒǸKÀ`. +* Èyí tí _ipa ọ̀nà_ (i.e. _paths_) `/items/{item_id}` ní àṣàyàn `str` _àwọn ohun-ini_ (i.e. _query parameter_) `q`. + +### Ìbáṣepọ̀ àkọsílẹ̀ API + +Ní báyìí, lọ sí http://127.0.0.1:8000/docs. + +Lẹ́yìn náà, iwọ yóò rí ìdáhùn àkọsílẹ̀ API tí ó jẹ́ ìbáṣepọ̀ alaifọwọyi/aládàáṣiṣẹ́ (tí a pèṣè nípaṣẹ̀ Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Ìdàkejì àkọsílẹ̀ API + +Ní báyìí, lọ sí http://127.0.0.1:8000/redoc. + +Wà á rí àwọn àkọsílẹ̀ aládàáṣiṣẹ́ mìíràn (tí a pese nipasẹ ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Àpẹẹrẹ ìgbésókè mìíràn + +Ní báyìí ṣe àtúnṣe fáìlì `main.py` láti gba kókó èsì láti inú ìbéèrè `PUT`. + +Ní báyìí, ṣe ìkéde kókó èsì API nínú kóòdù rẹ nipa lílo àwọn ìtọ́kasí àmì irúfẹ́ Python, ọpẹ́ pàtàkìsi sí Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +Olupin yóò tún ṣe àtúnṣe laifọwọyi/aládàáṣiṣẹ́ (nítorí wípé ó se àfikún `-reload` si àṣẹ kóòdù `uvicorn` lókè). + +### Ìbáṣepọ̀ ìgbésókè àkọsílẹ̀ API + +Ní báyìí, lọ sí http://127.0.0.1:8000/docs. + +* Ìbáṣepọ̀ àkọsílẹ̀ API yóò ṣe imudojuiwọn àkọsílẹ̀ API laifọwọyi, pẹ̀lú kókó èsì ìdáhùn API tuntun: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Tẹ bọtini "Gbiyanju rẹ" i.e. "Try it out", yóò gbà ọ́ láàyè láti jẹ́ kí ó tẹ́ àlàyé tí ó nílò kí ó le sọ̀rọ̀ tààrà pẹ̀lú API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Lẹhinna tẹ bọtini "Ṣiṣe" i.e. "Execute", olùmúlò (i.e. user interface) yóò sọrọ pẹ̀lú API rẹ, yóò ṣe afiranṣẹ àwọn èròjà, pàápàá jùlọ yóò gba àwọn àbájáde yóò si ṣafihan wọn loju ìbòjú: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Ìdàkejì ìgbésókè àkọsílẹ̀ API + +Ní báyìí, lọ sí http://127.0.0.1:8000/redoc. + +* Ìdàkejì àkọsílẹ̀ API yóò ṣ'afihan ìbéèrè èròjà/pàrámítà tuntun àti kókó èsì ti API: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Àtúnyẹ̀wò + +Ni akopọ, ìwọ yóò kéde ni **kete** àwọn iru èròjà/pàrámítà, kókó èsì API, abbl (i.e. àti bẹbẹ lọ), bi àwọn èròjà iṣẹ. + +O ṣe ìyẹn pẹ̀lú irúfẹ́ àmì ìtọ́kasí ìgbàlódé Python. + +O ò nílò láti kọ́ síńtáàsì tuntun, ìlànà tàbí ọ̀wọ́ kíláàsì kan pàtó, abbl (i.e. àti bẹbẹ lọ). + +Ìtọ́kasí **Python 3.8+** + +Fún àpẹẹrẹ, fún `int`: + +```Python +item_id: int +``` + +tàbí fún àwòṣe `Item` tí ó nira díẹ̀ síi: + +```Python +item: Item +``` + +... àti pẹ̀lú ìkéde kan ṣoṣo yẹn ìwọ yóò gbà: + +* Atilẹyin olootu, pẹ̀lú: + * Pipari. + * Àyẹ̀wò irúfẹ́ àmì ìtọ́kasí. +* Ìfọwọ́sí àkójọf'áyẹ̀wò (i.e. data): + * Aṣiṣe alaifọwọyi/aládàáṣiṣẹ́ àti aṣiṣe ti ó hàn kedere nígbàtí àwọn àkójọf'áyẹ̀wò (i.e. data) kò wulo tabi tí kò fẹsẹ̀ múlẹ̀. + * Ìfọwọ́sí fún ohun elo JSON tí ó jìn gan-an. +* Ìyípadà tí input àkójọf'áyẹ̀wò: tí ó wà láti nẹtiwọọki si àkójọf'áyẹ̀wò àti irúfẹ́ àmì ìtọ́kasí Python. Ó ń ka láti: + * JSON. + * èròjà ọ̀nà tí ò gbé gbà. + * èròjà ìbéèrè. + * Àwọn Kúkì + * Àwọn Àkọlé + * Àwọn Fọọmu + * Àwọn Fáìlì +* Ìyípadà èsì àkójọf'áyẹ̀wò: yíyípadà láti àkójọf'áyẹ̀wò àti irúfẹ́ àmì ìtọ́kasí Python si nẹtiwọọki (gẹ́gẹ́ bí JSON): + * Yí irúfẹ́ àmì ìtọ́kasí padà (`str`, `int`, `float`, `bool`, `list`, abbl i.e. àti bèbè ló). + * Àwọn ohun èlò `datetime`. + * Àwọn ohun èlò `UUID`. + * Àwọn awoṣẹ́ ibi ìpamọ́ àkójọf'áyẹ̀wò. + * ...àti ọ̀pọ̀lọpọ̀ díẹ̀ síi. +* Ìbáṣepọ̀ àkọsílẹ̀ API aládàáṣiṣẹ́, pẹ̀lú ìdàkejì àgbékalẹ̀-àwọn-olùmúlò (i.e user interfaces) méjì: + * Àgbékalẹ̀-olùmúlò Swagger. + * ReDoc. + +--- + +Nisinsin yi, tí ó padà sí àpẹẹrẹ ti tẹ́lẹ̀, **FastAPI** yóò: + +* Fọwọ́ sí i pé `item_id` wà nínú ọ̀nà ìbéèrè HTTP fún `GET` àti `PUT`. +* Fọwọ́ sí i pé `item_id` jẹ́ irúfẹ́ àmì ìtọ́kasí `int` fún ìbéèrè HTTP `GET` àti `PUT`. + * Tí kìí bá ṣe bẹ, oníbàárà yóò ríi àṣìṣe tí ó wúlò, kedere. +* Ṣàyẹ̀wò bóyá ìbéèrè àṣàyàn pàrámítà kan wà tí orúkọ rẹ̀ ń jẹ́ `q` (gẹ́gẹ́ bíi `http://127.0.0.1:8000/items/foo?q=somequery`) fún ìbéèrè HTTP `GET`. + * Bí wọ́n ṣe kéde pàrámítà `q` pẹ̀lú `= None`, ó jẹ́ àṣàyàn (i.e optional). + * Láìsí `None` yóò nílò (gẹ́gẹ́ bí kókó èsì ìbéèrè HTTP ṣe wà pẹ̀lú `PUT`). +* Fún àwọn ìbéèrè HTTP `PUT` sí `/items/{item_id}`, kà kókó èsì ìbéèrè HTTP gẹ́gẹ́ bí JSON: + * Ṣàyẹ̀wò pé ó ní àbùdá tí ó nílò èyí tíí ṣe `name` i.e. `orúkọ` tí ó yẹ kí ó jẹ́ `str`. + * Ṣàyẹ̀wò pé ó ní àbùdá tí ó nílò èyí tíí ṣe `price` i.e. `iye` tí ó gbọ́dọ̀ jẹ́ `float`. + * Ṣàyẹ̀wò pé ó ní àbùdá àṣàyàn `is_offer`, tí ó yẹ kí ó jẹ́ `bool`, tí ó bá wà níbẹ̀. + * Gbogbo èyí yóò tún ṣiṣẹ́ fún àwọn ohun èlò JSON tí ó jìn gidi gan-an. +* Yìí padà láti àti sí JSON lai fi ọwọ́ yi. +* Ṣe àkọsílẹ̀ ohun gbogbo pẹ̀lú OpenAPI, èyí tí yóò wà ní lílo nípaṣẹ̀: + * Àwọn ètò àkọsílẹ̀ ìbáṣepọ̀. + * Aládàáṣiṣẹ́ oníbárà èlètò tíí ṣẹ̀dá kóòdù, fún ọ̀pọ̀lọpọ̀ àwọn èdè. +* Pese àkọsílẹ̀ òní ìbáṣepọ̀ ti àwọn àgbékalẹ̀ ayélujára méjì tààrà. + +--- + +A ń ṣẹ̀ṣẹ̀ ń mú ẹyẹ bọ́ làpò ní, ṣùgbọ́n ó ti ni òye bí gbogbo rẹ̀ ṣe ń ṣiṣẹ́. + +Gbiyanju láti yí ìlà padà pẹ̀lú: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...láti: + +```Python + ... "item_name": item.name ... +``` + +...ṣí: + +```Python + ... "item_price": item.price ... +``` + +.. kí o sì wo bí olóòtú rẹ yóò ṣe parí àwọn àbùdá náà fúnra rẹ̀, yóò sì mọ irúfẹ́ wọn: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +Fún àpẹẹrẹ pípé síi pẹ̀lú àwọn àbùdá mìíràn, wo Ìdánilẹ́kọ̀ọ́ - Ìtọ́sọ́nà Olùmúlò. + +**Itaniji gẹ́gẹ́ bí isọ'ye**: ìdánilẹ́kọ̀ọ́ - itọsọna olùmúlò pẹ̀lú: + +* Ìkéde àṣàyàn **pàrámítà** láti àwọn oriṣiriṣi ibòmíràn gẹ́gẹ́ bíi: àwọn **àkọlé èsì API**, **kúkì**, **ààyè fọọmu**, àti **fáìlì**. +* Bíi ó ṣe lé ṣètò **àwọn ìdíwọ́ ìfọwọ́sí** bí `maximum_length` tàbí `regex`. +* Ó lágbára púpọ̀ ó sì rọrùn láti lo ètò **Àfikún Ìgbẹ́kẹ̀lé Kóòdù**. +* Ààbò àti ìfọwọ́sowọ́pọ̀, pẹ̀lú àtìlẹ́yìn fún **OAuth2** pẹ̀lú **àmì JWT** àti **HTTP Ipilẹ ìfọwọ́sowọ́pọ̀**. +* Àwọn ìlànà ìlọsíwájú (ṣùgbọ́n tí ó rọrùn bákan náà) fún ìkéde **àwọn àwòṣe JSON tó jinlẹ̀** (ọpẹ́ pàtàkìsi sí Pydantic). +* Iṣọpọ **GraphQL** pẹ̀lú Strawberry àti àwọn ohun èlò ìwé kóòdù afọwọkọ mìíràn tí kò yí padà. +* Ọpọlọpọ àwọn àfikún àwọn ẹ̀yà (ọpẹ́ pàtàkìsi sí Starlette) bí: + * **WebSockets** + * àwọn ìdánwò tí ó rọrùn púpọ̀ lórí HTTPX àti `pytest` + * **CORS** + * **Cookie Sessions** + * ...àti síwájú síi. + +## Ìṣesí + +Àwọn àlá TechEmpower fi hàn pé **FastAPI** ń ṣiṣẹ́ lábẹ́ Uvicorn gẹ́gẹ́ bí ọ̀kan lára àwọn ìlànà Python tí ó yára jùlọ tí ó wà, ní ìsàlẹ̀ Starlette àti Uvicorn fúnra wọn (tí FastAPI ń lò fúnra rẹ̀). (*) + +Láti ní òye síi nípa rẹ̀, wo abala àwọn Àlá. + +## Àṣàyàn Àwọn Àfikún Ìgbẹ́kẹ̀lé Kóòdù + +Èyí tí Pydantic ń lò: + +* email_validator - fún ifọwọsi ímeèlì. +* pydantic-settings - fún ètò ìsàkóso. +* pydantic-extra-types - fún àfikún oríṣi láti lọ pẹ̀lú Pydantic. + +Èyí tí Starlette ń lò: + +* httpx - Nílò tí ó bá fẹ́ láti lọ `TestClient`. +* jinja2 - Nílò tí ó bá fẹ́ láti lọ iṣeto awoṣe aiyipada. +* python-multipart - Nílò tí ó bá fẹ́ láti ṣe àtìlẹ́yìn fún "àyẹ̀wò" fọọmu, pẹ̀lú `request.form()`. +* itsdangerous - Nílò fún àtìlẹ́yìn `SessionMiddleware`. +* pyyaml - Nílò fún àtìlẹ́yìn Starlette's `SchemaGenerator` (ó ṣe ṣe kí ó má nílò rẹ̀ fún FastAPI). +* ujson - Nílò tí ó bá fẹ́ láti lọ `UJSONResponse`. + +Èyí tí FastAPI / Starlette ń lò: + +* uvicorn - Fún olupin tí yóò sẹ́ àmúyẹ àti tí yóò ṣe ìpèsè fún iṣẹ́ rẹ tàbí ohun èlò rẹ. +* orjson - Nílò tí ó bá fẹ́ láti lọ `ORJSONResponse`. + +Ó lè fi gbogbo àwọn wọ̀nyí sórí ẹrọ pẹ̀lú `pip install "fastapi[all]"`. + +## Iwe-aṣẹ + +Iṣẹ́ yìí ni iwe-aṣẹ lábẹ́ àwọn òfin tí iwe-aṣẹ MIT. diff --git a/docs/yo/mkdocs.yml b/docs/yo/mkdocs.yml new file mode 100644 index 0000000000..de18856f44 --- /dev/null +++ b/docs/yo/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml diff --git a/docs/zh/docs/advanced/additional-responses.md b/docs/zh/docs/advanced/additional-responses.md new file mode 100644 index 0000000000..2a1e1ed891 --- /dev/null +++ b/docs/zh/docs/advanced/additional-responses.md @@ -0,0 +1,219 @@ +# OPENAPI 中的其他响应 + +您可以声明附加响应,包括附加状态代码、媒体类型、描述等。 + +这些额外的响应将包含在OpenAPI模式中,因此它们也将出现在API文档中。 + +但是对于那些额外的响应,你必须确保你直接返回一个像 `JSONResponse` 一样的 `Response` ,并包含你的状态代码和内容。 + +## `model`附加响应 +您可以向路径操作装饰器传递参数 `responses` 。 + +它接收一个 `dict`,键是每个响应的状态代码(如`200`),值是包含每个响应信息的其他 `dict`。 + +每个响应字典都可以有一个关键模型,其中包含一个 `Pydantic` 模型,就像 `response_model` 一样。 + +**FastAPI**将采用该模型,生成其`JSON Schema`并将其包含在`OpenAPI`中的正确位置。 + +例如,要声明另一个具有状态码 `404` 和`Pydantic`模型 `Message` 的响应,可以写: +```Python hl_lines="18 22" +{!../../../docs_src/additional_responses/tutorial001.py!} +``` + + +!!! Note + 请记住,您必须直接返回 `JSONResponse` 。 + +!!! Info + `model` 密钥不是OpenAPI的一部分。 + **FastAPI**将从那里获取`Pydantic`模型,生成` JSON Schema` ,并将其放在正确的位置。 + - 正确的位置是: + - 在键 `content` 中,其具有另一个`JSON`对象( `dict` )作为值,该`JSON`对象包含: + - 媒体类型的密钥,例如 `application/json` ,它包含另一个`JSON`对象作为值,该对象包含: + - 一个键` schema` ,它的值是来自模型的`JSON Schema`,正确的位置在这里。 + - **FastAPI**在这里添加了对OpenAPI中另一个地方的全局JSON模式的引用,而不是直接包含它。这样,其他应用程序和客户端可以直接使用这些JSON模式,提供更好的代码生成工具等。 + + +**在OpenAPI中为该路径操作生成的响应将是:** + +```json hl_lines="3-12" +{ + "responses": { + "404": { + "description": "Additional Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } +} + +``` +**模式被引用到OpenAPI模式中的另一个位置:** +```json hl_lines="4-16" +{ + "components": { + "schemas": { + "Message": { + "title": "Message", + "required": [ + "message" + ], + "type": "object", + "properties": { + "message": { + "title": "Message", + "type": "string" + } + } + }, + "Item": { + "title": "Item", + "required": [ + "id", + "value" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + } + }, + "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" + } + } + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + } + } + } + } + } + } +} + +``` +## 主响应的其他媒体类型 + +您可以使用相同的 `responses` 参数为相同的主响应添加不同的媒体类型。 + +例如,您可以添加一个额外的媒体类型` image/png` ,声明您的路径操作可以返回JSON对象(媒体类型 `application/json` )或PNG图像: + +```Python hl_lines="19-24 28" +{!../../../docs_src/additional_responses/tutorial002.py!} +``` + +!!! Note + - 请注意,您必须直接使用 `FileResponse` 返回图像。 + +!!! Info + - 除非在 `responses` 参数中明确指定不同的媒体类型,否则**FastAPI**将假定响应与主响应类具有相同的媒体类型(默认为` application/json` )。 + - 但是如果您指定了一个自定义响应类,并将 `None `作为其媒体类型,**FastAPI**将使用 `application/json` 作为具有关联模型的任何其他响应。 + +## 组合信息 +您还可以联合接收来自多个位置的响应信息,包括 `response_model `、 `status_code` 和 `responses `参数。 + +您可以使用默认的状态码 `200` (或者您需要的自定义状态码)声明一个 `response_model `,然后直接在OpenAPI模式中在 `responses` 中声明相同响应的其他信息。 + +**FastAPI**将保留来自 `responses` 的附加信息,并将其与模型中的JSON Schema结合起来。 + +例如,您可以使用状态码 `404` 声明响应,该响应使用`Pydantic`模型并具有自定义的` description` 。 + +以及一个状态码为 `200` 的响应,它使用您的 `response_model` ,但包含自定义的 `example` : + +```Python hl_lines="20-31" +{!../../../docs_src/additional_responses/tutorial003.py!} +``` + +所有这些都将被合并并包含在您的OpenAPI中,并在API文档中显示: + +## 联合预定义响应和自定义响应 + +您可能希望有一些应用于许多路径操作的预定义响应,但是你想将不同的路径和自定义的相应组合在一块。 +对于这些情况,你可以使用Python的技术,将 `dict` 与 `**dict_to_unpack` 解包: +```Python +old_dict = { + "old key": "old value", + "second old key": "second old value", +} +new_dict = {**old_dict, "new key": "new value"} +``` + +这里, new_dict 将包含来自 old_dict 的所有键值对加上新的键值对: +```python +{ + "old key": "old value", + "second old key": "second old value", + "new key": "new value", +} +``` +您可以使用该技术在路径操作中重用一些预定义的响应,并将它们与其他自定义响应相结合。 +**例如:** +```Python hl_lines="13-17 26" +{!../../../docs_src/additional_responses/tutorial004.py!} +``` +## 有关OpenAPI响应的更多信息 + +要了解您可以在响应中包含哪些内容,您可以查看OpenAPI规范中的以下部分: + + [OpenAPI响应对象](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#responsesObject),它包括 Response Object 。 + + [OpenAPI响应对象](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#responseObject),您可以直接在 `responses` 参数中的每个响应中包含任何内容。包括 `description` 、 `headers` 、 `content` (其中是声明不同的媒体类型和JSON Schemas)和 `links` 。 diff --git a/docs/zh/docs/advanced/generate-clients.md b/docs/zh/docs/advanced/generate-clients.md new file mode 100644 index 0000000000..e222e479c8 --- /dev/null +++ b/docs/zh/docs/advanced/generate-clients.md @@ -0,0 +1,266 @@ +# 生成客户端 + +因为 **FastAPI** 是基于OpenAPI规范的,自然您可以使用许多相匹配的工具,包括自动生成API文档 (由 Swagger UI 提供)。 + +一个不太明显而又特别的优势是,你可以为你的API针对不同的**编程语言**来**生成客户端**(有时候被叫做 **SDKs** )。 + +## OpenAPI 客户端生成 + +有许多工具可以从**OpenAPI**生成客户端。 + +一个常见的工具是 OpenAPI Generator。 + +如果您正在开发**前端**,一个非常有趣的替代方案是 openapi-typescript-codegen。 + +## 生成一个 TypeScript 前端客户端 + +让我们从一个简单的 FastAPI 应用开始: + +=== "Python 3.9+" + + ```Python hl_lines="7-9 12-13 16-17 21" + {!> ../../../docs_src/generate_clients/tutorial001_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9-11 14-15 18 19 23" + {!> ../../../docs_src/generate_clients/tutorial001.py!} + ``` + +请注意,*路径操作* 定义了他们所用于请求数据和回应数据的模型,所使用的模型是`Item` 和 `ResponseMessage`。 + +### API 文档 + +如果您访问API文档,您将看到它具有在请求中发送和在响应中接收数据的**模式(schemas)**: + + + +您可以看到这些模式,因为它们是用程序中的模型声明的。 + +那些信息可以在应用的 **OpenAPI模式** 被找到,然后显示在API文档中(通过Swagger UI)。 + +OpenAPI中所包含的模型里有相同的信息可以用于 **生成客户端代码**。 + +### 生成一个TypeScript 客户端 + +现在我们有了带有模型的应用,我们可以为前端生成客户端代码。 + +#### 安装 `openapi-typescript-codegen` + +您可以使用以下工具在前端代码中安装 `openapi-typescript-codegen`: + +
+ +```console +$ npm install openapi-typescript-codegen --save-dev + +---> 100% +``` + +
+ +#### 生成客户端代码 + +要生成客户端代码,您可以使用现在将要安装的命令行应用程序 `openapi`。 + +因为它安装在本地项目中,所以您可能无法直接使用此命令,但您可以将其放在 `package.json` 文件中。 + +它可能看起来是这样的: + +```JSON hl_lines="7" +{ + "name": "frontend-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios" + }, + "author": "", + "license": "", + "devDependencies": { + "openapi-typescript-codegen": "^0.20.1", + "typescript": "^4.6.2" + } +} +``` + +在这里添加 NPM `generate-client` 脚本后,您可以使用以下命令运行它: + +
+ +```console +$ npm run generate-client + +frontend-app@1.0.0 generate-client /home/user/code/frontend-app +> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios +``` + +
+ +此命令将在 `./src/client` 中生成代码,并将在其内部使用 `axios`(前端HTTP库)。 + +### 尝试客户端代码 + +现在您可以导入并使用客户端代码,它可能看起来像这样,请注意,您可以为这些方法使用自动补全: + + + +您还将自动补全要发送的数据: + + + +!!! tip + 请注意, `name` 和 `price` 的自动补全,是通过其在`Item`模型(FastAPI)中的定义实现的。 + +如果发送的数据字段不符,你也会看到编辑器的错误提示: + + + +响应(response)对象也拥有自动补全: + + + +## 带有标签的 FastAPI 应用 + +在许多情况下,你的FastAPI应用程序会更复杂,你可能会使用标签来分隔不同组的*路径操作(path operations)*。 + +例如,您可以有一个用 `items` 的部分和另一个用于 `users` 的部分,它们可以用标签来分隔: + +=== "Python 3.9+" + + ```Python hl_lines="21 26 34" + {!> ../../../docs_src/generate_clients/tutorial002_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="23 28 36" + {!> ../../../docs_src/generate_clients/tutorial002.py!} + ``` + +### 生成带有标签的 TypeScript 客户端 + +如果您使用标签为FastAPI应用生成客户端,它通常也会根据标签分割客户端代码。 + +通过这种方式,您将能够为客户端代码进行正确地排序和分组: + + + +在这个案例中,您有: + +* `ItemsService` +* `UsersService` + +### 客户端方法名称 + +现在生成的方法名像 `createItemItemsPost` 看起来不太简洁: + +```TypeScript +ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) +``` + +...这是因为客户端生成器为每个 *路径操作* 使用OpenAPI的内部 **操作 ID(operation ID)**。 + +OpenAPI要求每个操作 ID 在所有 *路径操作* 中都是唯一的,因此 FastAPI 使用**函数名**、**路径**和**HTTP方法/操作**来生成此操作ID,因为这样可以确保这些操作 ID 是唯一的。 + +但接下来我会告诉你如何改进。 🤓 + +## 自定义操作ID和更好的方法名 + +您可以**修改**这些操作ID的**生成**方式,以使其更简洁,并在客户端中具有**更简洁的方法名称**。 + +在这种情况下,您必须确保每个操作ID在其他方面是**唯一**的。 + +例如,您可以确保每个*路径操作*都有一个标签,然后根据**标签**和*路径操作***名称**(函数名)来生成操作ID。 + +### 自定义生成唯一ID函数 + +FastAPI为每个*路径操作*使用一个**唯一ID**,它用于**操作ID**,也用于任何所需自定义模型的名称,用于请求或响应。 + +你可以自定义该函数。它接受一个 `APIRoute` 对象作为输入,并输出一个字符串。 + +例如,以下是一个示例,它使用第一个标签(你可能只有一个标签)和*路径操作*名称(函数名)。 + +然后,你可以将这个自定义函数作为 `generate_unique_id_function` 参数传递给 **FastAPI**: + +=== "Python 3.9+" + + ```Python hl_lines="6-7 10" + {!> ../../../docs_src/generate_clients/tutorial003_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8-9 12" + {!> ../../../docs_src/generate_clients/tutorial003.py!} + ``` + +### 使用自定义操作ID生成TypeScript客户端 + +现在,如果你再次生成客户端,你会发现它具有改善的方法名称: + + + +正如你所见,现在方法名称中只包含标签和函数名,不再包含URL路径和HTTP操作的信息。 + +### 预处理用于客户端生成器的OpenAPI规范 + +生成的代码仍然存在一些**重复的信息**。 + +我们已经知道该方法与 **items** 相关,因为它在 `ItemsService` 中(从标签中获取),但方法名中仍然有标签名作为前缀。😕 + +一般情况下对于OpenAPI,我们可能仍然希望保留它,因为这将确保操作ID是**唯一的**。 + +但对于生成的客户端,我们可以在生成客户端之前**修改** OpenAPI 操作ID,以使方法名称更加美观和**简洁**。 + +我们可以将 OpenAPI JSON 下载到一个名为`openapi.json`的文件中,然后使用以下脚本**删除此前缀的标签**: + +```Python +{!../../../docs_src/generate_clients/tutorial004.py!} +``` + +通过这样做,操作ID将从类似于 `items-get_items` 的名称重命名为 `get_items` ,这样客户端生成器就可以生成更简洁的方法名称。 + +### 使用预处理的OpenAPI生成TypeScript客户端 + +现在,由于最终结果保存在文件openapi.json中,你可以修改 package.json 文件以使用此本地文件,例如: + +```JSON hl_lines="7" +{ + "name": "frontend-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "generate-client": "openapi --input ./openapi.json --output ./src/client --client axios" + }, + "author": "", + "license": "", + "devDependencies": { + "openapi-typescript-codegen": "^0.20.1", + "typescript": "^4.6.2" + } +} +``` + +生成新的客户端之后,你现在将拥有**清晰的方法名称**,具备**自动补全**、**错误提示**等功能: + + + +## 优点 + +当使用自动生成的客户端时,你将获得以下的自动补全功能: + +* 方法。 +* 请求体中的数据、查询参数等。 +* 响应数据。 + +你还将获得针对所有内容的错误提示。 + +每当你更新后端代码并**重新生成**前端代码时,新的*路径操作*将作为方法可用,旧的方法将被删除,并且其他任何更改将反映在生成的代码中。 🤓 + +这也意味着如果有任何更改,它将自动**反映**在客户端代码中。如果你**构建**客户端,在使用的数据上存在**不匹配**时,它将报错。 + +因此,你将在开发周期的早期**检测到许多错误**,而不必等待错误在生产环境中向最终用户展示,然后尝试调试问题所在。 ✨ diff --git a/docs/zh/docs/advanced/index.md b/docs/zh/docs/advanced/index.md index d71838cd77..824f91f47b 100644 --- a/docs/zh/docs/advanced/index.md +++ b/docs/zh/docs/advanced/index.md @@ -1,4 +1,4 @@ -# 高级用户指南 - 简介 +# 高级用户指南 ## 额外特性 diff --git a/docs/zh/docs/advanced/response-change-status-code.md b/docs/zh/docs/advanced/response-change-status-code.md new file mode 100644 index 0000000000..a289cf2017 --- /dev/null +++ b/docs/zh/docs/advanced/response-change-status-code.md @@ -0,0 +1,31 @@ +# 响应 - 更改状态码 + +你可能之前已经了解到,你可以设置默认的[响应状态码](../tutorial/response-status-code.md){.internal-link target=_blank}。 + +但在某些情况下,你需要返回一个不同于默认值的状态码。 + +## 使用场景 + +例如,假设你想默认返回一个HTTP状态码为“OK”`200`。 + +但如果数据不存在,你想创建它,并返回一个HTTP状态码为“CREATED”`201`。 + +但你仍然希望能够使用`response_model`过滤和转换你返回的数据。 + +对于这些情况,你可以使用一个`Response`参数。 + +## 使用 `Response` 参数 + +你可以在你的*路径操作函数*中声明一个`Response`类型的参数(就像你可以为cookies和头部做的那样)。 + +然后你可以在这个*临时*响应对象中设置`status_code`。 + +```Python hl_lines="1 9 12" +{!../../../docs_src/response_change_status_code/tutorial001.py!} +``` + +然后你可以像平常一样返回任何你需要的对象(例如一个`dict`或者一个数据库模型)。如果你声明了一个`response_model`,它仍然会被用来过滤和转换你返回的对象。 + +**FastAPI**将使用这个临时响应来提取状态码(也包括cookies和头部),并将它们放入包含你返回的值的最终响应中,该响应由任何`response_model`过滤。 + +你也可以在依赖项中声明`Response`参数,并在其中设置状态码。但请注意,最后设置的状态码将会生效。 diff --git a/docs/zh/docs/advanced/response-headers.md b/docs/zh/docs/advanced/response-headers.md new file mode 100644 index 0000000000..85dab15ac0 --- /dev/null +++ b/docs/zh/docs/advanced/response-headers.md @@ -0,0 +1,39 @@ +# 响应头 + +## 使用 `Response` 参数 + +你可以在你的*路径操作函数*中声明一个`Response`类型的参数(就像你可以为cookies做的那样)。 + +然后你可以在这个*临时*响应对象中设置头部。 +```Python hl_lines="1 7-8" +{!../../../docs_src/response_headers/tutorial002.py!} +``` + +然后你可以像平常一样返回任何你需要的对象(例如一个`dict`或者一个数据库模型)。如果你声明了一个`response_model`,它仍然会被用来过滤和转换你返回的对象。 + +**FastAPI**将使用这个临时响应来提取头部(也包括cookies和状态码),并将它们放入包含你返回的值的最终响应中,该响应由任何`response_model`过滤。 + +你也可以在依赖项中声明`Response`参数,并在其中设置头部(和cookies)。 + +## 直接返回 `Response` + +你也可以在直接返回`Response`时添加头部。 + +按照[直接返回响应](response-directly.md){.internal-link target=_blank}中所述创建响应,并将头部作为附加参数传递: +```Python hl_lines="10-12" +{!../../../docs_src/response_headers/tutorial001.py!} +``` + + +!!! 注意 "技术细节" + 你也可以使用`from starlette.responses import Response`或`from starlette.responses import JSONResponse`。 + + **FastAPI**提供了与`fastapi.responses`相同的`starlette.responses`,只是为了方便开发者。但是,大多数可用的响应都直接来自Starlette。 + + 由于`Response`经常用于设置头部和cookies,因此**FastAPI**还在`fastapi.Response`中提供了它。 + +## 自定义头部 + +请注意,可以使用'X-'前缀添加自定义专有头部。 + +但是,如果你有自定义头部,你希望浏览器中的客户端能够看到它们,你需要将它们添加到你的CORS配置中(在[CORS(跨源资源共享)](../tutorial/cors.md){.internal-link target=_blank}中阅读更多),使用在Starlette的CORS文档中记录的`expose_headers`参数。 diff --git a/docs/zh/docs/advanced/security/index.md b/docs/zh/docs/advanced/security/index.md new file mode 100644 index 0000000000..fdc8075c76 --- /dev/null +++ b/docs/zh/docs/advanced/security/index.md @@ -0,0 +1,16 @@ +# 高级安全 + +## 附加特性 + +除 [教程 - 用户指南: 安全性](../../tutorial/security/){.internal-link target=_blank} 中涵盖的功能之外,还有一些额外的功能来处理安全性. + +!!! tip "小贴士" + 接下来的章节 **并不一定是 "高级的"**. + + 而且对于你的使用场景来说,解决方案很可能就在其中。 + +## 先阅读教程 + +接下来的部分假设你已经阅读了主要的 [教程 - 用户指南: 安全性](../../tutorial/security/){.internal-link target=_blank}. + +它们都基于相同的概念,但支持一些额外的功能. diff --git a/docs/zh/docs/advanced/settings.md b/docs/zh/docs/advanced/settings.md new file mode 100644 index 0000000000..76070fb7fa --- /dev/null +++ b/docs/zh/docs/advanced/settings.md @@ -0,0 +1,433 @@ +# 设置和环境变量 + +在许多情况下,您的应用程序可能需要一些外部设置或配置,例如密钥、数据库凭据、电子邮件服务的凭据等等。 + +这些设置中的大多数是可变的(可以更改的),比如数据库的 URL。而且许多设置可能是敏感的,比如密钥。 + +因此,通常会将它们提供为由应用程序读取的环境变量。 + +## 环境变量 + +!!! tip + 如果您已经知道什么是"环境变量"以及如何使用它们,请随意跳到下面的下一节。 + +环境变量(也称为"env var")是一种存在于 Python 代码之外、存在于操作系统中的变量,可以被您的 Python 代码(或其他程序)读取。 + +您可以在 shell 中创建和使用环境变量,而无需使用 Python: + +=== "Linux、macOS、Windows Bash" + +
+ + ```console + // 您可以创建一个名为 MY_NAME 的环境变量 + $ export MY_NAME="Wade Wilson" + + // 然后您可以与其他程序一起使用它,例如 + $ echo "Hello $MY_NAME" + + Hello Wade Wilson + ``` + +
+ +=== "Windows PowerShell" + +
+ + ```console + // 创建一个名为 MY_NAME 的环境变量 + $ $Env:MY_NAME = "Wade Wilson" + + // 与其他程序一起使用它,例如 + $ echo "Hello $Env:MY_NAME" + + Hello Wade Wilson + ``` + +
+ +### 在 Python 中读取环境变量 + +您还可以在 Python 之外的地方(例如终端中或使用任何其他方法)创建环境变量,然后在 Python 中读取它们。 + +例如,您可以有一个名为 `main.py` 的文件,其中包含以下内容: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +!!! tip + `os.getenv()` 的第二个参数是要返回的默认值。 + + 如果没有提供默认值,默认为 `None`,此处我们提供了 `"World"` 作为要使用的默认值。 + +然后,您可以调用该 Python 程序: + +
+ +```console +// 这里我们还没有设置环境变量 +$ python main.py + +// 因为我们没有设置环境变量,所以我们得到默认值 + +Hello World from Python + +// 但是如果我们先创建一个环境变量 +$ export MY_NAME="Wade Wilson" + +// 然后再次调用程序 +$ python main.py + +// 现在它可以读取环境变量 + +Hello Wade Wilson from Python +``` + +
+ +由于环境变量可以在代码之外设置,但可以由代码读取,并且不需要与其他文件一起存储(提交到 `git`),因此通常将它们用于配置或设置。 + + + +您还可以仅为特定程序调用创建一个环境变量,该环境变量仅对该程序可用,并且仅在其运行期间有效。 + +要做到这一点,在程序本身之前的同一行创建它: + +
+ +```console +// 在此程序调用行中创建一个名为 MY_NAME 的环境变量 +$ MY_NAME="Wade Wilson" python main.py + +// 现在它可以读取环境变量 + +Hello Wade Wilson from Python + +// 之后环境变量不再存在 +$ python main.py + +Hello World from Python +``` + +
+ +!!! tip + 您可以在 Twelve-Factor App: Config 中阅读更多相关信息。 + +### 类型和验证 + +这些环境变量只能处理文本字符串,因为它们是外部于 Python 的,并且必须与其他程序和整个系统兼容(甚至与不同的操作系统,如 Linux、Windows、macOS)。 + +这意味着从环境变量中在 Python 中读取的任何值都将是 `str` 类型,任何类型的转换或验证都必须在代码中完成。 + +## Pydantic 的 `Settings` + +幸运的是,Pydantic 提供了一个很好的工具来处理来自环境变量的设置,即Pydantic: Settings management。 + +### 创建 `Settings` 对象 + +从 Pydantic 导入 `BaseSettings` 并创建一个子类,与 Pydantic 模型非常相似。 + +与 Pydantic 模型一样,您使用类型注释声明类属性,还可以指定默认值。 + +您可以使用与 Pydantic 模型相同的验证功能和工具,比如不同的数据类型和使用 `Field()` 进行附加验证。 + +```Python hl_lines="2 5-8 11" +{!../../../docs_src/settings/tutorial001.py!} +``` + +!!! tip + 如果您需要一个快速的复制粘贴示例,请不要使用此示例,而应使用下面的最后一个示例。 + +然后,当您创建该 `Settings` 类的实例(在此示例中是 `settings` 对象)时,Pydantic 将以不区分大小写的方式读取环境变量,因此,大写的变量 `APP_NAME` 仍将为属性 `app_name` 读取。 + +然后,它将转换和验证数据。因此,当您使用该 `settings` 对象时,您将获得您声明的类型的数据(例如 `items_per_user` 将为 `int` 类型)。 + +### 使用 `settings` + +然后,您可以在应用程序中使用新的 `settings` 对象: + +```Python hl_lines="18-20" +{!../../../docs_src/settings/tutorial001.py!} +``` + +### 运行服务器 + +接下来,您将运行服务器,并将配置作为环境变量传递。例如,您可以设置一个 `ADMIN_EMAIL` 和 `APP_NAME`,如下所示: + +
+ +```console +$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp"uvicorn main:app + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +!!! tip + 要为单个命令设置多个环境变量,只需用空格分隔它们,并将它们全部放在命令之前。 + +然后,`admin_email` 设置将为 `"deadpool@example.com"`。 + +`app_name` 将为 `"ChimichangApp"`。 + +而 `items_per_user` 将保持其默认值为 `50`。 + +## 在另一个模块中设置 + +您可以将这些设置放在另一个模块文件中,就像您在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中所见的那样。 + +例如,您可以创建一个名为 `config.py` 的文件,其中包含以下内容: + +```Python +{!../../../docs_src/settings/app01/config.py!} +``` + +然后在一个名为 `main.py` 的文件中使用它: + +```Python hl_lines="3 11-13" +{!../../../docs_src/settings/app01/main.py!} +``` +!!! tip + 您还需要一个名为 `__init__.py` 的文件,就像您在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中看到的那样。 + +## 在依赖项中使用设置 + +在某些情况下,从依赖项中提供设置可能比在所有地方都使用全局对象 `settings` 更有用。 + +这在测试期间尤其有用,因为很容易用自定义设置覆盖依赖项。 + +### 配置文件 + +根据前面的示例,您的 `config.py` 文件可能如下所示: + +```Python hl_lines="10" +{!../../../docs_src/settings/app02/config.py!} +``` + +请注意,现在我们不创建默认实例 `settings = Settings()`。 + +### 主应用程序文件 + +现在我们创建一个依赖项,返回一个新的 `config.Settings()`。 + +=== "Python 3.9+" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an_py39/main.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.8+ 非注解版本" + + !!! tip + 如果可能,请尽量使用 `Annotated` 版本。 + + ```Python hl_lines="5 11-12" + {!> ../../../docs_src/settings/app02/main.py!} + ``` + +!!! tip + 我们稍后会讨论 `@lru_cache`。 + + 目前,您可以将 `get_settings()` 视为普通函数。 + +然后,我们可以将其作为依赖项从“路径操作函数”中引入,并在需要时使用它。 + +=== "Python 3.9+" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an_py39/main.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.8+ 非注解版本" + + !!! tip + 如果可能,请尽量使用 `Annotated` 版本。 + + ```Python hl_lines="16 18-20" + {!> ../../../docs_src/settings/app02/main.py!} + ``` + +### 设置和测试 + +然后,在测试期间,通过创建 `get_settings` 的依赖项覆盖,很容易提供一个不同的设置对象: + +```Python hl_lines="9-10 13 21" +{!../../../docs_src/settings/app02/test_main.py!} +``` + +在依赖项覆盖中,我们在创建新的 `Settings` 对象时为 `admin_email` 设置了一个新值,然后返回该新对象。 + +然后,我们可以测试它是否被使用。 + +## 从 `.env` 文件中读取设置 + +如果您有许多可能经常更改的设置,可能在不同的环境中,将它们放在一个文件中,然后从该文件中读取它们,就像它们是环境变量一样,可能非常有用。 + +这种做法相当常见,有一个名称,这些环境变量通常放在一个名为 `.env` 的文件中,该文件被称为“dotenv”。 + +!!! tip + 以点 (`.`) 开头的文件是 Unix-like 系统(如 Linux 和 macOS)中的隐藏文件。 + + 但是,dotenv 文件实际上不一定要具有确切的文件名。 + +Pydantic 支持使用外部库从这些类型的文件中读取。您可以在Pydantic 设置: Dotenv (.env) 支持中阅读更多相关信息。 + +!!! tip + 要使其工作,您需要执行 `pip install python-dotenv`。 + +### `.env` 文件 + +您可以使用以下内容创建一个名为 `.env` 的文件: + +```bash +ADMIN_EMAIL="deadpool@example.com" +APP_NAME="ChimichangApp" +``` + +### 从 `.env` 文件中读取设置 + +然后,您可以使用以下方式更新您的 `config.py`: + +```Python hl_lines="9-10" +{!../../../docs_src/settings/app03/config.py!} +``` + +在这里,我们在 Pydantic 的 `Settings` 类中创建了一个名为 `Config` 的类,并将 `env_file` 设置为我们想要使用的 dotenv 文件的文件名。 + +!!! tip + `Config` 类仅用于 Pydantic 配置。您可以在Pydantic Model Config中阅读更多相关信息。 + +### 使用 `lru_cache` 仅创建一次 `Settings` + +从磁盘中读取文件通常是一项耗时的(慢)操作,因此您可能希望仅在首次读取后并重复使用相同的设置对象,而不是为每个请求都读取它。 + +但是,每次执行以下操作: + +```Python +Settings() +``` + +都会创建一个新的 `Settings` 对象,并且在创建时会再次读取 `.env` 文件。 + +如果依赖项函数只是这样的: + +```Python +def get_settings(): + return Settings() +``` + +我们将为每个请求创建该对象,并且将在每个请求中读取 `.env` 文件。 ⚠️ + +但是,由于我们在顶部使用了 `@lru_cache` 装饰器,因此只有在第一次调用它时,才会创建 `Settings` 对象一次。 ✔️ + +=== "Python 3.9+" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an_py39/main.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an/main.py!} + ``` + +=== "Python 3.8+ 非注解版本" + + !!! tip + 如果可能,请尽量使用 `Annotated` 版本。 + + ```Python hl_lines="1 10" + {!> ../../../docs_src/settings/app03/main.py!} + ``` + +然后,在下一次请求的依赖项中对 `get_settings()` 进行任何后续调用时,它不会执行 `get_settings()` 的内部代码并创建新的 `Settings` 对象,而是返回在第一次调用时返回的相同对象,一次又一次。 + +#### `lru_cache` 技术细节 + +`@lru_cache` 修改了它所装饰的函数,以返回第一次返回的相同值,而不是再次计算它,每次都执行函数的代码。 + +因此,下面的函数将对每个参数组合执行一次。然后,每个参数组合返回的值将在使用完全相同的参数组合调用函数时再次使用。 + +例如,如果您有一个函数: +```Python +@lru_cache +def say_hi(name: str, salutation: str = "Ms."): + return f"Hello {salutation} {name}" +``` + +您的程序可以像这样执行: + +```mermaid +sequenceDiagram + +participant code as Code +participant function as say_hi() +participant execute as Execute function + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Camila") + function ->> execute: 执行函数代码 + execute ->> code: 返回结果 + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Camila") + function ->> code: 返回存储的结果 + end + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Rick") + function ->> execute: 执行函数代码 + execute ->> code: 返回结果 + end + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Rick", salutation="Mr.") + function ->> execute: 执行函数代码 + execute ->> code: 返回结果 + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Rick") + function ->> code: 返回存储的结果 + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Camila") + function ->> code: 返回存储的结果 + end +``` + +对于我们的依赖项 `get_settings()`,该函数甚至不接受任何参数,因此它始终返回相同的值。 + +这样,它的行为几乎就像是一个全局变量。但是由于它使用了依赖项函数,因此我们可以轻松地进行测试时的覆盖。 + +`@lru_cache` 是 `functools` 的一部分,它是 Python 标准库的一部分,您可以在Python 文档中了解有关 `@lru_cache` 的更多信息。 + +## 小结 + +您可以使用 Pydantic 设置处理应用程序的设置或配置,利用 Pydantic 模型的所有功能。 + +* 通过使用依赖项,您可以简化测试。 +* 您可以使用 `.env` 文件。 +* 使用 `@lru_cache` 可以避免为每个请求重复读取 dotenv 文件,同时允许您在测试时进行覆盖。 diff --git a/docs/zh/docs/advanced/websockets.md b/docs/zh/docs/advanced/websockets.md new file mode 100644 index 0000000000..a5cbdd9651 --- /dev/null +++ b/docs/zh/docs/advanced/websockets.md @@ -0,0 +1,214 @@ +# WebSockets + +您可以在 **FastAPI** 中使用 [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)。 + +## 安装 `WebSockets` + +首先,您需要安装 `WebSockets`: + +```console +$ pip install websockets + +---> 100% +``` + +## WebSockets 客户端 + +### 在生产环境中 + +在您的生产系统中,您可能使用现代框架(如React、Vue.js或Angular)创建了一个前端。 + +要使用 WebSockets 与后端进行通信,您可能会使用前端的工具。 + +或者,您可能有一个原生移动应用程序,直接使用原生代码与 WebSocket 后端通信。 + +或者,您可能有其他与 WebSocket 终端通信的方式。 + +--- + +但是,在本示例中,我们将使用一个非常简单的HTML文档,其中包含一些JavaScript,全部放在一个长字符串中。 + +当然,这并不是最优的做法,您不应该在生产环境中使用它。 + +在生产环境中,您应该选择上述任一选项。 + +但这是一种专注于 WebSockets 的服务器端并提供一个工作示例的最简单方式: + +```Python hl_lines="2 6-38 41-43" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +## 创建 `websocket` + +在您的 **FastAPI** 应用程序中,创建一个 `websocket`: + +```Python hl_lines="1 46-47" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +!!! note "技术细节" + 您也可以使用 `from starlette.websockets import WebSocket`。 + + **FastAPI** 直接提供了相同的 `WebSocket`,只是为了方便开发人员。但它直接来自 Starlette。 + +## 等待消息并发送消息 + +在您的 WebSocket 路由中,您可以使用 `await` 等待消息并发送消息。 + +```Python hl_lines="48-52" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +您可以接收和发送二进制、文本和 JSON 数据。 + +## 尝试一下 + +如果您的文件名为 `main.py`,请使用以下命令运行应用程序: + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +在浏览器中打开 http://127.0.0.1:8000。 + +您将看到一个简单的页面,如下所示: + + + +您可以在输入框中输入消息并发送: + + + +您的 **FastAPI** 应用程序将回复: + + + +您可以发送(和接收)多条消息: + + + +所有这些消息都将使用同一个 WebSocket 连 + +接。 + +## 使用 `Depends` 和其他依赖项 + +在 WebSocket 端点中,您可以从 `fastapi` 导入并使用以下内容: + +* `Depends` +* `Security` +* `Cookie` +* `Header` +* `Path` +* `Query` + +它们的工作方式与其他 FastAPI 端点/ *路径操作* 相同: + +=== "Python 3.10+" + + ```Python hl_lines="68-69 82" + {!> ../../../docs_src/websockets/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="68-69 82" + {!> ../../../docs_src/websockets/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="69-70 83" + {!> ../../../docs_src/websockets/tutorial002_an.py!} + ``` + +=== "Python 3.10+ 非带注解版本" + + !!! tip + 如果可能,请尽量使用 `Annotated` 版本。 + + ```Python hl_lines="66-67 79" + {!> ../../../docs_src/websockets/tutorial002_py310.py!} + ``` + +=== "Python 3.8+ 非带注解版本" + + !!! tip + 如果可能,请尽量使用 `Annotated` 版本。 + + ```Python hl_lines="68-69 81" + {!> ../../../docs_src/websockets/tutorial002.py!} + ``` + +!!! info + 由于这是一个 WebSocket,抛出 `HTTPException` 并不是很合理,而是抛出 `WebSocketException`。 + + 您可以使用规范中定义的有效代码。 + +### 尝试带有依赖项的 WebSockets + +如果您的文件名为 `main.py`,请使用以下命令运行应用程序: + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +在浏览器中打开 http://127.0.0.1:8000。 + +在页面中,您可以设置: + +* "Item ID",用于路径。 +* "Token",作为查询参数。 + +!!! tip + 注意,查询参数 `token` 将由依赖项处理。 + +通过这样,您可以连接 WebSocket,然后发送和接收消息: + + + +## 处理断开连接和多个客户端 + +当 WebSocket 连接关闭时,`await websocket.receive_text()` 将引发 `WebSocketDisconnect` 异常,您可以捕获并处理该异常,就像本示例中的示例一样。 + +=== "Python 3.9+" + + ```Python hl_lines="79-81" + {!> ../../../docs_src/websockets/tutorial003_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="81-83" + {!> ../../../docs_src/websockets/tutorial003.py!} + ``` + +尝试以下操作: + +* 使用多个浏览器选项卡打开应用程序。 +* 从这些选项卡中发送消息。 +* 然后关闭其中一个选项卡。 + +这将引发 `WebSocketDisconnect` 异常,并且所有其他客户端都会收到类似以下的消息: + +``` +Client #1596980209979 left the chat +``` + +!!! tip + 上面的应用程序是一个最小和简单的示例,用于演示如何处理和向多个 WebSocket 连接广播消息。 + + 但请记住,由于所有内容都在内存中以单个列表的形式处理,因此它只能在进程运行时工作,并且只能使用单个进程。 + + 如果您需要与 FastAPI 集成更简单但更强大的功能,支持 Redis、PostgreSQL 或其他功能,请查看 [encode/broadcaster](https://github.com/encode/broadcaster)。 + +## 更多信息 + +要了解更多选项,请查看 Starlette 的文档: + +* [WebSocket 类](https://www.starlette.io/websockets/) +* [基于类的 WebSocket 处理](https://www.starlette.io/endpoints/#websocketendpoint)。 diff --git a/docs/zh/docs/async.md b/docs/zh/docs/async.md new file mode 100644 index 0000000000..59eebd0491 --- /dev/null +++ b/docs/zh/docs/async.md @@ -0,0 +1,430 @@ +# 并发 async / await + +有关路径操作函数的 `async def` 语法以及异步代码、并发和并行的一些背景知识。 + +## 赶时间吗? + +TL;DR: + +如果你正在使用第三方库,它们会告诉你使用 `await` 关键字来调用它们,就像这样: + +```Python +results = await some_library() +``` + +然后,通过 `async def` 声明你的 *路径操作函数*: + +```Python hl_lines="2" +@app.get('/') +async def read_results(): + results = await some_library() + return results +``` + +!!! note + 你只能在被 `async def` 创建的函数内使用 `await` + +--- + +如果你正在使用一个第三方库和某些组件(比如:数据库、API、文件系统...)进行通信,第三方库又不支持使用 `await` (目前大多数数据库三方库都是这样),这种情况你可以像平常那样使用 `def` 声明一个路径操作函数,就像这样: + +```Python hl_lines="2" +@app.get('/') +def results(): + results = some_library() + return results +``` + +--- + +如果你的应用程序不需要与其他任何东西通信而等待其响应,请使用 `async def`。 + +--- + +如果你不清楚,使用 `def` 就好. + +--- + +**注意**:你可以根据需要在路径操作函数中混合使用 `def` 和 `async def`,并使用最适合你的方式去定义每个函数。FastAPI 将为他们做正确的事情。 + +无论如何,在上述任何情况下,FastAPI 仍将异步工作,速度也非常快。 + +但是,通过遵循上述步骤,它将能够进行一些性能优化。 + +## 技术细节 + +Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 `await` 语法的东西来写**”异步代码“**。 + +让我们在下面的部分中逐一介绍: + +* **异步代码** +* **`async` 和 `await`** +* **协程** + +## 异步代码 + +异步代码仅仅意味着编程语言 💬 有办法告诉计算机/程序 🤖 在代码中的某个点,它 🤖 将不得不等待在某些地方完成一些事情。让我们假设一些事情被称为 "慢文件"📝. + +所以,在等待"慢文件"📝完成的这段时间,计算机可以做一些其他工作。 + +然后计算机/程序 🤖 每次有机会都会回来,因为它又在等待,或者它 🤖 完成了当前所有的工作。而且它 🤖 将查看它等待的所有任务中是否有已经完成的,做它必须做的任何事情。 + +接下来,它 🤖 完成第一个任务(比如是我们的"慢文件"📝) 并继续与之相关的一切。 + +这个"等待其他事情"通常指的是一些相对较慢(与处理器和 RAM 存储器的速度相比)的 I/O 操作,比如说: + +* 通过网络发送来自客户端的数据 +* 客户端接收来自网络中的数据 +* 磁盘中要由系统读取并提供给程序的文件的内容 +* 程序提供给系统的要写入磁盘的内容 +* 一个 API 的远程调用 +* 一个数据库操作,直到完成 +* 一个数据库查询,直到返回结果 +* 等等. + +这个执行的时间大多是在等待 I/O 操作,因此它们被叫做 "I/O 密集型" 操作。 + +它被称为"异步"的原因是因为计算机/程序不必与慢任务"同步",去等待任务完成的确切时刻,而在此期间不做任何事情直到能够获取任务结果才继续工作。 + +相反,作为一个"异步"系统,一旦完成,任务就可以排队等待一段时间(几微秒),等待计算机程序完成它要做的任何事情,然后回来获取结果并继续处理它们。 + +对于"同步"(与"异步"相反),他们通常也使用"顺序"一词,因为计算机程序在切换到另一个任务之前是按顺序执行所有步骤,即使这些步骤涉及到等待。 + +### 并发与汉堡 + +上述异步代码的思想有时也被称为“并发”,它不同于“并行”。 + +并发和并行都与“不同的事情或多或少同时发生”有关。 + +但是并发和并行之间的细节是完全不同的。 + +要了解差异,请想象以下关于汉堡的故事: + +### 并发汉堡 + +你和你的恋人一起去快餐店,你排队在后面,收银员从你前面的人接单。😍 + + + +然后轮到你了,你为你的恋人和你选了两个非常豪华的汉堡。🍔🍔 + + + +收银员对厨房里的厨师说了一些话,让他们知道他们必须为你准备汉堡(尽管他们目前正在为之前的顾客准备汉堡)。 + + + +你付钱了。 💸 + +收银员给你轮到的号码。 + + + +当你在等待的时候,你和你的恋人一起去挑选一张桌子,然后你们坐下来聊了很长时间(因为汉堡很豪华,需要一些时间来准备)。 + +当你和你的恋人坐在桌子旁,等待汉堡的时候,你可以用这段时间来欣赏你的恋人是多么的棒、可爱和聪明✨😍✨。 + + + +在等待中和你的恋人交谈时,你会不时地查看柜台上显示的号码,看看是否已经轮到你了。 + +然后在某个时刻,终于轮到你了。你去柜台拿汉堡然后回到桌子上。 + + + +你们享用了汉堡,整个过程都很开心。✨ + + + +!!! info + 漂亮的插画来自 Ketrina Thompson. 🎨 + +--- + +在那个故事里,假设你是计算机程序 🤖 。 + +当你在排队时,你只是闲着😴, 轮到你前不做任何事情(仅排队)。但排队很快,因为收银员只接订单(不准备订单),所以这一切都还好。 + +然后,当轮到你时,需要你做一些实际性的工作,比如查看菜单,决定你想要什么,让你的恋人选择,支付,检查你是否提供了正确的账单或卡,检查你的收费是否正确,检查订单是否有正确的项目,等等。 + +此时,即使你仍然没有汉堡,你和收银员的工作也"暂停"了⏸, 因为你必须等待一段时间 🕙 让你的汉堡做好。 + +但是,当你离开柜台并坐在桌子旁,在轮到你的号码前的这段时间,你可以将焦点切换到 🔀 你的恋人上,并做一些"工作"⏯ 🤓。你可以做一些非常"有成效"的事情,比如和你的恋人调情😍. + +之后,收银员 💁 把号码显示在显示屏上,并说到 "汉堡做好了",而当显示的号码是你的号码时,你不会立刻疯狂地跳起来。因为你知道没有人会偷你的汉堡,因为你有你的号码,而其他人又有他们自己的号码。 + +所以你要等待你的恋人完成故事(完成当前的工作⏯ /正在做的事🤓), 轻轻微笑,说你要吃汉堡⏸. + +然后你去柜台🔀, 到现在初始任务已经完成⏯, 拿起汉堡,说声谢谢,然后把它们送到桌上。这就完成了与计数器交互的步骤/任务⏹. 这反过来又产生了一项新任务,即"吃汉堡"🔀 ⏯, 上一个"拿汉堡"的任务已经结束了⏹. + +### 并行汉堡 + +现在让我们假设不是"并发汉堡",而是"并行汉堡"。 + +你和你的恋人一起去吃并行快餐。 + +你站在队伍中,同时是厨师的几个收银员(比方说8个)从前面的人那里接单。 + +你之前的每个人都在等待他们的汉堡准备好后才离开柜台,因为8名收银员都会在下一份订单前马上准备好汉堡。 + + + +然后,终于轮到你了,你为你的恋人和你订购了两个非常精美的汉堡。 + +你付钱了 💸。 + + + +收银员去厨房。 + +你站在柜台前 🕙等待着,这样就不会有人在你之前抢走你的汉堡,因为没有轮流的号码。 + + + +当你和你的恋人忙于不让任何人出现在你面前,并且在他们到来的时候拿走你的汉堡时,你无法关注到你的恋人。😞 + +这是"同步"的工作,你被迫与服务员/厨师 👨‍🍳"同步"。你在此必须等待 🕙 ,在收银员/厨师 👨‍🍳 完成汉堡并将它们交给你的确切时间到达之前一直等待,否则其他人可能会拿走它们。 + + + +你经过长时间的等待 🕙 ,收银员/厨师 👨‍🍳终于带着汉堡回到了柜台。 + + + +你拿着汉堡,和你的情人一起上桌。 + +你们仅仅是吃了它们,就结束了。⏹ + + + +没有太多的交谈或调情,因为大部分时间 🕙 都在柜台前等待😞。 + +!!! info + 漂亮的插画来自 Ketrina Thompson. 🎨 + +--- + +在这个并行汉堡的场景中,你是一个计算机程序 🤖 且有两个处理器(你和你的恋人),都在等待 🕙 ,并投入他们的注意力 ⏯ 在柜台上等待了很长一段时间。 + +这家快餐店有 8 个处理器(收银员/厨师)。而并发汉堡店可能只有 2 个(一个收银员和一个厨师)。 + +但最终的体验仍然不是最好的。😞 + +--- + +这将是与汉堡的类似故事。🍔 + +一种更"贴近生活"的例子,想象一家银行。 + +直到最近,大多数银行都有多个出纳员 👨‍💼👨‍💼👨‍💼👨‍💼 还有一条长长排队队伍🕙🕙🕙🕙🕙🕙🕙🕙。 + +所有收银员都是一个接一个的在客户面前做完所有的工作👨‍💼⏯. + +你必须经过 🕙 较长时间排队,否则你就没机会了。 + +你可不会想带你的恋人 😍 和你一起去银行办事🏦. + +### 汉堡结论 + +在"你与恋人一起吃汉堡"的这个场景中,因为有很多人在等待🕙, 使用并发系统更有意义⏸🔀⏯. + +大多数 Web 应用都是这样的。 + +你的服务器正在等待很多很多用户通过他们不太好的网络发送来的请求。 + +然后再次等待 🕙 响应回来。 + +这个"等待" 🕙 是以微秒为单位测量的,但总的来说,最后还是等待很久。 + +这就是为什么使用异步对于 Web API 很有意义的原因 ⏸🔀⏯。 + +这种异步机制正是 NodeJS 受到欢迎的原因(尽管 NodeJS 不是并行的),以及 Go 作为编程语言的优势所在。 + +这与 **FastAPI** 的性能水平相同。 + +您可以同时拥有并行性和异步性,您可以获得比大多数经过测试的 NodeJS 框架更高的性能,并且与 Go 不相上下, Go 是一种更接近于 C 的编译语言(全部归功于 Starlette)。 + +### 并发比并行好吗? + +不!这不是故事的本意。 + +并发不同于并行。而是在需要大量等待的特定场景下效果更好。因此,在 Web 应用程序开发中,它通常比并行要好得多,但这并不意味着全部。 + +因此,为了平衡这一点,想象一下下面的短篇故事: + +> 你必须打扫一个又大又脏的房子。 + +*是的,这就是完整的故事。* + +--- + +在任何地方, 都不需要等待 🕙 ,只需要在房子的多个地方做着很多工作。 + +你可以像汉堡的例子那样轮流执行,先是客厅,然后是厨房,但因为你不需要等待 🕙 ,对于任何事情都是清洁,清洁,还是清洁,轮流不会影响任何事情。 + +无论是否轮流执行(并发),都需要相同的时间来完成,而你也会完成相同的工作量。 + +但在这种情况下,如果你能带上 8 名前收银员/厨师,现在是清洁工一起清扫,他们中的每一个人(加上你)都能占据房子的一个区域来清扫,你就可以在额外的帮助下并行的更快地完成所有工作。 + +在这个场景中,每个清洁工(包括您)都将是一个处理器,完成这个工作的一部分。 + +由于大多数执行时间是由实际工作(而不是等待)占用的,并且计算机中的工作是由 CPU 完成的,所以他们称这些问题为"CPU 密集型"。 + +--- + +CPU 密集型操作的常见示例是需要复杂的数学处理。 + +例如: + +* **音频**或**图像**处理; +* **计算机视觉**: 一幅图像由数百万像素组成,每个像素有3种颜色值,处理通常需要同时对这些像素进行计算; +* **机器学习**: 它通常需要大量的"矩阵"和"向量"乘法。想象一个包含数字的巨大电子表格,并同时将所有数字相乘; +* **深度学习**: 这是机器学习的一个子领域,同样适用。只是没有一个数字的电子表格可以相乘,而是一个庞大的数字集合,在很多情况下,你需要使用一个特殊的处理器来构建和使用这些模型。 + +### 并发 + 并行: Web + 机器学习 + +使用 **FastAPI**,您可以利用 Web 开发中常见的并发机制的优势(NodeJS 的主要吸引力)。 + +并且,您也可以利用并行和多进程(让多个进程并行运行)的优点来处理与机器学习系统中类似的 **CPU 密集型** 工作。 + +这一点,再加上 Python 是**数据科学**、机器学习(尤其是深度学习)的主要语言这一简单事实,使得 **FastAPI** 与数据科学/机器学习 Web API 和应用程序(以及其他许多应用程序)非常匹配。 + +了解如何在生产环境中实现这种并行性,可查看此文 [Deployment](deployment/index.md){.internal-link target=_blank}。 + +## `async` 和 `await` + +现代版本的 Python 有一种非常直观的方式来定义异步代码。这使它看起来就像正常的"顺序"代码,并在适当的时候"等待"。 + +当有一个操作需要等待才能给出结果,且支持这个新的 Python 特性时,您可以编写如下代码: + +```Python +burgers = await get_burgers(2) +``` + +这里的关键是 `await`。它告诉 Python 它必须等待 ⏸ `get_burgers(2)` 完成它的工作 🕙 ,然后将结果存储在 `burgers` 中。这样,Python 就会知道此时它可以去做其他事情 🔀 ⏯ (比如接收另一个请求)。 + +要使 `await` 工作,它必须位于支持这种异步机制的函数内。因此,只需使用 `async def` 声明它: + +```Python hl_lines="1" +async def get_burgers(number: int): + # Do some asynchronous stuff to create the burgers + return burgers +``` + +...而不是 `def`: + +```Python hl_lines="2" +# This is not asynchronous +def get_sequential_burgers(number: int): + # Do some sequential stuff to create the burgers + return burgers +``` + +使用 `async def`,Python 就知道在该函数中,它将遇上 `await`,并且它可以"暂停" ⏸ 执行该函数,直至执行其他操作 🔀 后回来。 + +当你想调用一个 `async def` 函数时,你必须"等待"它。因此,这不会起作用: + +```Python +# This won't work, because get_burgers was defined with: async def +burgers = get_burgers(2) +``` + +--- + +因此,如果您使用的库告诉您可以使用 `await` 调用它,则需要使用 `async def` 创建路径操作函数 ,如: + +```Python hl_lines="2-3" +@app.get('/burgers') +async def read_burgers(): + burgers = await get_burgers(2) + return burgers +``` + +### 更多技术细节 + +您可能已经注意到,`await` 只能在 `async def` 定义的函数内部使用。 + +但与此同时,必须"等待"通过 `async def` 定义的函数。因此,带 `async def` 的函数也只能在 `async def` 定义的函数内部调用。 + +那么,这关于先有鸡还是先有蛋的问题,如何调用第一个 `async` 函数? + +如果您使用 **FastAPI**,你不必担心这一点,因为"第一个"函数将是你的路径操作函数,FastAPI 将知道如何做正确的事情。 + +但如果您想在没有 FastAPI 的情况下使用 `async` / `await`,则可以这样做。 + +### 编写自己的异步代码 + +Starlette (和 **FastAPI**) 是基于 AnyIO 实现的,这使得它们可以兼容 Python 的标准库 asyncioTrio。 + +特别是,你可以直接使用 AnyIO 来处理高级的并发用例,这些用例需要在自己的代码中使用更高级的模式。 + +即使您没有使用 **FastAPI**,您也可以使用 AnyIO 编写自己的异步程序,使其拥有较高的兼容性并获得一些好处(例如, 结构化并发)。 + +### 其他形式的异步代码 + +这种使用 `async` 和 `await` 的风格在语言中相对较新。 + +但它使处理异步代码变得容易很多。 + +这种相同的语法(或几乎相同)最近也包含在现代版本的 JavaScript 中(在浏览器和 NodeJS 中)。 + +但在此之前,处理异步代码非常复杂和困难。 + +在以前版本的 Python,你可以使用多线程或者 Gevent。但代码的理解、调试和思考都要复杂许多。 + +在以前版本的 NodeJS / 浏览器 JavaScript 中,你会使用"回调",因此也可能导致回调地狱。 + +## 协程 + +**协程**只是 `async def` 函数返回的一个非常奇特的东西的称呼。Python 知道它有点像一个函数,它可以启动,也会在某个时刻结束,而且它可能会在内部暂停 ⏸ ,只要内部有一个 `await`。 + +通过使用 `async` 和 `await` 的异步代码的所有功能大多数被概括为"协程"。它可以与 Go 的主要关键特性 "Goroutines" 相媲美。 + +## 结论 + +让我们再来回顾下上文所说的: + +> Python 的现代版本可以通过使用 `async` 和 `await` 语法创建**协程**,并用于支持**异步代码**。 + +现在应该能明白其含义了。✨ + +所有这些使得 FastAPI(通过 Starlette)如此强大,也是它拥有如此令人印象深刻的性能的原因。 + +## 非常技术性的细节 + +!!! warning + 你可以跳过这里。 + + 这些都是 FastAPI 如何在内部工作的技术细节。 + + 如果您有相当多的技术知识(协程、线程、阻塞等),并且对 FastAPI 如何处理 `async def` 与常规 `def` 感到好奇,请继续。 + +### 路径操作函数 + +当你使用 `def` 而不是 `async def` 来声明一个*路径操作函数*时,它运行在外部的线程池中并等待其结果,而不是直接调用(因为它会阻塞服务器)。 + +如果您使用过另一个不以上述方式工作的异步框架,并且您习惯于用普通的 `def` 定义普通的仅计算路径操作函数,以获得微小的性能增益(大约100纳秒),请注意,在 FastAPI 中,效果将完全相反。在这些情况下,最好使用 `async def`,除非路径操作函数内使用执行阻塞 I/O 的代码。 + +在这两种情况下,与您之前的框架相比,**FastAPI** 可能[仍然很快](/#performance){.internal-link target=_blank}。 + +### 依赖 + +这同样适用于[依赖](./tutorial/dependencies/index.md){.internal-link target=_blank}。如果一个依赖是标准的 `def` 函数而不是 `async def`,它将被运行在外部线程池中。 + +### 子依赖 + +你可以拥有多个相互依赖的依赖以及[子依赖](./tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} (作为函数的参数),它们中的一些可能是通过 `async def` 声明,也可能是通过 `def` 声明。它们仍然可以正常工作,这些通过 `def` 声明的函数将会在外部线程中调用(来自线程池),而不是"被等待"。 + +### 其他函数 + +您可直接调用通过 `def` 或 `async def` 创建的任何其他函数,FastAPI 不会影响您调用它们的方式。 + +这与 FastAPI 为您调用*路径操作函数*和依赖项的逻辑相反。 + +如果你的函数是通过 `def` 声明的,它将被直接调用(在代码中编写的地方),而不会在线程池中,如果这个函数通过 `async def` 声明,当在代码中调用时,你就应该使用 `await` 等待函数的结果。 + +--- + +再次提醒,这些是非常技术性的细节,如果你来搜索它可能对你有用。 + +否则,您最好应该遵守的指导原则赶时间吗?. diff --git a/docs/zh/docs/contributing.md b/docs/zh/docs/contributing.md index 36c3631c44..4ebd673150 100644 --- a/docs/zh/docs/contributing.md +++ b/docs/zh/docs/contributing.md @@ -97,7 +97,7 @@ $ python -m venv env
```console -$ pip install -e ."[dev,doc,test]" +$ pip install -r requirements.txt ---> 100% ``` diff --git a/docs/zh/docs/deployment/cloud.md b/docs/zh/docs/deployment/cloud.md new file mode 100644 index 0000000000..398f613728 --- /dev/null +++ b/docs/zh/docs/deployment/cloud.md @@ -0,0 +1,17 @@ +# 在云上部署 FastAPI + +您几乎可以使用**任何云服务商**来部署 FastAPI 应用程序。 + +在大多数情况下,主要的云服务商都有部署 FastAPI 的指南。 + +## 云服务商 - 赞助商 + +一些云服务商 ✨ [**赞助 FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨,这确保了FastAPI 及其**生态系统**持续健康地**发展**。 + +这表明了他们对 FastAPI 及其**社区**(您)的真正承诺,因为他们不仅想为您提供**良好的服务**,而且还想确保您拥有一个**良好且健康的框架**:FastAPI。 🙇 + +您可能想尝试他们的服务并阅读他们的指南: + +* Platform.sh +* Porter +* Deta diff --git a/docs/zh/docs/deployment/https.md b/docs/zh/docs/deployment/https.md new file mode 100644 index 0000000000..cf01a4585b --- /dev/null +++ b/docs/zh/docs/deployment/https.md @@ -0,0 +1,192 @@ +# 关于 HTTPS + +人们很容易认为 HTTPS 仅仅是“启用”或“未启用”的东西。 + +但实际情况比这复杂得多。 + +!!!提示 + 如果你很赶时间或不在乎,请继续阅读下一部分,下一部分会提供一个step-by-step的教程,告诉你怎么使用不同技术来把一切都配置好。 + +要从用户的视角**了解 HTTPS 的基础知识**,请查看 https://howhttps.works/。 + +现在,从**开发人员的视角**,在了解 HTTPS 时需要记住以下几点: + +* 要使用 HTTPS,**服务器**需要拥有由**第三方**生成的**"证书(certificate)"**。 + * 这些证书实际上是从第三方**获取**的,而不是“生成”的。 +* 证书有**生命周期**。 + * 它们会**过期**。 + * 然后它们需要**更新**,**再次从第三方获取**。 +* 连接的加密发生在 **TCP 层**。 + * 这是 HTTP 协议**下面的一层**。 + * 因此,**证书和加密**处理是在 **HTTP之前**完成的。 +* **TCP 不知道域名**。 仅仅知道 IP 地址。 + * 有关所请求的 **特定域名** 的信息位于 **HTTP 数据**中。 +* **HTTPS 证书**“证明”**某个域名**,但协议和加密发生在 TCP 层,在知道正在处理哪个域名**之前**。 +* **默认情况下**,这意味着你**每个 IP 地址只能拥有一个 HTTPS 证书**。 + * 无论你的服务器有多大,或者服务器上的每个应用程序有多小。 + * 不过,对此有一个**解决方案**。 +* **TLS** 协议(在 HTTP 之下的TCP 层处理加密的协议)有一个**扩展**,称为 **SNI**。 + * SNI 扩展允许一台服务器(具有 **单个 IP 地址**)拥有 **多个 HTTPS 证书** 并提供 **多个 HTTPS 域名/应用程序**。 + * 为此,服务器上会有**单独**的一个组件(程序)侦听**公共 IP 地址**,这个组件必须拥有服务器中的**所有 HTTPS 证书**。 +* **获得安全连接后**,通信协议**仍然是HTTP**。 + * 内容是 **加密过的**,即使它们是通过 **HTTP 协议** 发送的。 + +通常的做法是在服务器上运行**一个程序/HTTP 服务器**并**管理所有 HTTPS 部分**:接收**加密的 HTTPS 请求**, 将 **解密的 HTTP 请求** 发送到在同一服务器中运行的实际 HTTP 应用程序(在本例中为 **FastAPI** 应用程序),从应用程序中获取 **HTTP 响应**, 使用适当的 **HTTPS 证书**对其进行加密并使用 **HTTPS** 将其发送回客户端。 此服务器通常被称为 **TLS 终止代理(TLS Termination Proxy)**。 + +你可以用作 TLS 终止代理的一些选项包括: + +* Traefik(也可以处理证书更新) +* Caddy(也可以处理证书更新) +* Nginx +* HAProxy + +## Let's Encrypt + +在 Let's Encrypt 之前,这些 **HTTPS 证书** 由受信任的第三方出售。 + +过去,获得这些证书的过程非常繁琐,需要大量的文书工作,而且证书非常昂贵。 + +但随后 **Let's Encrypt** 创建了。 + +它是 Linux 基金会的一个项目。 它以自动方式免费提供 **HTTPS 证书**。 这些证书可以使用所有符合标准的安全加密,并且有效期很短(大约 3 个月),因此**安全性实际上更好**,因为它们的生命周期缩短了。 + +域可以被安全地验证并自动生成证书。 这还允许自动更新这些证书。 + +我们的想法是自动获取和更新这些证书,以便你可以永远免费拥有**安全的 HTTPS**。 + +## 面向开发人员的 HTTPS + +这里有一个 HTTPS API 看起来是什么样的示例,我们会分步说明,并且主要关注对开发人员重要的部分。 + + +### 域名 + +第一步我们要先**获取**一些**域名(Domain Name)**。 然后可以在 DNS 服务器(可能是你的同一家云服务商提供的)中配置它。 + +你可能拥有一个云服务器(虚拟机)或类似的东西,并且它会有一个固定 **公共IP地址**。 + +在 DNS 服务器中,你可以配置一条记录(“A 记录”)以将 **你的域名** 指向你服务器的公共 **IP 地址**。 + +这个操作一般只需要在最开始执行一次。 + +!!! tip + 域名这部分发生在 HTTPS 之前,由于这一切都依赖于域名和 IP 地址,所以先在这里提一下。 + +### DNS + +现在让我们关注真正的 HTTPS 部分。 + +首先,浏览器将通过 **DNS 服务器** 查询**域名的IP** 是什么,在本例中为 `someapp.example.com`。 + +DNS 服务器会告诉浏览器使用某个特定的 **IP 地址**。 这将是你在 DNS 服务器中为你的服务器配置的公共 IP 地址。 + + + +### TLS 握手开始 + +然后,浏览器将在**端口 443**(HTTPS 端口)上与该 IP 地址进行通信。 + +通信的第一部分只是建立客户端和服务器之间的连接并决定它们将使用的加密密钥等。 + + + +客户端和服务器之间建立 TLS 连接的过程称为 **TLS 握手**。 + +### 带有 SNI 扩展的 TLS + +**服务器中只有一个进程**可以侦听特定 **IP 地址**的特定 **端口**。 可能有其他进程在同一 IP 地址的其他端口上侦听,但每个 IP 地址和端口组合只有一个进程。 + +TLS (HTTPS) 默认使用端口`443`。 这就是我们需要的端口。 + +由于只有一个进程可以监听此端口,因此监听端口的进程将是 **TLS 终止代理**。 + +TLS 终止代理可以访问一个或多个 **TLS 证书**(HTTPS 证书)。 + +使用上面讨论的 **SNI 扩展**,TLS 终止代理将检查应该用于此连接的可用 TLS (HTTPS) 证书,并使用与客户端期望的域名相匹配的证书。 + +在这种情况下,它将使用`someapp.example.com`的证书。 + + + +客户端已经**信任**生成该 TLS 证书的实体(在本例中为 Let's Encrypt,但我们稍后会看到),因此它可以**验证**该证书是否有效。 + +然后,通过使用证书,客户端和 TLS 终止代理 **决定如何加密** **TCP 通信** 的其余部分。 这就完成了 **TLS 握手** 部分。 + +此后,客户端和服务器就拥有了**加密的 TCP 连接**,这就是 TLS 提供的功能。 然后他们可以使用该连接来启动实际的 **HTTP 通信**。 + +这就是 **HTTPS**,它只是 **安全 TLS 连接** 内的普通 **HTTP**,而不是纯粹的(未加密的)TCP 连接。 + +!!! tip + 请注意,通信加密发生在 **TCP 层**,而不是 HTTP 层。 + +### HTTPS 请求 + +现在客户端和服务器(特别是浏览器和 TLS 终止代理)具有 **加密的 TCP 连接**,它们可以开始 **HTTP 通信**。 + +接下来,客户端发送一个 **HTTPS 请求**。 这其实只是一个通过 TLS 加密连接的 HTTP 请求。 + + + +### 解密请求 + +TLS 终止代理将使用协商好的加密算法**解密请求**,并将**(解密的)HTTP 请求**传输到运行应用程序的进程(例如运行 FastAPI 应用的 Uvicorn 进程)。 + + + +### HTTP 响应 + +应用程序将处理请求并向 TLS 终止代理发送**(未加密)HTTP 响应**。 + + + +### HTTPS 响应 + +然后,TLS 终止代理将使用之前协商的加密算法(以`someapp.example.com`的证书开头)对响应进行加密,并将其发送回浏览器。 + +接下来,浏览器将验证响应是否有效和是否使用了正确的加密密钥等。然后它会**解密响应**并处理它。 + + + +客户端(浏览器)将知道响应来自正确的服务器,因为它使用了他们之前使用 **HTTPS 证书** 协商出的加密算法。 + +### 多个应用程序 + +在同一台(或多台)服务器中,可能存在**多个应用程序**,例如其他 API 程序或数据库。 + +只有一个进程可以处理特定的 IP 和端口(在我们的示例中为 TLS 终止代理),但其他应用程序/进程也可以在服务器上运行,只要它们不尝试使用相同的 **公共 IP 和端口的组合**。 + + + +这样,TLS 终止代理就可以为多个应用程序处理**多个域名**的 HTTPS 和证书,然后在每种情况下将请求传输到正确的应用程序。 + +### 证书更新 + +在未来的某个时候,每个证书都会**过期**(大约在获得证书后 3 个月)。 + +然后,会有另一个程序(在某些情况下是另一个程序,在某些情况下可能是同一个 TLS 终止代理)与 Let's Encrypt 通信并更新证书。 + + + +**TLS 证书** **与域名相关联**,而不是与 IP 地址相关联。 + +因此,要更新证书,更新程序需要向权威机构(Let's Encrypt)**证明**它确实**“拥有”并控制该域名**。 + +有多种方法可以做到这一点。 一些流行的方式是: + +* **修改一些DNS记录**。 + * 为此,续订程序需要支持 DNS 提供商的 API,因此,要看你使用的 DNS 提供商是否提供这一功能。 +* **在与域名关联的公共 IP 地址上作为服务器运行**(至少在证书获取过程中)。 + * 正如我们上面所说,只有一个进程可以监听特定的 IP 和端口。 + * 这就是当同一个 TLS 终止代理还负责证书续订过程时它非常有用的原因之一。 + * 否则,你可能需要暂时停止 TLS 终止代理,启动续订程序以获取证书,然后使用 TLS 终止代理配置它们,然后重新启动 TLS 终止代理。 这并不理想,因为你的应用程序在 TLS 终止代理关闭期间将不可用。 + +通过拥有一个**单独的系统来使用 TLS 终止代理来处理 HTTPS**, 而不是直接将 TLS 证书与应用程序服务器一起使用 (例如 Uvicorn),你可以在 +更新证书的过程中同时保持提供服务。 + +## 回顾 + +拥有**HTTPS** 非常重要,并且在大多数情况下相当**关键**。 作为开发人员,你围绕 HTTPS 所做的大部分努力就是**理解这些概念**以及它们的工作原理。 + +一旦你了解了**面向开发人员的 HTTPS** 的基础知识,你就可以轻松组合和配置不同的工具,以帮助你以简单的方式管理一切。 + +在接下来的一些章节中,我将向你展示几个为 **FastAPI** 应用程序设置 **HTTPS** 的具体示例。 🔒 diff --git a/docs/zh/docs/deployment/index.md b/docs/zh/docs/deployment/index.md new file mode 100644 index 0000000000..1ec0c5c5b5 --- /dev/null +++ b/docs/zh/docs/deployment/index.md @@ -0,0 +1,21 @@ +# 部署 + +部署 **FastAPI** 应用程序相对容易。 + +## 部署是什么意思 + +**部署**应用程序意味着执行必要的步骤以使其**可供用户使用**。 + +对于**Web API**来说,通常涉及将上传到**云服务器**中,搭配一个性能和稳定性都不错的**服务器程序**,以便你的**用户**可以高效地**访问**你的应用程序,而不会出现中断或其他问题。 + +这与**开发**阶段形成鲜明对比,在**开发**阶段,你不断更改代码、破坏代码、修复代码, 来回停止和重启服务器等。 + +## 部署策略 + +根据你的使用场景和使用的工具,有多种方法可以实现此目的。 + +你可以使用一些工具自行**部署服务器**,你也可以使用能为你完成部分工作的**云服务**,或其他可能的选项。 + +我将向你展示在部署 **FastAPI** 应用程序时你可能应该记住的一些主要概念(尽管其中大部分适用于任何其他类型的 Web 应用程序)。 + +在接下来的部分中,你将看到更多需要记住的细节以及一些技巧。 ✨ diff --git a/docs/zh/docs/deployment/manually.md b/docs/zh/docs/deployment/manually.md new file mode 100644 index 0000000000..15588043fe --- /dev/null +++ b/docs/zh/docs/deployment/manually.md @@ -0,0 +1,148 @@ +# 手动运行服务器 - Uvicorn + +在远程服务器计算机上运行 **FastAPI** 应用程序所需的主要东西是 ASGI 服务器程序,例如 **Uvicorn**。 + +有 3 个主要可选方案: + +* Uvicorn:高性能 ASGI 服务器。 +* Hypercorn:与 HTTP/2 和 Trio 等兼容的 ASGI 服务器。 +* Daphne:为 Django Channels 构建的 ASGI 服务器。 + +## 服务器主机和服务器程序 + +关于名称,有一个小细节需要记住。 💡 + +“**服务器**”一词通常用于指远程/云计算机(物理机或虚拟机)以及在该计算机上运行的程序(例如 Uvicorn)。 + +请记住,当您一般读到“服务器”这个名词时,它可能指的是这两者之一。 + +当提到远程主机时,通常将其称为**服务器**,但也称为**机器**(machine)、**VM**(虚拟机)、**节点**。 这些都是指某种类型的远程计算机,通常运行 Linux,您可以在其中运行程序。 + + +## 安装服务器程序 + +您可以使用以下命令安装 ASGI 兼容服务器: + +=== "Uvicorn" + + * Uvicorn,一个快如闪电 ASGI 服务器,基于 uvloop 和 httptools 构建。 + +
+ + ```console + $ pip install "uvicorn[standard]" + + ---> 100% + ``` + +
+ + !!! tip + 通过添加`standard`,Uvicorn 将安装并使用一些推荐的额外依赖项。 + + 其中包括`uvloop`,它是`asyncio`的高性能替代品,它提供了巨大的并发性能提升。 + +=== "Hypercorn" + + * Hypercorn,一个也与 HTTP/2 兼容的 ASGI 服务器。 + +
+ + ```console + $ pip install hypercorn + + ---> 100% + ``` + +
+ + ...或任何其他 ASGI 服务器。 + + +## 运行服务器程序 + +您可以按照之前教程中的相同方式运行应用程序,但不使用`--reload`选项,例如: + +=== "Uvicorn" + +
+ + ```console + $ uvicorn main:app --host 0.0.0.0 --port 80 + + INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) + ``` + +
+ + +=== "Hypercorn" + +
+ + ```console + $ hypercorn main:app --bind 0.0.0.0:80 + + Running on 0.0.0.0:8080 over http (CTRL + C to quit) + ``` + +
+ +!!! warning + 如果您正在使用`--reload`选项,请记住删除它。 + + `--reload` 选项消耗更多资源,并且更不稳定。 + + 它在**开发**期间有很大帮助,但您**不应该**在**生产环境**中使用它。 + +## Hypercorn with Trio + +Starlette 和 **FastAPI** 基于 AnyIO, 所以它们才能同时与 Python 的标准库 asyncioTrio 兼容。 + +尽管如此,Uvicorn 目前仅与 asyncio 兼容,并且通常使用 `uvloop`, 它是`asyncio`的高性能替代品。 + +但如果你想直接使用**Trio**,那么你可以使用**Hypercorn**,因为它支持它。 ✨ + +### 安装具有 Trio 的 Hypercorn + +首先,您需要安装具有 Trio 支持的 Hypercorn: + +
+ +```console +$ pip install "hypercorn[trio]" +---> 100% +``` + +
+ +### Run with Trio + +然后你可以传递值`trio`给命令行选项`--worker-class`: + +
+ +```console +$ hypercorn main:app --worker-class trio +``` + +
+ +这将通过您的应用程序启动 Hypercorn,并使用 Trio 作为后端。 + +现在您可以在应用程序内部使用 Trio。 或者更好的是,您可以使用 AnyIO,使您的代码与 Trio 和 asyncio 兼容。 🎉 + +## 部署概念 + +这些示例运行服务器程序(例如 Uvicorn),启动**单个进程**,在所有 IP(`0.0.0.0`)上监听预定义端口(例如`80`)。 + +这是基本思路。 但您可能需要处理一些其他事情,例如: + +* 安全性 - HTTPS +* 启动时运行 +* 重新启动 +* Replication(运行的进程数) +* 内存 +* 开始前的步骤 + +在接下来的章节中,我将向您详细介绍每个概念、如何思考它们,以及一些具体示例以及处理它们的策略。 🚀 diff --git a/docs/zh/docs/deployment/server-workers.md b/docs/zh/docs/deployment/server-workers.md new file mode 100644 index 0000000000..ee3de9b5da --- /dev/null +++ b/docs/zh/docs/deployment/server-workers.md @@ -0,0 +1,184 @@ +# Server Workers - Gunicorn with Uvicorn + +让我们回顾一下之前的部署概念: + +* 安全性 - HTTPS +* 启动时运行 +* 重新启动 +* **复制(运行的进程数)** +* 内存 +* 启动前的先前步骤 + +到目前为止,通过文档中的所有教程,您可能已经在**单个进程**上运行了像 Uvicorn 这样的**服务器程序**。 + +部署应用程序时,您可能希望进行一些**进程复制**,以利用**多核**并能够处理更多请求。 + +正如您在上一章有关[部署概念](./concepts.md){.internal-link target=_blank}中看到的,您可以使用多种策略。 + +在这里我将向您展示如何将 **Gunicorn** 与 **Uvicorn worker 进程** 一起使用。 + +!!! info + 如果您正在使用容器,例如 Docker 或 Kubernetes,我将在下一章中告诉您更多相关信息:[容器中的 FastAPI - Docker](./docker.md){.internal-link target=_blank}。 + + 特别是,当在 **Kubernetes** 上运行时,您可能**不想**使用 Gunicorn,而是运行 **每个容器一个 Uvicorn 进程**,但我将在本章后面告诉您这一点。 + + + +## Gunicorn with Uvicorn Workers + +**Gunicorn**主要是一个使用**WSGI标准**的应用服务器。 这意味着 Gunicorn 可以为 Flask 和 Django 等应用程序提供服务。 Gunicorn 本身与 **FastAPI** 不兼容,因为 FastAPI 使用最新的 **ASGI 标准**。 + +但 Gunicorn 支持充当 **进程管理器** 并允许用户告诉它要使用哪个特定的 **worker类**。 然后 Gunicorn 将使用该类启动一个或多个 **worker进程**。 + +**Uvicorn** 有一个 Gunicorn 兼容的worker类。 + +使用这种组合,Gunicorn 将充当 **进程管理器**,监听 **端口** 和 **IP**。 它会将通信**传输**到运行**Uvicorn类**的worker进程。 + +然后与Gunicorn兼容的**Uvicorn worker**类将负责将Gunicorn发送的数据转换为ASGI标准以供FastAPI使用。 + +## 安装 Gunicorn 和 Uvicorn + +
+ +```console +$ pip install "uvicorn[standard]" gunicorn + +---> 100% +``` + +
+ +这将安装带有`standard`扩展包(以获得高性能)的 Uvicorn 和 Gunicorn。 + +## Run Gunicorn with Uvicorn Workers + +接下来你可以通过以下命令运行Gunicorn: + +
+ +```console +$ gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 + +[19499] [INFO] Starting gunicorn 20.1.0 +[19499] [INFO] Listening at: http://0.0.0.0:80 (19499) +[19499] [INFO] Using worker: uvicorn.workers.UvicornWorker +[19511] [INFO] Booting worker with pid: 19511 +[19513] [INFO] Booting worker with pid: 19513 +[19514] [INFO] Booting worker with pid: 19514 +[19515] [INFO] Booting worker with pid: 19515 +[19511] [INFO] Started server process [19511] +[19511] [INFO] Waiting for application startup. +[19511] [INFO] Application startup complete. +[19513] [INFO] Started server process [19513] +[19513] [INFO] Waiting for application startup. +[19513] [INFO] Application startup complete. +[19514] [INFO] Started server process [19514] +[19514] [INFO] Waiting for application startup. +[19514] [INFO] Application startup complete. +[19515] [INFO] Started server process [19515] +[19515] [INFO] Waiting for application startup. +[19515] [INFO] Application startup complete. +``` + +
+ + +让我们看看每个选项的含义: + +* `main:app`:这与 Uvicorn 使用的语法相同,`main` 表示名为"`main`"的 Python 模块,因此是文件 `main.py`。 `app` 是 **FastAPI** 应用程序的变量名称。 + * 你可以想象 `main:app` 相当于一个 Python `import` 语句,例如: + + ```Python + from main import app + ``` + + * 因此,`main:app` 中的冒号相当于 `from main import app` 中的 Python `import` 部分。 + +* `--workers`:要使用的worker进程数量,每个进程将运行一个 Uvicorn worker进程,在本例中为 4 个worker进程。 + +* `--worker-class`:在worker进程中使用的与 Gunicorn 兼容的工作类。 + * 这里我们传递了 Gunicorn 可以导入和使用的类: + + ```Python + import uvicorn.workers.UvicornWorker + ``` + +* `--bind`:这告诉 Gunicorn 要监听的 IP 和端口,使用冒号 (`:`) 分隔 IP 和端口。 + * 如果您直接运行 Uvicorn,则可以使用`--host 0.0.0.0`和`--port 80`,而不是`--bind 0.0.0.0:80`(Gunicorn 选项)。 + + +在输出中,您可以看到它显示了每个进程的 **PID**(进程 ID)(它只是一个数字)。 + +你可以看到: + +* Gunicorn **进程管理器** 以 PID `19499` 开头(在您的情况下,它将是一个不同的数字)。 +* 然后它开始`Listening at: http://0.0.0.0:80`。 +* 然后它检测到它必须使用 `uvicorn.workers.UvicornWorker` 处的worker类。 +* 然后它启动**4个worker**,每个都有自己的PID:`19511`、`19513`、`19514`和`19515`。 + +Gunicorn 还将负责管理**死进程**和**重新启动**新进程(如果需要保持worker数量)。 因此,这在一定程度上有助于上面列表中**重启**的概念。 + +尽管如此,您可能还希望有一些外部的东西,以确保在必要时**重新启动 Gunicorn**,并且**在启动时运行它**等。 + +## Uvicorn with Workers + +Uvicorn 也有一个选项可以启动和运行多个 **worker进程**。 + +然而,到目前为止,Uvicorn 处理worker进程的能力比 Gunicorn 更有限。 因此,如果您想拥有这个级别(Python 级别)的进程管理器,那么最好尝试使用 Gunicorn 作为进程管理器。 + +无论如何,您都可以像这样运行它: + +
+ +```console +$ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 +INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit) +INFO: Started parent process [27365] +INFO: Started server process [27368] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27369] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27370] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27367] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +这里唯一的新选项是 `--workers` 告诉 Uvicorn 启动 4 个工作进程。 + +您还可以看到它显示了每个进程的 **PID**,父进程(这是 **进程管理器**)的 PID 为`27365`,每个工作进程的 PID 为:`27368`、`27369`, `27370`和`27367`。 + +## 部署概念 + +在这里,您了解了如何使用 **Gunicorn**(或 Uvicorn)管理 **Uvicorn 工作进程**来**并行**应用程序的执行,利用 CPU 中的 **多核**,并 能够满足**更多请求**。 + +从上面的部署概念列表来看,使用worker主要有助于**复制**部分,并对**重新启动**有一点帮助,但您仍然需要照顾其他部分: + +* **安全 - HTTPS** +* **启动时运行** +* ***重新启动*** +* 复制(运行的进程数) +* **内存** +* **启动之前的先前步骤** + +## 容器和 Docker + +在关于 [容器中的 FastAPI - Docker](./docker.md){.internal-link target=_blank} 的下一章中,我将介绍一些可用于处理其他 **部署概念** 的策略。 + +我还将向您展示 **官方 Docker 镜像**,其中包括 **Gunicorn 和 Uvicorn worker** 以及一些对简单情况有用的默认配置。 + +在那里,我还将向您展示如何 **从头开始构建自己的镜像** 以运行单个 Uvicorn 进程(没有 Gunicorn)。 这是一个简单的过程,并且可能是您在使用像 **Kubernetes** 这样的分布式容器管理系统时想要做的事情。 + +## 回顾 + +您可以使用**Gunicorn**(或Uvicorn)作为Uvicorn工作进程的进程管理器,以利用**多核CPU**,**并行运行多个进程**。 + +如果您要设置**自己的部署系统**,同时自己处理其他部署概念,则可以使用这些工具和想法。 + +请查看下一章,了解带有容器(例如 Docker 和 Kubernetes)的 **FastAPI**。 您将看到这些工具也有简单的方法来解决其他**部署概念**。 ✨ diff --git a/docs/zh/docs/deployment/versions.md b/docs/zh/docs/deployment/versions.md new file mode 100644 index 0000000000..75b870139c --- /dev/null +++ b/docs/zh/docs/deployment/versions.md @@ -0,0 +1,87 @@ +# 关于 FastAPI 版本 + +**FastAPI** 已在许多应用程序和系统的生产环境中使用。 并且测试覆盖率保持在100%。 但其开发进度仍在快速推进。 + +经常添加新功能,定期修复错误,并且代码仍在持续改进。 + +这就是为什么当前版本仍然是`0.x.x`,这反映出每个版本都可能有Breaking changes。 这遵循语义版本控制的约定。 + +你现在就可以使用 **FastAPI** 创建生产环境应用程序(你可能已经这样做了一段时间),你只需确保使用的版本可以与其余代码正确配合即可。 + +## 固定你的 `fastapi` 版本 + +你应该做的第一件事是将你正在使用的 **FastAPI** 版本“固定”到你知道适用于你的应用程序的特定最新版本。 + +例如,假设你在应用程序中使用版本`0.45.0`。 + +如果你使用`requirements.txt`文件,你可以使用以下命令指定版本: + +````txt +fastapi==0.45.0 +```` + +这意味着你将使用版本`0.45.0`。 + +或者你也可以将其固定为: + +````txt +fastapi>=0.45.0,<0.46.0 +```` + +这意味着你将使用`0.45.0`或更高版本,但低于`0.46.0`,例如,版本`0.45.2`仍会被接受。 + +如果你使用任何其他工具来管理你的安装,例如 Poetry、Pipenv 或其他工具,它们都有一种定义包的特定版本的方法。 + +## 可用版本 + +你可以在[发行说明](../release-notes.md){.internal-link target=_blank}中查看可用版本(例如查看当前最新版本)。 + +## 关于版本 + +遵循语义版本控制约定,任何低于`1.0.0`的版本都可能会添加 breaking changes。 + +FastAPI 还遵循这样的约定:任何`PATCH`版本更改都是为了bug修复和non-breaking changes。 + +!!! tip + "PATCH"是最后一个数字,例如,在`0.2.3`中,PATCH版本是`3`。 + +因此,你应该能够固定到如下版本: + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +"MINOR"版本中会添加breaking changes和新功能。 + +!!! tip + "MINOR"是中间的数字,例如,在`0.2.3`中,MINOR版本是`2`。 + +## 升级FastAPI版本 + +你应该为你的应用程序添加测试。 + +使用 **FastAPI** 编写测试非常简单(感谢 Starlette),请参考文档:[测试](../tutorial/testing.md){.internal-link target=_blank} + +添加测试后,你可以将 **FastAPI** 版本升级到更新版本,并通过运行测试来确保所有代码都能正常工作。 + +如果一切正常,或者在进行必要的更改之后,并且所有测试都通过了,那么你可以将`fastapi`固定到新的版本。 + +## 关于Starlette + +你不应该固定`starlette`的版本。 + +不同版本的 **FastAPI** 将使用特定的较新版本的 Starlette。 + +因此,**FastAPI** 自己可以使用正确的 Starlette 版本。 + +## 关于 Pydantic + +Pydantic 包含针对 **FastAPI** 的测试及其自己的测试,因此 Pydantic 的新版本(`1.0.0`以上)始终与 FastAPI 兼容。 + +你可以将 Pydantic 固定到适合你的`1.0.0`以上和`2.0.0`以下的任何版本。 + +例如: + +````txt +pydantic>=1.2.0,<2.0.0 +```` diff --git a/docs/zh/docs/help-fastapi.md b/docs/zh/docs/help-fastapi.md index 2a99950e31..9b70d115a2 100644 --- a/docs/zh/docs/help-fastapi.md +++ b/docs/zh/docs/help-fastapi.md @@ -114,8 +114,6 @@ 聊天室仅供闲聊。 -我们之前还使用过 Gitter chat,但它不支持频道等高级功能,聊天也比较麻烦,所以现在推荐使用 Discord。 - ### 别在聊天室里提问 注意,聊天室更倾向于“闲聊”,经常有人会提出一些笼统得让人难以回答的问题,所以在这里提问一般没人回答。 diff --git a/docs/zh/docs/index.md b/docs/zh/docs/index.md index 4db3ef10c4..d776e58134 100644 --- a/docs/zh/docs/index.md +++ b/docs/zh/docs/index.md @@ -24,7 +24,7 @@ --- -FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 3.6+ 并基于标准的 Python 类型提示。 +FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 3.8+ 并基于标准的 Python 类型提示。 关键特性: @@ -107,7 +107,7 @@ FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框 ## 依赖 -Python 3.6 及更高版本 +Python 3.8 及更高版本 FastAPI 站在以下巨人的肩膀之上: @@ -323,7 +323,7 @@ def update_item(item_id: int, item: Item): 你不需要去学习新的语法、了解特定库的方法或类,等等。 -只需要使用标准的 **Python 3.6 及更高版本**。 +只需要使用标准的 **Python 3.8 及更高版本**。 举个例子,比如声明 `int` 类型: @@ -437,7 +437,6 @@ item: Item 用于 Pydantic: -* ujson - 更快的 JSON 「解析」。 * email_validator - 用于 email 校验。 用于 Starlette: diff --git a/docs/zh/docs/learn/index.md b/docs/zh/docs/learn/index.md new file mode 100644 index 0000000000..38696f6fea --- /dev/null +++ b/docs/zh/docs/learn/index.md @@ -0,0 +1,5 @@ +# 学习 + +以下是学习 **FastAPI** 的介绍部分和教程。 + +您可以认为这是一本 **书**,一门 **课程**,是 **官方** 且推荐的学习FastAPI的方法。😎 diff --git a/docs/zh/docs/tutorial/background-tasks.md b/docs/zh/docs/tutorial/background-tasks.md new file mode 100644 index 0000000000..94b75d4fdd --- /dev/null +++ b/docs/zh/docs/tutorial/background-tasks.md @@ -0,0 +1,126 @@ +# 后台任务 + +你可以定义在返回响应后运行的后台任务。 + +这对需要在请求之后执行的操作很有用,但客户端不必在接收响应之前等待操作完成。 + +包括这些例子: + +* 执行操作后发送的电子邮件通知: + * 由于连接到电子邮件服务器并发送电子邮件往往很“慢”(几秒钟),您可以立即返回响应并在后台发送电子邮件通知。 +* 处理数据: + * 例如,假设您收到的文件必须经过一个缓慢的过程,您可以返回一个"Accepted"(HTTP 202)响应并在后台处理它。 + +## 使用 `BackgroundTasks` + +首先导入 `BackgroundTasks` 并在 *路径操作函数* 中使用类型声明 `BackgroundTasks` 定义一个参数: + +```Python hl_lines="1 13" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +**FastAPI** 会创建一个 `BackgroundTasks` 类型的对象并作为该参数传入。 + +## 创建一个任务函数 + +创建要作为后台任务运行的函数。 + +它只是一个可以接收参数的标准函数。 + +它可以是 `async def` 或普通的 `def` 函数,**FastAPI** 知道如何正确处理。 + +在这种情况下,任务函数将写入一个文件(模拟发送电子邮件)。 + +由于写操作不使用 `async` 和 `await`,我们用普通的 `def` 定义函数: + +```Python hl_lines="6-9" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +## 添加后台任务 + +在你的 *路径操作函数* 里,用 `.add_task()` 方法将任务函数传到 *后台任务* 对象中: + +```Python hl_lines="14" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +`.add_task()` 接收以下参数: + +* 在后台运行的任务函数(`write_notification`)。 +* 应按顺序传递给任务函数的任意参数序列(`email`)。 +* 应传递给任务函数的任意关键字参数(`message="some notification"`)。 + +## 依赖注入 + +使用 `BackgroundTasks` 也适用于依赖注入系统,你可以在多个级别声明 `BackgroundTasks` 类型的参数:在 *路径操作函数* 里,在依赖中(可依赖),在子依赖中,等等。 + +**FastAPI** 知道在每种情况下该做什么以及如何复用同一对象,因此所有后台任务被合并在一起并且随后在后台运行: + +=== "Python 3.10+" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="14 16 23 26" + {!> ../../../docs_src/background_tasks/tutorial002_an.py!} + ``` + +=== "Python 3.10+ 没Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="11 13 20 23" + {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} + ``` + +=== "Python 3.8+ 没Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + +该示例中,信息会在响应发出 *之后* 被写到 `log.txt` 文件。 + +如果请求中有查询,它将在后台任务中写入日志。 + +然后另一个在 *路径操作函数* 生成的后台任务会使用路径参数 `email` 写入一条信息。 + +## 技术细节 + +`BackgroundTasks` 类直接来自 `starlette.background`。 + +它被直接导入/包含到FastAPI以便你可以从 `fastapi` 导入,并避免意外从 `starlette.background` 导入备用的 `BackgroundTask` (后面没有 `s`)。 + +通过仅使用 `BackgroundTasks` (而不是 `BackgroundTask`),使得能将它作为 *路径操作函数* 的参数 ,并让**FastAPI**为您处理其余部分, 就像直接使用 `Request` 对象。 + +在FastAPI中仍然可以单独使用 `BackgroundTask`,但您必须在代码中创建对象,并返回包含它的Starlette `Response`。 + +更多细节查看 Starlette's official docs for Background Tasks. + +## 告诫 + +如果您需要执行繁重的后台计算,并且不一定需要由同一进程运行(例如,您不需要共享内存、变量等),那么使用其他更大的工具(如 Celery)可能更好。 + +它们往往需要更复杂的配置,即消息/作业队列管理器,如RabbitMQ或Redis,但它们允许您在多个进程中运行后台任务,甚至是在多个服务器中。 + +要查看示例,查阅 [Project Generators](../project-generation.md){.internal-link target=_blank},它们都包括已经配置的Celery。 + +但是,如果您需要从同一个**FastAPI**应用程序访问变量和对象,或者您需要执行小型后台任务(如发送电子邮件通知),您只需使用 `BackgroundTasks` 即可。 + +## 回顾 + +导入并使用 `BackgroundTasks` 通过 *路径操作函数* 中的参数和依赖项来添加后台任务。 diff --git a/docs/zh/docs/tutorial/bigger-applications.md b/docs/zh/docs/tutorial/bigger-applications.md index 9f0134f683..1389595669 100644 --- a/docs/zh/docs/tutorial/bigger-applications.md +++ b/docs/zh/docs/tutorial/bigger-applications.md @@ -79,7 +79,7 @@ 你可以导入它并通过与 `FastAPI` 类相同的方式创建一个「实例」: -```Python hl_lines="1 3" +```Python hl_lines="1 3" title="app/routers/users.py" {!../../../docs_src/bigger_applications/app/routers/users.py!} ``` @@ -89,7 +89,7 @@ 使用方式与 `FastAPI` 类相同: -```Python hl_lines="6 11 16" +```Python hl_lines="6 11 16" title="app/routers/users.py" {!../../../docs_src/bigger_applications/app/routers/users.py!} ``` @@ -112,7 +112,7 @@ 现在我们将使用一个简单的依赖项来读取一个自定义的 `X-Token` 请求首部: -```Python hl_lines="1 4-6" +```Python hl_lines="1 4-6" title="app/dependencies.py" {!../../../docs_src/bigger_applications/app/dependencies.py!} ``` @@ -143,7 +143,7 @@ 因此,我们可以将其添加到 `APIRouter` 中,而不是将其添加到每个路径操作中。 -```Python hl_lines="5-10 16 21" +```Python hl_lines="5-10 16 21" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -195,7 +195,7 @@ async def read_item(item_id: str): 因此,我们通过 `..` 对依赖项使用了相对导入: -```Python hl_lines="3" +```Python hl_lines="3" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -265,7 +265,7 @@ from ...dependencies import get_token_header 但是我们仍然可以添加*更多*将会应用于特定的*路径操作*的 `tags`,以及一些特定于该*路径操作*的额外 `responses`: -```Python hl_lines="30-31" +```Python hl_lines="30-31" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -290,7 +290,7 @@ from ...dependencies import get_token_header 我们甚至可以声明[全局依赖项](dependencies/global-dependencies.md){.internal-link target=_blank},它会和每个 `APIRouter` 的依赖项组合在一起: -```Python hl_lines="1 3 7" +```Python hl_lines="1 3 7" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -298,7 +298,7 @@ from ...dependencies import get_token_header 现在,我们导入具有 `APIRouter` 的其他子模块: -```Python hl_lines="5" +```Python hl_lines="5" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -360,7 +360,7 @@ from .routers.users import router 因此,为了能够在同一个文件中使用它们,我们直接导入子模块: -```Python hl_lines="4" +```Python hl_lines="5" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -368,7 +368,7 @@ from .routers.users import router 现在,让我们来包含来自 `users` 和 `items` 子模块的 `router`。 -```Python hl_lines="10-11" +```Python hl_lines="10-11" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -401,7 +401,7 @@ from .routers.users import router 对于此示例,它将非常简单。但是假设由于它是与组织中的其他项目所共享的,因此我们无法对其进行修改,以及直接在 `APIRouter` 中添加 `prefix`、`dependencies`、`tags` 等: -```Python hl_lines="3" +```Python hl_lines="3" title="app/internal/admin.py" {!../../../docs_src/bigger_applications/app/internal/admin.py!} ``` @@ -409,7 +409,7 @@ from .routers.users import router 我们可以通过将这些参数传递给 `app.include_router()` 来完成所有的声明,而不必修改原始的 `APIRouter`: -```Python hl_lines="14-17" +```Python hl_lines="14-17" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -432,7 +432,7 @@ from .routers.users import router 这里我们这样做了...只是为了表明我们可以做到🤷: -```Python hl_lines="21-23" +```Python hl_lines="21-23" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` diff --git a/docs/zh/docs/tutorial/body-fields.md b/docs/zh/docs/tutorial/body-fields.md index 053cae71c7..fb6c6d9b6a 100644 --- a/docs/zh/docs/tutorial/body-fields.md +++ b/docs/zh/docs/tutorial/body-fields.md @@ -6,9 +6,41 @@ 首先,你必须导入它: -```Python hl_lines="2" -{!../../../docs_src/body_fields/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="2" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` !!! warning 注意,`Field` 是直接从 `pydantic` 导入的,而不是像其他的(`Query`,`Path`,`Body` 等)都从 `fastapi` 导入。 @@ -17,9 +49,41 @@ 然后,你可以对模型属性使用 `Field`: -```Python hl_lines="9-10" -{!../../../docs_src/body_fields/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12-15" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9-12" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` `Field` 的工作方式和 `Query`、`Path` 和 `Body` 相同,包括它们的参数等等也完全相同。 diff --git a/docs/zh/docs/tutorial/body-multiple-params.md b/docs/zh/docs/tutorial/body-multiple-params.md index 34fa5b638f..c93ef2f5ce 100644 --- a/docs/zh/docs/tutorial/body-multiple-params.md +++ b/docs/zh/docs/tutorial/body-multiple-params.md @@ -8,9 +8,41 @@ 你还可以通过将默认值设置为 `None` 来将请求体参数声明为可选参数: -```Python hl_lines="17-19" -{!../../../docs_src/body_multiple_params/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="17-19" + {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` !!! note 请注意,在这种情况下,将从请求体获取的 `item` 是可选的。因为它的默认值为 `None`。 @@ -30,9 +62,17 @@ 但是你也可以声明多个请求体参数,例如 `item` 和 `user`: -```Python hl_lines="20" -{!../../../docs_src/body_multiple_params/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` 在这种情况下,**FastAPI** 将注意到该函数中有多个请求体参数(两个 Pydantic 模型参数)。 @@ -72,9 +112,41 @@ 但是你可以使用 `Body` 指示 **FastAPI** 将其作为请求体的另一个键进行处理。 -```Python hl_lines="22" -{!../../../docs_src/body_multiple_params/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="24" + {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ``` 在这种情况下,**FastAPI** 将期望像这样的请求体: @@ -109,9 +181,41 @@ q: str = None 比如: -```Python hl_lines="25" -{!../../../docs_src/body_multiple_params/tutorial004.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="28" + {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="25" + {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` !!! info `Body` 同样具有与 `Query`、`Path` 以及其他后面将看到的类完全相同的额外校验和元数据参数。 @@ -131,9 +235,41 @@ item: Item = Body(embed=True) 比如: -```Python hl_lines="15" -{!../../../docs_src/body_multiple_params/tutorial005.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="15" + {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` 在这种情况下,**FastAPI** 将期望像这样的请求体: diff --git a/docs/zh/docs/tutorial/body-nested-models.md b/docs/zh/docs/tutorial/body-nested-models.md index 7649ee6fea..c65308bef7 100644 --- a/docs/zh/docs/tutorial/body-nested-models.md +++ b/docs/zh/docs/tutorial/body-nested-models.md @@ -6,9 +6,17 @@ 你可以将一个属性定义为拥有子元素的类型。例如 Python `list`: -```Python hl_lines="12" -{!../../../docs_src/body_nested_models/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial001.py!} + ``` 这将使 `tags` 成为一个由元素组成的列表。不过它没有声明每个元素的类型。 @@ -21,7 +29,7 @@ 首先,从 Python 的标准库 `typing` 模块中导入 `List`: ```Python hl_lines="1" -{!../../../docs_src/body_nested_models/tutorial002.py!} +{!> ../../../docs_src/body_nested_models/tutorial002.py!} ``` ### 声明具有子类型的 List @@ -43,9 +51,23 @@ my_list: List[str] 因此,在我们的示例中,我们可以将 `tags` 明确地指定为一个「字符串列表」: -```Python hl_lines="14" -{!../../../docs_src/body_nested_models/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002.py!} + ``` ## Set 类型 @@ -55,9 +77,23 @@ Python 具有一种特殊的数据类型来保存一组唯一的元素,即 `se 然后我们可以导入 `Set` 并将 `tag` 声明为一个由 `str` 组成的 `set`: -```Python hl_lines="1 14" -{!../../../docs_src/body_nested_models/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 14" + {!> ../../../docs_src/body_nested_models/tutorial003.py!} + ``` 这样,即使你收到带有重复数据的请求,这些数据也会被转换为一组唯一项。 @@ -79,17 +115,45 @@ Pydantic 模型的每个属性都具有类型。 例如,我们可以定义一个 `Image` 模型: -```Python hl_lines="9 10 11" -{!../../../docs_src/body_nested_models/tutorial004.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="7-9" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` ### 将子模型用作类型 然后我们可以将其用作一个属性的类型: -```Python hl_lines="20" -{!../../../docs_src/body_nested_models/tutorial004.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` 这意味着 **FastAPI** 将期望类似于以下内容的请求体: @@ -122,9 +186,23 @@ Pydantic 模型的每个属性都具有类型。 例如,在 `Image` 模型中我们有一个 `url` 字段,我们可以把它声明为 Pydantic 的 `HttpUrl`,而不是 `str`: -```Python hl_lines="4 10" -{!../../../docs_src/body_nested_models/tutorial005.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="2 8" + {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005.py!} + ``` 该字符串将被检查是否为有效的 URL,并在 JSON Schema / OpenAPI 文档中进行记录。 @@ -132,9 +210,23 @@ Pydantic 模型的每个属性都具有类型。 你还可以将 Pydantic 模型用作 `list`、`set` 等的子类型: -```Python hl_lines="20" -{!../../../docs_src/body_nested_models/tutorial006.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006.py!} + ``` 这将期望(转换,校验,记录文档等)下面这样的 JSON 请求体: @@ -169,9 +261,23 @@ Pydantic 模型的每个属性都具有类型。 你可以定义任意深度的嵌套模型: -```Python hl_lines="9 14 20 23 27" -{!../../../docs_src/body_nested_models/tutorial007.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="7 12 18 21 25" + {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007.py!} + ``` !!! info 请注意 `Offer` 拥有一组 `Item` 而反过来 `Item` 又是一个可选的 `Image` 列表是如何发生的。 @@ -186,9 +292,17 @@ images: List[Image] 例如: -```Python hl_lines="15" -{!../../../docs_src/body_nested_models/tutorial008.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="13" + {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_nested_models/tutorial008.py!} + ``` ## 无处不在的编辑器支持 @@ -218,9 +332,17 @@ images: List[Image] 在下面的例子中,你将接受任意键为 `int` 类型并且值为 `float` 类型的 `dict`: -```Python hl_lines="15" -{!../../../docs_src/body_nested_models/tutorial009.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="7" + {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/body_nested_models/tutorial009.py!} + ``` !!! tip 请记住 JSON 仅支持将 `str` 作为键。 diff --git a/docs/zh/docs/tutorial/body.md b/docs/zh/docs/tutorial/body.md index f80ab5bf59..5cf53c0c28 100644 --- a/docs/zh/docs/tutorial/body.md +++ b/docs/zh/docs/tutorial/body.md @@ -17,9 +17,17 @@ 首先,你需要从 `pydantic` 中导入 `BaseModel`: -```Python hl_lines="2" -{!../../../docs_src/body/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="2" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body/tutorial001.py!} + ``` ## 创建数据模型 @@ -27,9 +35,17 @@ 使用标准的 Python 类型来声明所有属性: -```Python hl_lines="5-9" -{!../../../docs_src/body/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="5-9" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="7-11" + {!> ../../../docs_src/body/tutorial001.py!} + ``` 和声明查询参数时一样,当一个模型属性具有默认值时,它不是必需的。否则它是一个必需属性。将默认值设为 `None` 可使其成为可选属性。 @@ -57,9 +73,17 @@ 使用与声明路径和查询参数的相同方式声明请求体,即可将其添加到「路径操作」中: -```Python hl_lines="16" -{!../../../docs_src/body/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} + ``` ...并且将它的类型声明为你创建的 `Item` 模型。 @@ -112,9 +136,17 @@ Pydantic 本身甚至也进行了一些更改以支持此功能。 在函数内部,你可以直接访问模型对象的所有属性: -```Python hl_lines="19" -{!../../../docs_src/body/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/body/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="21" + {!> ../../../docs_src/body/tutorial002.py!} + ``` ## 请求体 + 路径参数 @@ -122,9 +154,17 @@ Pydantic 本身甚至也进行了一些更改以支持此功能。 **FastAPI** 将识别出与路径参数匹配的函数参数应**从路径中获取**,而声明为 Pydantic 模型的函数参数应**从请求体中获取**。 -```Python hl_lines="15-16" -{!../../../docs_src/body/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="15-16" + {!> ../../../docs_src/body/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} + ``` ## 请求体 + 路径参数 + 查询参数 @@ -132,9 +172,17 @@ Pydantic 本身甚至也进行了一些更改以支持此功能。 **FastAPI** 会识别它们中的每一个,并从正确的位置获取数据。 -```Python hl_lines="16" -{!../../../docs_src/body/tutorial004.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial004_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} + ``` 函数参数将依次按如下规则进行识别: diff --git a/docs/zh/docs/tutorial/cookie-params.md b/docs/zh/docs/tutorial/cookie-params.md index d67daf0f90..f115f9677b 100644 --- a/docs/zh/docs/tutorial/cookie-params.md +++ b/docs/zh/docs/tutorial/cookie-params.md @@ -6,9 +6,41 @@ 首先,导入 `Cookie`: -```Python hl_lines="3" -{!../../../docs_src/cookie_params/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="1" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` ## 声明 `Cookie` 参数 @@ -17,9 +49,41 @@ 第一个值是参数的默认值,同时也可以传递所有验证参数或注释参数,来校验参数: -```Python hl_lines="9" -{!../../../docs_src/cookie_params/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="7" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` !!! note "技术细节" `Cookie` 、`Path` 、`Query`是兄弟类,它们都继承自公共的 `Param` 类 diff --git a/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md index 5813272ee5..1866da2984 100644 --- a/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md @@ -6,18 +6,18 @@ 在前面的例子中, 我们从依赖项 ("可依赖对象") 中返回了一个 `dict`: -=== "Python 3.6 以及 以上" - - ```Python hl_lines="9" - {!> ../../../docs_src/dependencies/tutorial001.py!} - ``` - -=== "Python 3.10 以及以上" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + 但是后面我们在路径操作函数的参数 `commons` 中得到了一个 `dict`。 我们知道编辑器不能为 `dict` 提供很多支持(比如补全),因为编辑器不知道 `dict` 的键和值类型。 @@ -79,46 +79,46 @@ fluffy = Cat(name="Mr Fluffy") 所以,我们可以将上面的依赖项 "可依赖对象" `common_parameters` 更改为类 `CommonQueryParams`: -=== "Python 3.6 以及 以上" - - ```Python hl_lines="11-15" - {!> ../../../docs_src/dependencies/tutorial002.py!} - ``` - -=== "Python 3.10 以及 以上" +=== "Python 3.10+" ```Python hl_lines="9-13" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` -注意用于创建类实例的 `__init__` 方法: +=== "Python 3.8+" -=== "Python 3.6 以及 以上" - - ```Python hl_lines="12" + ```Python hl_lines="11-15" {!> ../../../docs_src/dependencies/tutorial002.py!} ``` -=== "Python 3.10 以及 以上" +注意用于创建类实例的 `__init__` 方法: + +=== "Python 3.10+" ```Python hl_lines="10" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` -...它与我们以前的 `common_parameters` 具有相同的参数: +=== "Python 3.6+" -=== "Python 3.6 以及 以上" - - ```Python hl_lines="9" - {!> ../../../docs_src/dependencies/tutorial001.py!} + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} ``` -=== "Python 3.10 以及 以上" +...它与我们以前的 `common_parameters` 具有相同的参数: + +=== "Python 3.10+" ```Python hl_lines="6" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + 这些参数就是 **FastAPI** 用来 "处理" 依赖项的。 在两个例子下,都有: @@ -133,18 +133,18 @@ fluffy = Cat(name="Mr Fluffy") 现在,您可以使用这个类来声明你的依赖项了。 -=== "Python 3.6 以及 以上" - - ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial002.py!} - ``` - -=== "Python 3.10 以及 以上" +=== "Python 3.10+" ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + **FastAPI** 调用 `CommonQueryParams` 类。这将创建该类的一个 "实例",该实例将作为参数 `commons` 被传递给你的函数。 ## 类型注解 vs `Depends` @@ -183,18 +183,18 @@ commons = Depends(CommonQueryParams) ..就像: -=== "Python 3.6 以及 以上" - - ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial003.py!} - ``` - -=== "Python 3.10 以及 以上" +=== "Python 3.10+" ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial003_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + 但是声明类型是被鼓励的,因为那样你的编辑器就会知道将传递什么作为参数 `commons` ,然后它可以帮助你完成代码,类型检查,等等: @@ -227,18 +227,18 @@ commons: CommonQueryParams = Depends() 同样的例子看起来像这样: -=== "Python 3.6 以及 以上" - - ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial004.py!} - ``` - -=== "Python 3.10 以及 以上" +=== "Python 3.10+" ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial004_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + ... **FastAPI** 会知道怎么处理。 !!! tip diff --git a/docs/zh/docs/tutorial/dependencies/index.md b/docs/zh/docs/tutorial/dependencies/index.md index c717da0f63..7a133061de 100644 --- a/docs/zh/docs/tutorial/dependencies/index.md +++ b/docs/zh/docs/tutorial/dependencies/index.md @@ -1,4 +1,4 @@ -# 依赖项 - 第一步 +# 依赖项 FastAPI 提供了简单易用,但功能强大的**依赖注入**系统。 diff --git a/docs/zh/docs/tutorial/encoder.md b/docs/zh/docs/tutorial/encoder.md index cb813940ce..859ebc2e87 100644 --- a/docs/zh/docs/tutorial/encoder.md +++ b/docs/zh/docs/tutorial/encoder.md @@ -20,18 +20,18 @@ 它接收一个对象,比如Pydantic模型,并会返回一个JSON兼容的版本: -=== "Python 3.6 and above" - - ```Python hl_lines="5 22" - {!> ../../../docs_src/encoder/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="4 21" {!> ../../../docs_src/encoder/tutorial001_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + 在这个例子中,它将Pydantic模型转换为`dict`,并将`datetime`转换为`str`。 调用它的结果后就可以使用Python标准编码中的`json.dumps()`。 diff --git a/docs/zh/docs/tutorial/extra-data-types.md b/docs/zh/docs/tutorial/extra-data-types.md index ac3e076545..f4a77050ca 100644 --- a/docs/zh/docs/tutorial/extra-data-types.md +++ b/docs/zh/docs/tutorial/extra-data-types.md @@ -44,23 +44,87 @@ * 产生的模式将指定那些 `set` 的值是唯一的 (使用 JSON 模式的 `uniqueItems`)。 * `bytes`: * 标准的 Python `bytes`。 - * 在请求和相应中被当作 `str` 处理。 + * 在请求和响应中被当作 `str` 处理。 * 生成的模式将指定这个 `str` 是 `binary` "格式"。 * `Decimal`: * 标准的 Python `Decimal`。 - * 在请求和相应中被当做 `float` 一样处理。 + * 在请求和响应中被当做 `float` 一样处理。 * 您可以在这里检查所有有效的pydantic数据类型: Pydantic data types. ## 例子 下面是一个*路径操作*的示例,其中的参数使用了上面的一些类型。 -```Python hl_lines="1 3 12-16" -{!../../../docs_src/extra_data_types/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 3 13-17" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="1 2 11-15" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="1 2 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` 注意,函数内的参数有原生的数据类型,你可以,例如,执行正常的日期操作,如: -```Python hl_lines="18-19" -{!../../../docs_src/extra_data_types/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="19-20" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="17-18" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` diff --git a/docs/zh/docs/tutorial/extra-models.md b/docs/zh/docs/tutorial/extra-models.md index 1fbe77be8d..06427a73d4 100644 --- a/docs/zh/docs/tutorial/extra-models.md +++ b/docs/zh/docs/tutorial/extra-models.md @@ -17,9 +17,17 @@ 下面是应该如何根据它们的密码字段以及使用位置去定义模型的大概思路: -```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" -{!../../../docs_src/extra_models/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" + {!> ../../../docs_src/extra_models/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" + {!> ../../../docs_src/extra_models/tutorial001.py!} + ``` ### 关于 `**user_in.dict()` @@ -150,9 +158,17 @@ UserInDB( 这样,我们可以仅声明模型之间的差异部分(具有明文的 `password`、具有 `hashed_password` 以及不包括密码)。 -```Python hl_lines="9 15-16 19-20 23-24" -{!../../../docs_src/extra_models/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="7 13-14 17-18 21-22" + {!> ../../../docs_src/extra_models/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} + ``` ## `Union` 或者 `anyOf` @@ -166,9 +182,17 @@ UserInDB( !!! note 定义一个 `Union` 类型时,首先包括最详细的类型,然后是不太详细的类型。在下面的示例中,更详细的 `PlaneItem` 位于 `Union[PlaneItem,CarItem]` 中的 `CarItem` 之前。 -```Python hl_lines="1 14-15 18-20 33" -{!../../../docs_src/extra_models/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003.py!} + ``` ## 模型列表 @@ -176,9 +200,17 @@ UserInDB( 为此,请使用标准的 Python `typing.List`: -```Python hl_lines="1 20" -{!../../../docs_src/extra_models/tutorial004.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="18" + {!> ../../../docs_src/extra_models/tutorial004_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} + ``` ## 任意 `dict` 构成的响应 @@ -188,9 +220,17 @@ UserInDB( 在这种情况下,你可以使用 `typing.Dict`: -```Python hl_lines="1 8" -{!../../../docs_src/extra_models/tutorial005.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="6" + {!> ../../../docs_src/extra_models/tutorial005_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} + ``` ## 总结 diff --git a/docs/zh/docs/tutorial/handling-errors.md b/docs/zh/docs/tutorial/handling-errors.md index 9b066bc2ce..a0d66e557c 100644 --- a/docs/zh/docs/tutorial/handling-errors.md +++ b/docs/zh/docs/tutorial/handling-errors.md @@ -145,7 +145,7 @@ ``` -访问 `/items/foo`,可以看到以下内容替换了默认 JSON 错误信息: +访问 `/items/foo`,可以看到默认的 JSON 错误信息: ```JSON { @@ -163,7 +163,7 @@ ``` -以下是文本格式的错误信息: +被替换为了以下文本格式的错误信息: ``` 1 validation error diff --git a/docs/zh/docs/tutorial/header-params.md b/docs/zh/docs/tutorial/header-params.md index c4b1c38cee..2701167b32 100644 --- a/docs/zh/docs/tutorial/header-params.md +++ b/docs/zh/docs/tutorial/header-params.md @@ -6,9 +6,41 @@ 首先导入 `Header`: -```Python hl_lines="3" -{!../../../docs_src/header_params/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="1" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` ## 声明 `Header` 参数 @@ -16,9 +48,41 @@ 第一个值是默认值,你可以传递所有的额外验证或注释参数: -```Python hl_lines="9" -{!../../../docs_src/header_params/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` !!! note "技术细节" `Header` 是 `Path`, `Query` 和 `Cookie` 的兄弟类型。它也继承自通用的 `Param` 类. @@ -44,9 +108,41 @@ 如果出于某些原因,你需要禁用下划线到连字符的自动转换,设置`Header`的参数 `convert_underscores` 为 `False`: -```Python hl_lines="10" -{!../../../docs_src/header_params/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11" + {!> ../../../docs_src/header_params/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12" + {!> ../../../docs_src/header_params/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="8" + {!> ../../../docs_src/header_params/tutorial002_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} + ``` !!! warning 在设置 `convert_underscores` 为 `False` 之前,请记住,一些HTTP代理和服务器不允许使用带有下划线的headers。 @@ -62,9 +158,50 @@ 比如, 为了声明一个 `X-Token` header 可以出现多次,你可以这样写: -```Python hl_lines="9" -{!../../../docs_src/header_params/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial003_py310.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_py39.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} + ``` 如果你与*路径操作*通信时发送两个HTTP headers,就像: diff --git a/docs/zh/docs/tutorial/index.md b/docs/zh/docs/tutorial/index.md index 6093caeb60..6180d3de39 100644 --- a/docs/zh/docs/tutorial/index.md +++ b/docs/zh/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# 教程 - 用户指南 - 简介 +# 教程 - 用户指南 本教程将一步步向你展示如何使用 **FastAPI** 的绝大部分特性。 diff --git a/docs/zh/docs/tutorial/path-params-numeric-validations.md b/docs/zh/docs/tutorial/path-params-numeric-validations.md index 13512a08ed..9b41ad7cf4 100644 --- a/docs/zh/docs/tutorial/path-params-numeric-validations.md +++ b/docs/zh/docs/tutorial/path-params-numeric-validations.md @@ -6,9 +6,41 @@ 首先,从 `fastapi` 导入 `Path`: -```Python hl_lines="1" -{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3-4" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="1" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` ## 声明元数据 @@ -16,9 +48,41 @@ 例如,要声明路径参数 `item_id`的 `title` 元数据值,你可以输入: -```Python hl_lines="8" -{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` !!! note 路径参数总是必需的,因为它必须是路径的一部分。 @@ -43,9 +107,14 @@ 因此,你可以将函数声明为: -```Python hl_lines="7" -{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} -``` +=== "Python 3.8 non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="7" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002.py!} + ``` ## 按需对参数排序的技巧 diff --git a/docs/zh/docs/tutorial/query-params-str-validations.md b/docs/zh/docs/tutorial/query-params-str-validations.md index 070074839f..39253eb0d4 100644 --- a/docs/zh/docs/tutorial/query-params-str-validations.md +++ b/docs/zh/docs/tutorial/query-params-str-validations.md @@ -4,9 +4,17 @@ 让我们以下面的应用程序为例: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} + ``` 查询参数 `q` 的类型为 `str`,默认值为 `None`,因此它是可选的。 diff --git a/docs/zh/docs/tutorial/request-files.md b/docs/zh/docs/tutorial/request-files.md index e18d6fc9f3..2c48f33cac 100644 --- a/docs/zh/docs/tutorial/request-files.md +++ b/docs/zh/docs/tutorial/request-files.md @@ -124,18 +124,18 @@ contents = myfile.file.read() 您可以通过使用标准类型注解并将 None 作为默认值的方式将一个文件参数设为可选: -=== "Python 3.6 及以上版本" - - ```Python hl_lines="9 17" - {!> ../../../docs_src/request_files/tutorial001_02.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="7 14" {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} + ``` + ## 带有额外元数据的 `UploadFile` 您也可以将 `File()` 与 `UploadFile` 一起使用,例如,设置额外的元数据: @@ -152,18 +152,18 @@ FastAPI 支持同时上传多个文件。 上传多个文件时,要声明含 `bytes` 或 `UploadFile` 的列表(`List`): -=== "Python 3.6 及以上版本" - - ```Python hl_lines="10 15" - {!> ../../../docs_src/request_files/tutorial002.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="8 13" {!> ../../../docs_src/request_files/tutorial002_py39.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002.py!} + ``` + 接收的也是含 `bytes` 或 `UploadFile` 的列表(`list`)。 @@ -177,18 +177,18 @@ FastAPI 支持同时上传多个文件。 和之前的方式一样, 您可以为 `File()` 设置额外参数, 即使是 `UploadFile`: -=== "Python 3.6 及以上版本" - - ```Python hl_lines="18" - {!> ../../../docs_src/request_files/tutorial003.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="16" {!> ../../../docs_src/request_files/tutorial003_py39.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + ## 小结 本节介绍了如何用 `File` 把上传文件声明为(表单数据的)输入参数。 diff --git a/docs/zh/docs/tutorial/response-model.md b/docs/zh/docs/tutorial/response-model.md index ea3d0666de..e731b6989e 100644 --- a/docs/zh/docs/tutorial/response-model.md +++ b/docs/zh/docs/tutorial/response-model.md @@ -8,9 +8,23 @@ * `@app.delete()` * 等等。 -```Python hl_lines="17" -{!../../../docs_src/response_model/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001.py!} + ``` !!! note 注意,`response_model`是「装饰器」方法(`get`,`post` 等)的一个参数。不像之前的所有参数和请求体,它不属于*路径操作函数*。 @@ -58,21 +72,45 @@ FastAPI 将使用此 `response_model` 来: 相反,我们可以创建一个有明文密码的输入模型和一个没有明文密码的输出模型: -```Python hl_lines="9 11 16" -{!../../../docs_src/response_model/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` 这样,即便我们的*路径操作函数*将会返回包含密码的相同输入用户: -```Python hl_lines="24" -{!../../../docs_src/response_model/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` ...我们已经将 `response_model` 声明为了不包含密码的 `UserOut` 模型: -```Python hl_lines="22" -{!../../../docs_src/response_model/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` 因此,**FastAPI** 将会负责过滤掉未在输出模型中声明的所有数据(使用 Pydantic)。 diff --git a/docs/zh/docs/tutorial/schema-extra-example.md b/docs/zh/docs/tutorial/schema-extra-example.md index 8f5fbfe70b..ebc04da8b4 100644 --- a/docs/zh/docs/tutorial/schema-extra-example.md +++ b/docs/zh/docs/tutorial/schema-extra-example.md @@ -10,9 +10,17 @@ 您可以使用 `Config` 和 `schema_extra` 为Pydantic模型声明一个示例,如Pydantic 文档:定制 Schema 中所述: -```Python hl_lines="15-23" -{!../../../docs_src/schema_extra_example/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="13-21" + {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="15-23" + {!> ../../../docs_src/schema_extra_example/tutorial001.py!} + ``` 这些额外的信息将按原样添加到输出的JSON模式中。 @@ -20,9 +28,17 @@ 在 `Field`, `Path`, `Query`, `Body` 和其他你之后将会看到的工厂函数,你可以为JSON 模式声明额外信息,你也可以通过给工厂函数传递其他的任意参数来给JSON 模式声明额外信息,比如增加 `example`: -```Python hl_lines="4 10-13" -{!../../../docs_src/schema_extra_example/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="2 8-11" + {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4 10-13" + {!> ../../../docs_src/schema_extra_example/tutorial002.py!} + ``` !!! warning 请记住,传递的那些额外参数不会添加任何验证,只会添加注释,用于文档的目的。 @@ -33,9 +49,41 @@ 比如,你可以将请求体的一个 `example` 传递给 `Body`: -```Python hl_lines="20-25" -{!../../../docs_src/schema_extra_example/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="23-28" + {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="18-23" + {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="20-25" + {!> ../../../docs_src/schema_extra_example/tutorial003.py!} + ``` ## 文档 UI 中的例子 diff --git a/docs/zh/docs/tutorial/security/first-steps.md b/docs/zh/docs/tutorial/security/first-steps.md index 86c3320ce1..dda9564177 100644 --- a/docs/zh/docs/tutorial/security/first-steps.md +++ b/docs/zh/docs/tutorial/security/first-steps.md @@ -20,9 +20,26 @@ 把下面的示例代码复制到 `main.py`: -```Python -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python + {!> ../../../docs_src/security/tutorial001.py!} + ``` ## 运行 diff --git a/docs/zh/docs/tutorial/security/index.md b/docs/zh/docs/tutorial/security/index.md index 8f302a16c0..0595f5f636 100644 --- a/docs/zh/docs/tutorial/security/index.md +++ b/docs/zh/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# 安全性简介 +# 安全性 有许多方法可以处理安全性、身份认证和授权等问题。 diff --git a/docs/zh/docs/tutorial/security/oauth2-jwt.md b/docs/zh/docs/tutorial/security/oauth2-jwt.md index 054198545e..33a4d7fc76 100644 --- a/docs/zh/docs/tutorial/security/oauth2-jwt.md +++ b/docs/zh/docs/tutorial/security/oauth2-jwt.md @@ -170,7 +170,7 @@ $ openssl rand -hex 32 创建并返回真正的 JWT 访问令牌。 -```Python hl_lines="115-128" +```Python hl_lines="115-130" {!../../../docs_src/security/tutorial004.py!} ``` diff --git a/docs/zh/docs/tutorial/security/simple-oauth2.md b/docs/zh/docs/tutorial/security/simple-oauth2.md index 276f3d63b6..c7f46177f0 100644 --- a/docs/zh/docs/tutorial/security/simple-oauth2.md +++ b/docs/zh/docs/tutorial/security/simple-oauth2.md @@ -1,94 +1,98 @@ -# 使用密码和 Bearer 的简单 OAuth2 +# OAuth2 实现简单的 Password 和 Bearer 验证 -现在让我们接着上一章继续开发,并添加缺少的部分以实现一个完整的安全性流程。 +本章添加上一章示例中欠缺的部分,实现完整的安全流。 ## 获取 `username` 和 `password` -我们将使用 **FastAPI** 的安全性实用工具来获取 `username` 和 `password`。 +首先,使用 **FastAPI** 安全工具获取 `username` 和 `password`。 -OAuth2 规定在使用(我们打算用的)「password 流程」时,客户端/用户必须将 `username` 和 `password` 字段作为表单数据发送。 +OAuth2 规范要求使用**密码流**时,客户端或用户必须以表单数据形式发送 `username` 和 `password` 字段。 -而且规范明确了字段必须这样命名。因此 `user-name` 或 `email` 是行不通的。 +并且,这两个字段必须命名为 `username` 和 `password` ,不能使用 `user-name` 或 `email` 等其它名称。 -不过不用担心,你可以在前端按照你的想法将它展示给最终用户。 +不过也不用担心,前端仍可以显示终端用户所需的名称。 -而且你的数据库模型也可以使用你想用的任何其他名称。 +数据库模型也可以使用所需的名称。 -但是对于登录*路径操作*,我们需要使用这些名称来与规范兼容(以具备例如使用集成的 API 文档系统的能力)。 +但对于登录*路径操作*,则要使用兼容规范的 `username` 和 `password`,(例如,实现与 API 文档集成)。 -规范还写明了 `username` 和 `password` 必须作为表单数据发送(因此,此处不能使用 JSON)。 +该规范要求必须以表单数据形式发送 `username` 和 `password`,因此,不能使用 JSON 对象。 -### `scope` +### `Scope`(作用域) -规范还提到客户端可以发送另一个表单字段「`scope`」。 +OAuth2 还支持客户端发送**`scope`**表单字段。 -这个表单字段的名称为 `scope`(单数形式),但实际上它是一个由空格分隔的「作用域」组成的长字符串。 +虽然表单字段的名称是 `scope`(单数),但实际上,它是以空格分隔的,由多个**scope**组成的长字符串。 -每个「作用域」只是一个字符串(中间没有空格)。 +**作用域**只是不带空格的字符串。 -它们通常用于声明特定的安全权限,例如: +常用于声明指定安全权限,例如: -* `users:read` 或者 `users:write` 是常见的例子。 -* Facebook / Instagram 使用 `instagram_basic`。 -* Google 使用了 `https://www.googleapis.com/auth/drive` 。 +* 常见用例为,`users:read` 或 `users:write` +* 脸书和 Instagram 使用 `instagram_basic` +* 谷歌使用 `https://www.googleapis.com/auth/drive` -!!! info - 在 OAuth2 中「作用域」只是一个声明所需特定权限的字符串。 +!!! info "说明" - 它有没有 `:` 这样的其他字符或者是不是 URL 都没有关系。 + OAuth2 中,**作用域**只是声明指定权限的字符串。 - 这些细节是具体的实现。 + 是否使用冒号 `:` 等符号,或是不是 URL 并不重要。 - 对 OAuth2 来说它们就只是字符串而已。 + 这些细节只是特定的实现方式。 + + 对 OAuth2 来说,都只是字符串而已。 ## 获取 `username` 和 `password` 的代码 -现在,让我们使用 **FastAPI** 提供的实用工具来处理此问题。 +接下来,使用 **FastAPI** 工具获取用户名与密码。 ### `OAuth2PasswordRequestForm` -首先,导入 `OAuth2PasswordRequestForm`,然后在 `token` 的*路径操作*中通过 `Depends` 将其作为依赖项使用。 +首先,导入 `OAuth2PasswordRequestForm`,然后,在 `/token` *路径操作* 中,用 `Depends` 把该类作为依赖项。 ```Python hl_lines="4 76" {!../../../docs_src/security/tutorial003.py!} ``` -`OAuth2PasswordRequestForm` 是一个类依赖项,声明了如下的请求表单: +`OAuth2PasswordRequestForm` 是用以下几项内容声明表单请求体的类依赖项: -* `username`。 -* `password`。 -* 一个可选的 `scope` 字段,是一个由空格分隔的字符串组成的大字符串。 -* 一个可选的 `grant_type`. +* `username` +* `password` +* 可选的 `scope` 字段,由多个空格分隔的字符串组成的长字符串 +* 可选的 `grant_type` -!!! tip - OAuth2 规范实际上*要求* `grant_type` 字段使用一个固定的值 `password`,但是 `OAuth2PasswordRequestForm` 没有作强制约束。 +!!! tip "提示" - 如果你需要强制要求这一点,请使用 `OAuth2PasswordRequestFormStrict` 而不是 `OAuth2PasswordRequestForm`。 + 实际上,OAuth2 规范*要求* `grant_type` 字段使用固定值 `password`,但 `OAuth2PasswordRequestForm` 没有作强制约束。 -* 一个可选的 `client_id`(我们的示例不需要它)。 -* 一个可选的 `client_secret`(我们的示例不需要它)。 + 如需强制使用固定值 `password`,则不要用 `OAuth2PasswordRequestForm`,而是用 `OAuth2PasswordRequestFormStrict`。 -!!! info - `OAuth2PasswordRequestForm` 并不像 `OAuth2PasswordBearer` 一样是 FastAPI 的一个特殊的类。 +* 可选的 `client_id`(本例未使用) +* 可选的 `client_secret`(本例未使用) - `OAuth2PasswordBearer` 使得 **FastAPI** 明白它是一个安全方案。所以它得以通过这种方式添加到 OpenAPI 中。 +!!! info "说明" - 但 `OAuth2PasswordRequestForm` 只是一个你可以自己编写的类依赖项,或者你也可以直接声明 `Form` 参数。 + `OAuth2PasswordRequestForm` 与 `OAuth2PasswordBearer` 一样,都不是 FastAPI 的特殊类。 - 但是由于这是一种常见的使用场景,因此 FastAPI 出于简便直接提供了它。 + **FastAPI** 把 `OAuth2PasswordBearer` 识别为安全方案。因此,可以通过这种方式把它添加至 OpenAPI。 + + 但 `OAuth2PasswordRequestForm` 只是可以自行编写的类依赖项,也可以直接声明 `Form` 参数。 + + 但由于这种用例很常见,FastAPI 为了简便,就直接提供了对它的支持。 ### 使用表单数据 -!!! tip - 类依赖项 `OAuth2PasswordRequestForm` 的实例不会有用空格分隔的长字符串属性 `scope`,而是具有一个 `scopes` 属性,该属性将包含实际被发送的每个作用域字符串组成的列表。 +!!! tip "提示" - 在此示例中我们没有使用 `scopes`,但如果你需要的话可以使用该功能。 + `OAuth2PasswordRequestForm` 类依赖项的实例没有以空格分隔的长字符串属性 `scope`,但它支持 `scopes` 属性,由已发送的 scope 字符串列表组成。 -现在,使用表单字段中的 `username` 从(伪)数据库中获取用户数据。 + 本例没有使用 `scopes`,但开发者也可以根据需要使用该属性。 -如果没有这个用户,我们将返回一个错误消息,提示「用户名或密码错误」。 +现在,即可使用表单字段 `username`,从(伪)数据库中获取用户数据。 -对于这个错误,我们使用 `HTTPException` 异常: +如果不存在指定用户,则返回错误消息,提示**用户名或密码错误**。 + +本例使用 `HTTPException` 异常显示此错误: ```Python hl_lines="3 77-79" {!../../../docs_src/security/tutorial003.py!} @@ -96,27 +100,27 @@ OAuth2 规定在使用(我们打算用的)「password 流程」时,客户 ### 校验密码 -目前我们已经从数据库中获取了用户数据,但尚未校验密码。 +至此,我们已经从数据库中获取了用户数据,但尚未校验密码。 -让我们首先将这些数据放入 Pydantic `UserInDB` 模型中。 +接下来,首先将数据放入 Pydantic 的 `UserInDB` 模型。 -永远不要保存明文密码,因此,我们将使用(伪)哈希密码系统。 +注意:永远不要保存明文密码,本例暂时先使用(伪)哈希密码系统。 -如果密码不匹配,我们将返回同一个错误。 +如果密码不匹配,则返回与上面相同的错误。 -#### 哈希密码 +#### 密码哈希 -「哈希」的意思是:将某些内容(在本例中为密码)转换为看起来像乱码的字节序列(只是一个字符串)。 +**哈希**是指,将指定内容(本例中为密码)转换为形似乱码的字节序列(其实就是字符串)。 -每次你传入完全相同的内容(完全相同的密码)时,你都会得到完全相同的乱码。 +每次传入完全相同的内容(比如,完全相同的密码)时,得到的都是完全相同的乱码。 -但是你不能从乱码转换回密码。 +但这个乱码无法转换回传入的密码。 -##### 为什么使用哈希密码 +##### 为什么使用密码哈希 -如果你的数据库被盗,小偷将无法获得用户的明文密码,只有哈希值。 +原因很简单,假如数据库被盗,窃贼无法获取用户的明文密码,得到的只是哈希值。 -因此,小偷将无法尝试在另一个系统中使用这些相同的密码(由于许多用户在任何地方都使用相同的密码,因此这很危险)。 +这样一来,窃贼就无法在其它应用中使用窃取的密码,要知道,很多用户在所有系统中都使用相同的密码,风险超大。 ```Python hl_lines="80-83" {!../../../docs_src/security/tutorial003.py!} @@ -124,9 +128,9 @@ OAuth2 规定在使用(我们打算用的)「password 流程」时,客户 #### 关于 `**user_dict` -`UserInDB(**user_dict)` 表示: +`UserInDB(**user_dict)` 是指: -*直接将 `user_dict` 的键和值作为关键字参数传递,等同于:* +*直接把 `user_dict` 的键与值当作关键字参数传递,等效于:* ```Python UserInDB( @@ -138,75 +142,79 @@ UserInDB( ) ``` -!!! info - 有关 `user_dict` 的更完整说明,请参阅[**额外的模型**文档](../extra-models.md#about-user_indict){.internal-link target=_blank}。 +!!! info "说明" -## 返回令牌 + `user_dict` 的说明,详见[**更多模型**一章](../extra-models.md#about-user_indict){.internal-link target=_blank}。 -`token` 端点的响应必须是一个 JSON 对象。 +## 返回 Token -它应该有一个 `token_type`。在我们的例子中,由于我们使用的是「Bearer」令牌,因此令牌类型应为「`bearer`」。 +`token` 端点的响应必须是 JSON 对象。 -并且还应该有一个 `access_token` 字段,它是一个包含我们的访问令牌的字符串。 +响应返回的内容应该包含 `token_type`。本例中用的是**Bearer**Token,因此, Token 类型应为**`bearer`**。 -对于这个简单的示例,我们将极其不安全地返回相同的 `username` 作为令牌。 +返回内容还应包含 `access_token` 字段,它是包含权限 Token 的字符串。 -!!! tip - 在下一章中,你将看到一个真实的安全实现,使用了哈希密码和 JWT 令牌。 +本例只是简单的演示,返回的 Token 就是 `username`,但这种方式极不安全。 - 但现在,让我们仅关注我们需要的特定细节。 +!!! tip "提示" + + 下一章介绍使用哈希密码和 JWT Token 的真正安全机制。 + + 但现在,仅关注所需的特定细节。 ```Python hl_lines="85" {!../../../docs_src/security/tutorial003.py!} ``` -!!! tip - 根据规范,你应该像本示例一样,返回一个带有 `access_token` 和 `token_type` 的 JSON。 +!!! tip "提示" - 这是你必须在代码中自行完成的工作,并且要确保使用了这些 JSON 字段。 + 按规范的要求,应像本示例一样,返回带有 `access_token` 和 `token_type` 的 JSON 对象。 - 这几乎是唯一的你需要自己记住并正确地执行以符合规范的事情。 + 这是开发者必须在代码中自行完成的工作,并且要确保使用这些 JSON 的键。 - 其余的,**FastAPI** 都会为你处理。 + 这几乎是唯一需要开发者牢记在心,并按规范要求正确执行的事。 + + **FastAPI** 则负责处理其它的工作。 ## 更新依赖项 -现在我们将更新我们的依赖项。 +接下来,更新依赖项。 -我们想要仅当此用户处于启用状态时才能获取 `current_user`。 +使之仅在当前用户为激活状态时,才能获取 `current_user`。 -因此,我们创建了一个额外的依赖项 `get_current_active_user`,而该依赖项又以 `get_current_user` 作为依赖项。 +为此,要再创建一个依赖项 `get_current_active_user`,此依赖项以 `get_current_user` 依赖项为基础。 -如果用户不存在或处于未启用状态,则这两个依赖项都将仅返回 HTTP 错误。 +如果用户不存在,或状态为未激活,这两个依赖项都会返回 HTTP 错误。 -因此,在我们的端点中,只有当用户存在,身份认证通过且处于启用状态时,我们才能获得该用户: +因此,在端点中,只有当用户存在、通过身份验证、且状态为激活时,才能获得该用户: ```Python hl_lines="58-67 69-72 90" {!../../../docs_src/security/tutorial003.py!} ``` -!!! info - 我们在此处返回的值为 `Bearer` 的额外响应头 `WWW-Authenticate` 也是规范的一部分。 +!!! info "说明" - 任何的 401「未认证」HTTP(错误)状态码都应该返回 `WWW-Authenticate` 响应头。 + 此处返回值为 `Bearer` 的响应头 `WWW-Authenticate` 也是规范的一部分。 - 对于 bearer 令牌(我们的例子),该响应头的值应为 `Bearer`。 + 任何 401**UNAUTHORIZED**HTTP(错误)状态码都应返回 `WWW-Authenticate` 响应头。 - 实际上你可以忽略这个额外的响应头,不会有什么问题。 + 本例中,因为使用的是 Bearer Token,该响应头的值应为 `Bearer`。 - 但此处提供了它以符合规范。 + 实际上,忽略这个附加响应头,也不会有什么问题。 - 而且,(现在或将来)可能会有工具期望得到并使用它,然后对你或你的用户有用处。 + 之所以在此提供这个附加响应头,是为了符合规范的要求。 - 这就是遵循标准的好处... + 说不定什么时候,就有工具用得上它,而且,开发者或用户也可能用得上。 + + 这就是遵循标准的好处…… ## 实际效果 -打开交互式文档:http://127.0.0.1:8000/docs。 +打开 API 文档:http://127.0.0.1:8000/docs。 -### 身份认证 +### 身份验证 -点击「Authorize」按钮。 +点击**Authorize**按钮。 使用以下凭证: @@ -216,15 +224,15 @@ UserInDB( -在系统中进行身份认证后,你将看到: +通过身份验证后,显示下图所示的内容: -### 获取本人的用户数据 +### 获取当前用户数据 -现在执行 `/users/me` 路径的 `GET` 操作。 +使用 `/users/me` 路径的 `GET` 操作。 -你将获得你的用户数据,如: +可以提取如下当前用户数据: ```JSON { @@ -238,7 +246,7 @@ UserInDB( -如果你点击锁定图标并注销,然后再次尝试同一操作,则会得到 HTTP 401 错误: +点击小锁图标,注销后,再执行同样的操作,则会得到 HTTP 401 错误: ```JSON { @@ -246,17 +254,17 @@ UserInDB( } ``` -### 未启用的用户 +### 未激活用户 -现在尝试使用未启用的用户,并通过以下方式进行身份认证: +测试未激活用户,输入以下信息,进行身份验证: 用户名:`alice` 密码:`secret2` -然后尝试执行 `/users/me` 路径的 `GET` 操作。 +然后,执行 `/users/me` 路径的 `GET` 操作。 -你将得到一个「未启用的用户」错误,如: +显示下列**未激活用户**错误信息: ```JSON { @@ -264,12 +272,12 @@ UserInDB( } ``` -## 总结 +## 小结 -现在你掌握了为你的 API 实现一个基于 `username` 和 `password` 的完整安全系统的工具。 +使用本章的工具实现基于 `username` 和 `password` 的完整 API 安全系统。 -使用这些工具,你可以使安全系统与任何数据库以及任何用户或数据模型兼容。 +这些工具让安全系统兼容任何数据库、用户及数据模型。 -唯一缺少的细节是它实际上还并不「安全」。 +唯一欠缺的是,它仍然不是真的**安全**。 -在下一章中,你将看到如何使用一个安全的哈希密码库和 JWT 令牌。 +下一章,介绍使用密码哈希支持库与 JWT 令牌实现真正的安全机制。 diff --git a/docs/zh/docs/tutorial/sql-databases.md b/docs/zh/docs/tutorial/sql-databases.md index 6b354c2b6d..c49374971e 100644 --- a/docs/zh/docs/tutorial/sql-databases.md +++ b/docs/zh/docs/tutorial/sql-databases.md @@ -78,9 +78,23 @@ ORM 具有在代码和数据库表(“*关系型”)中的**对象**之间 现在让我们看看每个文件/模块的作用。 +## 安装 SQLAlchemy + +先下载`SQLAlchemy`所需要的依赖: + +
+ +```console +$ pip install sqlalchemy + +---> 100% +``` + +
+ ## 创建 SQLAlchemy 部件 -让我们涉及到文件`sql_app/database.py`。 +让我们转到文件`sql_app/database.py`。 ### 导入 SQLAlchemy 部件 @@ -246,22 +260,22 @@ connect_args={"check_same_thread": False} 但是为了安全起见,`password`不会出现在其他同类 Pydantic*模型*中,例如用户请求时不应该从 API 返回响应中包含它。 -=== "Python 3.6 及以上版本" +=== "Python 3.10+" - ```Python hl_lines="3 6-8 11-12 23-24 27-28" - {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ```Python hl_lines="1 4-6 9-10 21-22 25-26" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="3 6-8 11-12 23-24 27-28" {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 及以上版本" +=== "Python 3.8+" - ```Python hl_lines="1 4-6 9-10 21-22 25-26" - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` #### SQLAlchemy 风格和 Pydantic 风格 @@ -290,22 +304,22 @@ name: str 不仅是这些项目的 ID,还有我们在 Pydantic*模型*中定义的用于读取项目的所有数据:`Item`. -=== "Python 3.6 及以上版本" +=== "Python 3.10+" - ```Python hl_lines="15-17 31-34" - {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ```Python hl_lines="13-15 29-32" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="15-17 31-34" {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 及以上版本" +=== "Python 3.8+" - ```Python hl_lines="13-15 29-32" - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` !!! tip @@ -319,22 +333,22 @@ name: str 在`Config`类中,设置属性`orm_mode = True`。 -=== "Python 3.6 及以上版本" +=== "Python 3.10+" - ```Python hl_lines="15 19-20 31 36-37" - {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ```Python hl_lines="13 17-18 29 34-35" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="15 19-20 31 36-37" {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 及以上版本" +=== "Python 3.8+" - ```Python hl_lines="13 17-18 29 34-35" - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ```Python hl_lines="15 19-20 31 36-37" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` !!! tip @@ -465,18 +479,18 @@ current_user.items 以非常简单的方式创建数据库表: -=== "Python 3.6 及以上版本" - - ```Python hl_lines="9" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="7" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + #### Alembic 注意 通常你可能会使用 Alembic,来进行格式化数据库(创建表等)。 @@ -485,7 +499,7 @@ current_user.items “迁移”是每当您更改 SQLAlchemy 模型的结构、添加新属性等以在数据库中复制这些更改、添加新列、新表等时所需的一组步骤。 -您可以在[Project Generation - Template](https://fastapi.tiangolo.com/zh/project-generation/)的模板中找到一个 FastAPI 项目中的 Alembic 示例。具体在[`alembic`代码目录中](https://github.com/tiangolo/full-stack-fastapi-postgresql/tree/master/%7B%7Bcookiecutter.project_slug%7D%7D/backend/app/alembic/)。 +您可以在[Project Generation - Template](https://fastapi.tiangolo.com/zh/project-generation/)的模板中找到一个 FastAPI 项目中的 Alembic 示例。具体在[`alembic`代码目录中](https://github.com/tiangolo/full-stack-fastapi-postgresql/tree/master/src/backend/app/alembic/)。 ### 创建依赖项 @@ -499,18 +513,18 @@ current_user.items 我们的依赖项将创建一个新的 SQLAlchemy `SessionLocal`,它将在单个请求中使用,然后在请求完成后关闭它。 -=== "Python 3.6 及以上版本" - - ```Python hl_lines="15-20" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="13-18" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="15-20" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + !!! info 我们将`SessionLocal()`请求的创建和处理放在一个`try`块中。 @@ -524,18 +538,18 @@ current_user.items *这将为我们在路径操作函数*中提供更好的编辑器支持,因为编辑器将知道`db`参数的类型`Session`: -=== "Python 3.6 及以上版本" - - ```Python hl_lines="24 32 38 47 53" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="22 30 36 45 51" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="24 32 38 47 53" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + !!! info "技术细节" 参数`db`实际上是 type `SessionLocal`,但是这个类(用 创建`sessionmaker()`)是 SQLAlchemy 的“代理” `Session`,所以,编辑器并不真正知道提供了哪些方法。 @@ -545,18 +559,18 @@ current_user.items 现在,到了最后,编写标准的**FastAPI** *路径操作*代码。 -=== "Python 3.6 及以上版本" - - ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + 我们在依赖项中的每个请求之前利用`yield`创建数据库会话,然后关闭它。 所以我们就可以在*路径操作函数*中创建需要的依赖,就能直接获取会话。 @@ -638,22 +652,22 @@ def read_user(user_id: int, db: Session = Depends(get_db)): * `sql_app/schemas.py`: -=== "Python 3.6 及以上版本" +=== "Python 3.10+" ```Python - {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 及以上版本" +=== "Python 3.8+" ```Python - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` * `sql_app/crud.py`: @@ -664,18 +678,18 @@ def read_user(user_id: int, db: Session = Depends(get_db)): * `sql_app/main.py`: -=== "Python 3.6 及以上版本" - - ```Python - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.8+" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + ## 执行项目 您可以复制这些代码并按原样使用它。 @@ -723,18 +737,18 @@ $ uvicorn sql_app.main:app --reload 我们将添加中间件(只是一个函数)将为每个请求创建一个新的 SQLAlchemy`SessionLocal`,将其添加到请求中,然后在请求完成后关闭它。 -=== "Python 3.6 及以上版本" - - ```Python hl_lines="14-22" - {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="12-20" {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} ``` +=== "Python 3.8+" + + ```Python hl_lines="14-22" + {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} + ``` + !!! info 我们将`SessionLocal()`请求的创建和处理放在一个`try`块中。 diff --git a/docs/zh/docs/tutorial/static-files.md b/docs/zh/docs/tutorial/static-files.md new file mode 100644 index 0000000000..e7c5c3f0a1 --- /dev/null +++ b/docs/zh/docs/tutorial/static-files.md @@ -0,0 +1,39 @@ +# 静态文件 + +您可以使用 `StaticFiles`从目录中自动提供静态文件。 + +## 使用`StaticFiles` + +* 导入`StaticFiles`。 +* "挂载"(Mount) 一个 `StaticFiles()` 实例到一个指定路径。 + +```Python hl_lines="2 6" +{!../../../docs_src/static_files/tutorial001.py!} +``` + +!!! note "技术细节" + 你也可以用 `from starlette.staticfiles import StaticFiles`。 + + **FastAPI** 提供了和 `starlette.staticfiles` 相同的 `fastapi.staticfiles` ,只是为了方便你,开发者。但它确实来自Starlette。 + +### 什么是"挂载"(Mounting) + +"挂载" 表示在特定路径添加一个完全"独立的"应用,然后负责处理所有子路径。 + +这与使用`APIRouter`不同,因为安装的应用程序是完全独立的。OpenAPI和来自你主应用的文档不会包含已挂载应用的任何东西等等。 + +你可以在**高级用户指南**中了解更多。 + +## 细节 + +这个 "子应用" 会被 "挂载" 到第一个 `"/static"` 指向的子路径。因此,任何以`"/static"`开头的路径都会被它处理。 + + `directory="static"` 指向包含你的静态文件的目录名字。 + +`name="static"` 提供了一个能被**FastAPI**内部使用的名字。 + +所有这些参数可以不同于"`static`",根据你应用的需要和具体细节调整它们。 + +## 更多信息 + +更多细节和选择查阅 Starlette's docs about Static Files. diff --git a/docs/zh/docs/tutorial/testing.md b/docs/zh/docs/tutorial/testing.md new file mode 100644 index 0000000000..77fff75964 --- /dev/null +++ b/docs/zh/docs/tutorial/testing.md @@ -0,0 +1,212 @@ +# 测试 + +感谢 Starlette,测试**FastAPI** 应用轻松又愉快。 + +它基于 HTTPX, 而HTTPX又是基于Requests设计的,所以很相似且易懂。 + +有了它,你可以直接与**FastAPI**一起使用 pytest。 + +## 使用 `TestClient` + +!!! 信息 + 要使用 `TestClient`,先要安装 `httpx`. + + 例:`pip install httpx`. + +导入 `TestClient`. + +通过传入你的**FastAPI**应用创建一个 `TestClient` 。 + +创建名字以 `test_` 开头的函数(这是标准的 `pytest` 约定)。 + +像使用 `httpx` 那样使用 `TestClient` 对象。 + +为你需要检查的地方用标准的Python表达式写个简单的 `assert` 语句(重申,标准的`pytest`)。 + +```Python hl_lines="2 12 15-18" +{!../../../docs_src/app_testing/tutorial001.py!} +``` + +!!! 提示 + 注意测试函数是普通的 `def`,不是 `async def`。 + + 还有client的调用也是普通的调用,不是用 `await`。 + + 这让你可以直接使用 `pytest` 而不会遇到麻烦。 + +!!! note "技术细节" + 你也可以用 `from starlette.testclient import TestClient`。 + + **FastAPI** 提供了和 `starlette.testclient` 一样的 `fastapi.testclient`,只是为了方便开发者。但它直接来自Starlette。 + +!!! 提示 + 除了发送请求之外,如果你还想测试时在FastAPI应用中调用 `async` 函数(例如异步数据库函数), 可以在高级教程中看下 [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} 。 + +## 分离测试 + +在实际应用中,你可能会把你的测试放在另一个文件里。 + +您的**FastAPI**应用程序也可能由一些文件/模块组成等等。 + +### **FastAPI** app 文件 + +假设你有一个像 [更大的应用](./bigger-applications.md){.internal-link target=_blank} 中所描述的文件结构: + +``` +. +├── app +│   ├── __init__.py +│   └── main.py +``` + +在 `main.py` 文件中你有一个 **FastAPI** app: + + +```Python +{!../../../docs_src/app_testing/main.py!} +``` + +### 测试文件 + +然后你会有一个包含测试的文件 `test_main.py` 。app可以像Python包那样存在(一样是目录,但有个 `__init__.py` 文件): + +``` hl_lines="5" +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +因为这文件在同一个包中,所以你可以通过相对导入从 `main` 模块(`main.py`)导入`app`对象: + +```Python hl_lines="3" +{!../../../docs_src/app_testing/test_main.py!} +``` + +...然后测试代码和之前一样的。 + +## 测试:扩展示例 + +现在让我们扩展这个例子,并添加更多细节,看下如何测试不同部分。 + +### 扩展后的 **FastAPI** app 文件 + +让我们继续之前的文件结构: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +假设现在包含**FastAPI** app的文件 `main.py` 有些其他**路径操作**。 + +有个 `GET` 操作会返回错误。 + +有个 `POST` 操作会返回一些错误。 + +所有*路径操作* 都需要一个`X-Token` 头。 + +=== "Python 3.10+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py310/main.py!} + ``` + +=== "Python 3.9+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} + ``` + +=== "Python 3.8+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an/main.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + {!> ../../../docs_src/app_testing/app_b_py310/main.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + +### 扩展后的测试文件 + +然后您可以使用扩展后的测试更新`test_main.py`: + +```Python +{!> ../../../docs_src/app_testing/app_b/test_main.py!} +``` + +每当你需要客户端在请求中传递信息,但你不知道如何传递时,你可以通过搜索(谷歌)如何用 `httpx`做,或者是用 `requests` 做,毕竟HTTPX的设计是基于Requests的设计的。 + +接着只需在测试中同样操作。 + +示例: + +* 传一个*路径* 或*查询* 参数,添加到URL上。 +* 传一个JSON体,传一个Python对象(例如一个`dict`)到参数 `json`。 +* 如果你需要发送 *Form Data* 而不是 JSON,使用 `data` 参数。 +* 要发送 *headers*,传 `dict` 给 `headers` 参数。 +* 对于 *cookies*,传 `dict` 给 `cookies` 参数。 + +关于如何传数据给后端的更多信息 (使用`httpx` 或 `TestClient`),请查阅 HTTPX 文档. + +!!! 信息 + 注意 `TestClient` 接收可以被转化为JSON的数据,而不是Pydantic模型。 + + 如果你在测试中有一个Pydantic模型,并且你想在测试时发送它的数据给应用,你可以使用在[JSON Compatible Encoder](encoder.md){.internal-link target=_blank}介绍的`jsonable_encoder` 。 + +## 运行起来 + +之后,你只需要安装 `pytest`: + +
+ +```console +$ pip install pytest + +---> 100% +``` + +
+ +他会自动检测文件和测试,执行测试,然后向你报告结果。 + +执行测试: + +
+ +```console +$ pytest + +================ test session starts ================ +platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 +rootdir: /home/user/code/superawesome-cli/app +plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 +collected 6 items + +---> 100% + +test_main.py ...... [100%] + +================= 1 passed in 0.03s ================= +``` + +
diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index f4c3c0ec16..de18856f44 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -1,202 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/zh/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode - features: - - search.suggest - - search.highlight - - content.tabs.link - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: zh -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - de: /de/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- 教程 - 用户指南: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/query-params-str-validations.md - - tutorial/path-params-numeric-validations.md - - tutorial/body-multiple-params.md - - tutorial/body-fields.md - - tutorial/middleware.md - - tutorial/body-nested-models.md - - tutorial/header-params.md - - tutorial/response-model.md - - tutorial/extra-models.md - - tutorial/response-status-code.md - - tutorial/schema-extra-example.md - - tutorial/extra-data-types.md - - tutorial/cookie-params.md - - tutorial/request-forms.md - - tutorial/request-files.md - - tutorial/request-forms-and-files.md - - tutorial/handling-errors.md - - tutorial/path-operation-configuration.md - - tutorial/encoder.md - - tutorial/body-updates.md - - 依赖项: - - tutorial/dependencies/index.md - - tutorial/dependencies/classes-as-dependencies.md - - tutorial/dependencies/sub-dependencies.md - - tutorial/dependencies/dependencies-in-path-operation-decorators.md - - tutorial/dependencies/global-dependencies.md - - 安全性: - - tutorial/security/index.md - - tutorial/security/first-steps.md - - tutorial/security/get-current-user.md - - tutorial/security/simple-oauth2.md - - tutorial/security/oauth2-jwt.md - - tutorial/cors.md - - tutorial/sql-databases.md - - tutorial/bigger-applications.md - - tutorial/metadata.md - - tutorial/debugging.md -- 高级用户指南: - - advanced/index.md - - advanced/path-operation-advanced-configuration.md - - advanced/additional-status-codes.md - - advanced/response-directly.md - - advanced/custom-response.md - - advanced/response-cookies.md - - advanced/wsgi.md -- contributing.md -- help-fastapi.md -- benchmarks.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: UA-133183413-1 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - 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 - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /de/ - name: de - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js +INHERIT: ../en/mkdocs.yml diff --git a/docs_src/additional_status_codes/tutorial001_an.py b/docs_src/additional_status_codes/tutorial001_an.py new file mode 100644 index 0000000000..b5ad6a16b6 --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse +from typing_extensions import Annotated + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: Annotated[Union[str, None], Body()] = None, + size: Annotated[Union[int, None], Body()] = None, +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001_an_py310.py b/docs_src/additional_status_codes/tutorial001_an_py310.py new file mode 100644 index 0000000000..1865074a11 --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: Annotated[str | None, Body()] = None, + size: Annotated[int | None, Body()] = None, +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001_an_py39.py b/docs_src/additional_status_codes/tutorial001_an_py39.py new file mode 100644 index 0000000000..89653dd8a0 --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: Annotated[Union[str, None], Body()] = None, + size: Annotated[Union[int, None], Body()] = None, +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001_py310.py b/docs_src/additional_status_codes/tutorial001_py310.py new file mode 100644 index 0000000000..38bfa455b8 --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_py310.py @@ -0,0 +1,23 @@ +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: str | None = Body(default=None), + size: int | None = Body(default=None), +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/app_testing/app_b/main.py b/docs_src/app_testing/app_b/main.py index 11558b8e81..45a1033783 100644 --- a/docs_src/app_testing/app_b/main.py +++ b/docs_src/app_testing/app_b/main.py @@ -33,6 +33,6 @@ async def create_item(item: Item, x_token: str = Header()): if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: - raise HTTPException(status_code=400, detail="Item already exists") + raise HTTPException(status_code=409, detail="Item already exists") fake_db[item.id] = item return item diff --git a/docs_src/app_testing/app_b/test_main.py b/docs_src/app_testing/app_b/test_main.py index d186b8ecba..4e2b98e237 100644 --- a/docs_src/app_testing/app_b/test_main.py +++ b/docs_src/app_testing/app_b/test_main.py @@ -61,5 +61,5 @@ def test_create_existing_item(): "description": "There goes my stealer", }, ) - assert response.status_code == 400 + assert response.status_code == 409 assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/sql_databases_peewee/__init__.py b/docs_src/app_testing/app_b_an/__init__.py similarity index 100% rename from docs_src/sql_databases_peewee/__init__.py rename to docs_src/app_testing/app_b_an/__init__.py diff --git a/docs_src/app_testing/app_b_an/main.py b/docs_src/app_testing/app_b_an/main.py new file mode 100644 index 0000000000..c63134fc9b --- /dev/null +++ b/docs_src/app_testing/app_b_an/main.py @@ -0,0 +1,39 @@ +from typing import Union + +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel +from typing_extensions import Annotated + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: Union[str, None] = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_an/test_main.py b/docs_src/app_testing/app_b_an/test_main.py new file mode 100644 index 0000000000..d186b8ecba --- /dev/null +++ b/docs_src/app_testing/app_b_an/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/tests/test_tutorial/test_sql_databases_peewee/__init__.py b/docs_src/app_testing/app_b_an_py310/__init__.py similarity index 100% rename from tests/test_tutorial/test_sql_databases_peewee/__init__.py rename to docs_src/app_testing/app_b_an_py310/__init__.py diff --git a/docs_src/app_testing/app_b_an_py310/main.py b/docs_src/app_testing/app_b_an_py310/main.py new file mode 100644 index 0000000000..48c27a0b88 --- /dev/null +++ b/docs_src/app_testing/app_b_an_py310/main.py @@ -0,0 +1,38 @@ +from typing import Annotated + +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: str | None = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_an_py310/test_main.py b/docs_src/app_testing/app_b_an_py310/test_main.py new file mode 100644 index 0000000000..d186b8ecba --- /dev/null +++ b/docs_src/app_testing/app_b_an_py310/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs/de/overrides/.gitignore b/docs_src/app_testing/app_b_an_py39/__init__.py similarity index 100% rename from docs/de/overrides/.gitignore rename to docs_src/app_testing/app_b_an_py39/__init__.py diff --git a/docs_src/app_testing/app_b_an_py39/main.py b/docs_src/app_testing/app_b_an_py39/main.py new file mode 100644 index 0000000000..935a510b74 --- /dev/null +++ b/docs_src/app_testing/app_b_an_py39/main.py @@ -0,0 +1,38 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: Union[str, None] = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_an_py39/test_main.py b/docs_src/app_testing/app_b_an_py39/test_main.py new file mode 100644 index 0000000000..d186b8ecba --- /dev/null +++ b/docs_src/app_testing/app_b_an_py39/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/app_testing/app_b_py310/main.py b/docs_src/app_testing/app_b_py310/main.py index b4c72de5c9..eccedcc7ce 100644 --- a/docs_src/app_testing/app_b_py310/main.py +++ b/docs_src/app_testing/app_b_py310/main.py @@ -31,6 +31,6 @@ async def create_item(item: Item, x_token: str = Header()): if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: - raise HTTPException(status_code=400, detail="Item already exists") + raise HTTPException(status_code=409, detail="Item already exists") fake_db[item.id] = item return item diff --git a/docs_src/app_testing/app_b_py310/test_main.py b/docs_src/app_testing/app_b_py310/test_main.py index d186b8ecba..4e2b98e237 100644 --- a/docs_src/app_testing/app_b_py310/test_main.py +++ b/docs_src/app_testing/app_b_py310/test_main.py @@ -61,5 +61,5 @@ def test_create_existing_item(): "description": "There goes my stealer", }, ) - assert response.status_code == 400 + assert response.status_code == 409 assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/app_testing/tutorial002.py b/docs_src/app_testing/tutorial002.py index b4a9c0586d..71c898b3cf 100644 --- a/docs_src/app_testing/tutorial002.py +++ b/docs_src/app_testing/tutorial002.py @@ -10,7 +10,7 @@ async def read_main(): return {"msg": "Hello World"} -@app.websocket_route("/ws") +@app.websocket("/ws") async def websocket(websocket: WebSocket): await websocket.accept() await websocket.send_json({"msg": "Hello WebSocket"}) diff --git a/docs_src/background_tasks/tutorial002_an.py b/docs_src/background_tasks/tutorial002_an.py new file mode 100644 index 0000000000..f63502b09a --- /dev/null +++ b/docs_src/background_tasks/tutorial002_an.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import BackgroundTasks, Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: Union[str, None] = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/background_tasks/tutorial002_an_py310.py b/docs_src/background_tasks/tutorial002_an_py310.py new file mode 100644 index 0000000000..1fc78fbc95 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_an_py310.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import BackgroundTasks, Depends, FastAPI + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: str | None = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/background_tasks/tutorial002_an_py39.py b/docs_src/background_tasks/tutorial002_an_py39.py new file mode 100644 index 0000000000..bfdd148753 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_an_py39.py @@ -0,0 +1,26 @@ +from typing import Annotated, Union + +from fastapi import BackgroundTasks, Depends, FastAPI + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: Union[str, None] = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs/es/overrides/.gitignore b/docs_src/bigger_applications/app_an/__init__.py similarity index 100% rename from docs/es/overrides/.gitignore rename to docs_src/bigger_applications/app_an/__init__.py diff --git a/docs_src/bigger_applications/app_an/dependencies.py b/docs_src/bigger_applications/app_an/dependencies.py new file mode 100644 index 0000000000..1374c54b31 --- /dev/null +++ b/docs_src/bigger_applications/app_an/dependencies.py @@ -0,0 +1,12 @@ +from fastapi import Header, HTTPException +from typing_extensions import Annotated + + +async def get_token_header(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def get_query_token(token: str): + if token != "jessica": + raise HTTPException(status_code=400, detail="No Jessica token provided") diff --git a/docs/fa/overrides/.gitignore b/docs_src/bigger_applications/app_an/internal/__init__.py similarity index 100% rename from docs/fa/overrides/.gitignore rename to docs_src/bigger_applications/app_an/internal/__init__.py diff --git a/docs_src/bigger_applications/app_an/internal/admin.py b/docs_src/bigger_applications/app_an/internal/admin.py new file mode 100644 index 0000000000..99d3da86b9 --- /dev/null +++ b/docs_src/bigger_applications/app_an/internal/admin.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.post("/") +async def update_admin(): + return {"message": "Admin getting schwifty"} diff --git a/docs_src/bigger_applications/app_an/main.py b/docs_src/bigger_applications/app_an/main.py new file mode 100644 index 0000000000..ae544a3aac --- /dev/null +++ b/docs_src/bigger_applications/app_an/main.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +from .dependencies import get_query_token, get_token_header +from .internal import admin +from .routers import items, users + +app = FastAPI(dependencies=[Depends(get_query_token)]) + + +app.include_router(users.router) +app.include_router(items.router) +app.include_router( + admin.router, + prefix="/admin", + tags=["admin"], + dependencies=[Depends(get_token_header)], + responses={418: {"description": "I'm a teapot"}}, +) + + +@app.get("/") +async def root(): + return {"message": "Hello Bigger Applications!"} diff --git a/docs/fr/overrides/.gitignore b/docs_src/bigger_applications/app_an/routers/__init__.py similarity index 100% rename from docs/fr/overrides/.gitignore rename to docs_src/bigger_applications/app_an/routers/__init__.py diff --git a/docs_src/bigger_applications/app_an/routers/items.py b/docs_src/bigger_applications/app_an/routers/items.py new file mode 100644 index 0000000000..bde9ff4d55 --- /dev/null +++ b/docs_src/bigger_applications/app_an/routers/items.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, Depends, HTTPException + +from ..dependencies import get_token_header + +router = APIRouter( + prefix="/items", + tags=["items"], + dependencies=[Depends(get_token_header)], + responses={404: {"description": "Not found"}}, +) + + +fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}} + + +@router.get("/") +async def read_items(): + return fake_items_db + + +@router.get("/{item_id}") +async def read_item(item_id: str): + if item_id not in fake_items_db: + raise HTTPException(status_code=404, detail="Item not found") + return {"name": fake_items_db[item_id]["name"], "item_id": item_id} + + +@router.put( + "/{item_id}", + tags=["custom"], + responses={403: {"description": "Operation forbidden"}}, +) +async def update_item(item_id: str): + if item_id != "plumbus": + raise HTTPException( + status_code=403, detail="You can only update the item: plumbus" + ) + return {"item_id": item_id, "name": "The great Plumbus"} diff --git a/docs_src/bigger_applications/app_an/routers/users.py b/docs_src/bigger_applications/app_an/routers/users.py new file mode 100644 index 0000000000..39b3d7e7cf --- /dev/null +++ b/docs_src/bigger_applications/app_an/routers/users.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/users/", tags=["users"]) +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] + + +@router.get("/users/me", tags=["users"]) +async def read_user_me(): + return {"username": "fakecurrentuser"} + + +@router.get("/users/{username}", tags=["users"]) +async def read_user(username: str): + return {"username": username} diff --git a/docs/he/overrides/.gitignore b/docs_src/bigger_applications/app_an_py39/__init__.py similarity index 100% rename from docs/he/overrides/.gitignore rename to docs_src/bigger_applications/app_an_py39/__init__.py diff --git a/docs_src/bigger_applications/app_an_py39/dependencies.py b/docs_src/bigger_applications/app_an_py39/dependencies.py new file mode 100644 index 0000000000..5c7612aa09 --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/dependencies.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Header, HTTPException + + +async def get_token_header(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def get_query_token(token: str): + if token != "jessica": + raise HTTPException(status_code=400, detail="No Jessica token provided") diff --git a/docs/id/overrides/.gitignore b/docs_src/bigger_applications/app_an_py39/internal/__init__.py similarity index 100% rename from docs/id/overrides/.gitignore rename to docs_src/bigger_applications/app_an_py39/internal/__init__.py diff --git a/docs_src/bigger_applications/app_an_py39/internal/admin.py b/docs_src/bigger_applications/app_an_py39/internal/admin.py new file mode 100644 index 0000000000..99d3da86b9 --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/internal/admin.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.post("/") +async def update_admin(): + return {"message": "Admin getting schwifty"} diff --git a/docs_src/bigger_applications/app_an_py39/main.py b/docs_src/bigger_applications/app_an_py39/main.py new file mode 100644 index 0000000000..ae544a3aac --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/main.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +from .dependencies import get_query_token, get_token_header +from .internal import admin +from .routers import items, users + +app = FastAPI(dependencies=[Depends(get_query_token)]) + + +app.include_router(users.router) +app.include_router(items.router) +app.include_router( + admin.router, + prefix="/admin", + tags=["admin"], + dependencies=[Depends(get_token_header)], + responses={418: {"description": "I'm a teapot"}}, +) + + +@app.get("/") +async def root(): + return {"message": "Hello Bigger Applications!"} diff --git a/docs/it/overrides/.gitignore b/docs_src/bigger_applications/app_an_py39/routers/__init__.py similarity index 100% rename from docs/it/overrides/.gitignore rename to docs_src/bigger_applications/app_an_py39/routers/__init__.py diff --git a/docs_src/bigger_applications/app_an_py39/routers/items.py b/docs_src/bigger_applications/app_an_py39/routers/items.py new file mode 100644 index 0000000000..bde9ff4d55 --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/routers/items.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, Depends, HTTPException + +from ..dependencies import get_token_header + +router = APIRouter( + prefix="/items", + tags=["items"], + dependencies=[Depends(get_token_header)], + responses={404: {"description": "Not found"}}, +) + + +fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}} + + +@router.get("/") +async def read_items(): + return fake_items_db + + +@router.get("/{item_id}") +async def read_item(item_id: str): + if item_id not in fake_items_db: + raise HTTPException(status_code=404, detail="Item not found") + return {"name": fake_items_db[item_id]["name"], "item_id": item_id} + + +@router.put( + "/{item_id}", + tags=["custom"], + responses={403: {"description": "Operation forbidden"}}, +) +async def update_item(item_id: str): + if item_id != "plumbus": + raise HTTPException( + status_code=403, detail="You can only update the item: plumbus" + ) + return {"item_id": item_id, "name": "The great Plumbus"} diff --git a/docs_src/bigger_applications/app_an_py39/routers/users.py b/docs_src/bigger_applications/app_an_py39/routers/users.py new file mode 100644 index 0000000000..39b3d7e7cf --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/routers/users.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/users/", tags=["users"]) +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] + + +@router.get("/users/me", tags=["users"]) +async def read_user_me(): + return {"username": "fakecurrentuser"} + + +@router.get("/users/{username}", tags=["users"]) +async def read_user(username: str): + return {"username": username} diff --git a/docs_src/body/tutorial003.py b/docs_src/body/tutorial003.py index 89a6b833ce..2f33cc0389 100644 --- a/docs_src/body/tutorial003.py +++ b/docs_src/body/tutorial003.py @@ -15,5 +15,5 @@ app = FastAPI() @app.put("/items/{item_id}") -async def create_item(item_id: int, item: Item): +async def update_item(item_id: int, item: Item): return {"item_id": item_id, **item.dict()} diff --git a/docs_src/body/tutorial003_py310.py b/docs_src/body/tutorial003_py310.py index a936f28fdc..440b210e6b 100644 --- a/docs_src/body/tutorial003_py310.py +++ b/docs_src/body/tutorial003_py310.py @@ -13,5 +13,5 @@ app = FastAPI() @app.put("/items/{item_id}") -async def create_item(item_id: int, item: Item): +async def update_item(item_id: int, item: Item): return {"item_id": item_id, **item.dict()} diff --git a/docs_src/body/tutorial004.py b/docs_src/body/tutorial004.py index e2df0df2ba..0671e0a278 100644 --- a/docs_src/body/tutorial004.py +++ b/docs_src/body/tutorial004.py @@ -15,7 +15,7 @@ app = FastAPI() @app.put("/items/{item_id}") -async def create_item(item_id: int, item: Item, q: Union[str, None] = None): +async def update_item(item_id: int, item: Item, q: Union[str, None] = None): result = {"item_id": item_id, **item.dict()} if q: result.update({"q": q}) diff --git a/docs_src/body/tutorial004_py310.py b/docs_src/body/tutorial004_py310.py index 60cfd96109..b352b70ab6 100644 --- a/docs_src/body/tutorial004_py310.py +++ b/docs_src/body/tutorial004_py310.py @@ -13,7 +13,7 @@ app = FastAPI() @app.put("/items/{item_id}") -async def create_item(item_id: int, item: Item, q: str | None = None): +async def update_item(item_id: int, item: Item, q: str | None = None): result = {"item_id": item_id, **item.dict()} if q: result.update({"q": q}) diff --git a/docs_src/body_fields/tutorial001_an.py b/docs_src/body_fields/tutorial001_an.py new file mode 100644 index 0000000000..15ea1b53dc --- /dev/null +++ b/docs_src/body_fields/tutorial001_an.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = Field( + default=None, title="The description of the item", max_length=300 + ) + price: float = Field(gt=0, description="The price must be greater than zero") + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_fields/tutorial001_an_py310.py b/docs_src/body_fields/tutorial001_an_py310.py new file mode 100644 index 0000000000..c9d99e1c2b --- /dev/null +++ b/docs_src/body_fields/tutorial001_an_py310.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = Field( + default=None, title="The description of the item", max_length=300 + ) + price: float = Field(gt=0, description="The price must be greater than zero") + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_fields/tutorial001_an_py39.py b/docs_src/body_fields/tutorial001_an_py39.py new file mode 100644 index 0000000000..6ef14470cd --- /dev/null +++ b/docs_src/body_fields/tutorial001_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = Field( + default=None, title="The description of the item", max_length=300 + ) + price: float = Field(gt=0, description="The price must be greater than zero") + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial001_an.py b/docs_src/body_multiple_params/tutorial001_an.py new file mode 100644 index 0000000000..308eee8544 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_an.py @@ -0,0 +1,28 @@ +from typing import Union + +from fastapi import FastAPI, Path +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: Union[str, None] = None, + item: Union[Item, None] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial001_an_py310.py b/docs_src/body_multiple_params/tutorial001_an_py310.py new file mode 100644 index 0000000000..2d79c19c28 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_an_py310.py @@ -0,0 +1,27 @@ +from typing import Annotated + +from fastapi import FastAPI, Path +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: str | None = None, + item: Item | None = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial001_an_py39.py b/docs_src/body_multiple_params/tutorial001_an_py39.py new file mode 100644 index 0000000000..1c0ac3a7b3 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_an_py39.py @@ -0,0 +1,27 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Path +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: Union[str, None] = None, + item: Union[Item, None] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial003_an.py b/docs_src/body_multiple_params/tutorial003_an.py new file mode 100644 index 0000000000..39ef7340a5 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_an.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: Annotated[int, Body()] +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial003_an_py310.py b/docs_src/body_multiple_params/tutorial003_an_py310.py new file mode 100644 index 0000000000..593ff893ca --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_an_py310.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: Annotated[int, Body()] +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial003_an_py39.py b/docs_src/body_multiple_params/tutorial003_an_py39.py new file mode 100644 index 0000000000..042351e0b7 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_an_py39.py @@ -0,0 +1,26 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: Annotated[int, Body()] +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial004.py b/docs_src/body_multiple_params/tutorial004.py index beea7d1e38..8ce4c7a976 100644 --- a/docs_src/body_multiple_params/tutorial004.py +++ b/docs_src/body_multiple_params/tutorial004.py @@ -25,7 +25,7 @@ async def update_item( item: Item, user: User, importance: int = Body(gt=0), - q: Union[str, None] = None + q: Union[str, None] = None, ): results = {"item_id": item_id, "item": item, "user": user, "importance": importance} if q: diff --git a/docs_src/body_multiple_params/tutorial004_an.py b/docs_src/body_multiple_params/tutorial004_an.py new file mode 100644 index 0000000000..f6830f3920 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_an.py @@ -0,0 +1,34 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: Annotated[int, Body(gt=0)], + q: Union[str, None] = None, +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial004_an_py310.py b/docs_src/body_multiple_params/tutorial004_an_py310.py new file mode 100644 index 0000000000..cd5c66fefb --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_an_py310.py @@ -0,0 +1,33 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: Annotated[int, Body(gt=0)], + q: str | None = None, +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial004_an_py39.py b/docs_src/body_multiple_params/tutorial004_an_py39.py new file mode 100644 index 0000000000..567427c03e --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_an_py39.py @@ -0,0 +1,33 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: Annotated[int, Body(gt=0)], + q: Union[str, None] = None, +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial004_py310.py b/docs_src/body_multiple_params/tutorial004_py310.py index 6d495d4082..14acbc3b4b 100644 --- a/docs_src/body_multiple_params/tutorial004_py310.py +++ b/docs_src/body_multiple_params/tutorial004_py310.py @@ -23,7 +23,7 @@ async def update_item( item: Item, user: User, importance: int = Body(gt=0), - q: str | None = None + q: str | None = None, ): results = {"item_id": item_id, "item": item, "user": user, "importance": importance} if q: diff --git a/docs_src/body_multiple_params/tutorial005_an.py b/docs_src/body_multiple_params/tutorial005_an.py new file mode 100644 index 0000000000..dadde80b55 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_an.py @@ -0,0 +1,20 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial005_an_py310.py b/docs_src/body_multiple_params/tutorial005_an_py310.py new file mode 100644 index 0000000000..f97680ba01 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial005_an_py39.py b/docs_src/body_multiple_params/tutorial005_an_py39.py new file mode 100644 index 0000000000..9a52425c16 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_an_py39.py @@ -0,0 +1,19 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/conditional_openapi/tutorial001.py b/docs_src/conditional_openapi/tutorial001.py index 717e723e83..eedb0d2742 100644 --- a/docs_src/conditional_openapi/tutorial001.py +++ b/docs_src/conditional_openapi/tutorial001.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/extending_openapi/tutorial003.py b/docs_src/configure_swagger_ui/tutorial001.py similarity index 100% rename from docs_src/extending_openapi/tutorial003.py rename to docs_src/configure_swagger_ui/tutorial001.py diff --git a/docs_src/extending_openapi/tutorial004.py b/docs_src/configure_swagger_ui/tutorial002.py similarity index 100% rename from docs_src/extending_openapi/tutorial004.py rename to docs_src/configure_swagger_ui/tutorial002.py diff --git a/docs_src/extending_openapi/tutorial005.py b/docs_src/configure_swagger_ui/tutorial003.py similarity index 100% rename from docs_src/extending_openapi/tutorial005.py rename to docs_src/configure_swagger_ui/tutorial003.py diff --git a/docs_src/cookie_params/tutorial001_an.py b/docs_src/cookie_params/tutorial001_an.py new file mode 100644 index 0000000000..6d5931229c --- /dev/null +++ b/docs_src/cookie_params/tutorial001_an.py @@ -0,0 +1,11 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: Annotated[Union[str, None], Cookie()] = None): + return {"ads_id": ads_id} diff --git a/docs_src/cookie_params/tutorial001_an_py310.py b/docs_src/cookie_params/tutorial001_an_py310.py new file mode 100644 index 0000000000..69ac683fed --- /dev/null +++ b/docs_src/cookie_params/tutorial001_an_py310.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import Cookie, FastAPI + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: Annotated[str | None, Cookie()] = None): + return {"ads_id": ads_id} diff --git a/docs_src/cookie_params/tutorial001_an_py39.py b/docs_src/cookie_params/tutorial001_an_py39.py new file mode 100644 index 0000000000..e18d0a332e --- /dev/null +++ b/docs_src/cookie_params/tutorial001_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated, Union + +from fastapi import Cookie, FastAPI + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: Annotated[Union[str, None], Cookie()] = None): + return {"ads_id": ads_id} diff --git a/docs_src/custom_docs_ui/tutorial001.py b/docs_src/custom_docs_ui/tutorial001.py new file mode 100644 index 0000000000..4384433e37 --- /dev/null +++ b/docs_src/custom_docs_ui/tutorial001.py @@ -0,0 +1,38 @@ +from fastapi import FastAPI +from fastapi.openapi.docs import ( + get_redoc_html, + get_swagger_ui_html, + get_swagger_ui_oauth2_redirect_html, +) + +app = FastAPI(docs_url=None, redoc_url=None) + + +@app.get("/docs", include_in_schema=False) +async def custom_swagger_ui_html(): + return get_swagger_ui_html( + openapi_url=app.openapi_url, + title=app.title + " - Swagger UI", + oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, + swagger_js_url="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js", + swagger_css_url="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css", + ) + + +@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False) +async def swagger_ui_redirect(): + return get_swagger_ui_oauth2_redirect_html() + + +@app.get("/redoc", include_in_schema=False) +async def redoc_html(): + return get_redoc_html( + openapi_url=app.openapi_url, + title=app.title + " - ReDoc", + redoc_js_url="https://unpkg.com/redoc@next/bundles/redoc.standalone.js", + ) + + +@app.get("/users/{username}") +async def read_user(username: str): + return {"message": f"Hello {username}"} diff --git a/docs_src/extending_openapi/tutorial002.py b/docs_src/custom_docs_ui/tutorial002.py similarity index 100% rename from docs_src/extending_openapi/tutorial002.py rename to docs_src/custom_docs_ui/tutorial002.py diff --git a/docs_src/dependencies/tutorial001_02_an.py b/docs_src/dependencies/tutorial001_02_an.py new file mode 100644 index 0000000000..455d60c822 --- /dev/null +++ b/docs_src/dependencies/tutorial001_02_an.py @@ -0,0 +1,25 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +CommonsDep = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonsDep): + return commons + + +@app.get("/users/") +async def read_users(commons: CommonsDep): + return commons diff --git a/docs_src/dependencies/tutorial001_02_an_py310.py b/docs_src/dependencies/tutorial001_02_an_py310.py new file mode 100644 index 0000000000..f59637bb23 --- /dev/null +++ b/docs_src/dependencies/tutorial001_02_an_py310.py @@ -0,0 +1,22 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +CommonsDep = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonsDep): + return commons + + +@app.get("/users/") +async def read_users(commons: CommonsDep): + return commons diff --git a/docs_src/dependencies/tutorial001_02_an_py39.py b/docs_src/dependencies/tutorial001_02_an_py39.py new file mode 100644 index 0000000000..df969ae9c8 --- /dev/null +++ b/docs_src/dependencies/tutorial001_02_an_py39.py @@ -0,0 +1,24 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +CommonsDep = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonsDep): + return commons + + +@app.get("/users/") +async def read_users(commons: CommonsDep): + return commons diff --git a/docs_src/dependencies/tutorial001_an.py b/docs_src/dependencies/tutorial001_an.py new file mode 100644 index 0000000000..81e24fe86c --- /dev/null +++ b/docs_src/dependencies/tutorial001_an.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return commons diff --git a/docs_src/dependencies/tutorial001_an_py310.py b/docs_src/dependencies/tutorial001_an_py310.py new file mode 100644 index 0000000000..f2e57ae7b8 --- /dev/null +++ b/docs_src/dependencies/tutorial001_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return commons diff --git a/docs_src/dependencies/tutorial001_an_py39.py b/docs_src/dependencies/tutorial001_an_py39.py new file mode 100644 index 0000000000..5d9fe6ddfd --- /dev/null +++ b/docs_src/dependencies/tutorial001_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return commons diff --git a/docs_src/dependencies/tutorial002_an.py b/docs_src/dependencies/tutorial002_an.py new file mode 100644 index 0000000000..964ccf66ca --- /dev/null +++ b/docs_src/dependencies/tutorial002_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial002_an_py310.py b/docs_src/dependencies/tutorial002_an_py310.py new file mode 100644 index 0000000000..0777263125 --- /dev/null +++ b/docs_src/dependencies/tutorial002_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial002_an_py39.py b/docs_src/dependencies/tutorial002_an_py39.py new file mode 100644 index 0000000000..844a23c5a7 --- /dev/null +++ b/docs_src/dependencies/tutorial002_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_an.py b/docs_src/dependencies/tutorial003_an.py new file mode 100644 index 0000000000..ba8e9f7174 --- /dev/null +++ b/docs_src/dependencies/tutorial003_an.py @@ -0,0 +1,26 @@ +from typing import Any, Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_an_py310.py b/docs_src/dependencies/tutorial003_an_py310.py new file mode 100644 index 0000000000..44116989b7 --- /dev/null +++ b/docs_src/dependencies/tutorial003_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated, Any + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_an_py39.py b/docs_src/dependencies/tutorial003_an_py39.py new file mode 100644 index 0000000000..9e9123ad2f --- /dev/null +++ b/docs_src/dependencies/tutorial003_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Any, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_an.py b/docs_src/dependencies/tutorial004_an.py new file mode 100644 index 0000000000..78881a354c --- /dev/null +++ b/docs_src/dependencies/tutorial004_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends()]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_an_py310.py b/docs_src/dependencies/tutorial004_an_py310.py new file mode 100644 index 0000000000..2c009efccd --- /dev/null +++ b/docs_src/dependencies/tutorial004_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends()]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_an_py39.py b/docs_src/dependencies/tutorial004_an_py39.py new file mode 100644 index 0000000000..74268626b4 --- /dev/null +++ b/docs_src/dependencies/tutorial004_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends()]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial005_an.py b/docs_src/dependencies/tutorial005_an.py new file mode 100644 index 0000000000..6785099dab --- /dev/null +++ b/docs_src/dependencies/tutorial005_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Cookie, Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +def query_extractor(q: Union[str, None] = None): + return q + + +def query_or_cookie_extractor( + q: Annotated[str, Depends(query_extractor)], + last_query: Annotated[Union[str, None], Cookie()] = None, +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query( + query_or_default: Annotated[str, Depends(query_or_cookie_extractor)] +): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial005_an_py310.py b/docs_src/dependencies/tutorial005_an_py310.py new file mode 100644 index 0000000000..6c0aa0b368 --- /dev/null +++ b/docs_src/dependencies/tutorial005_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Cookie, Depends, FastAPI + +app = FastAPI() + + +def query_extractor(q: str | None = None): + return q + + +def query_or_cookie_extractor( + q: Annotated[str, Depends(query_extractor)], + last_query: Annotated[str | None, Cookie()] = None, +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query( + query_or_default: Annotated[str, Depends(query_or_cookie_extractor)] +): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial005_an_py39.py b/docs_src/dependencies/tutorial005_an_py39.py new file mode 100644 index 0000000000..e8887e1621 --- /dev/null +++ b/docs_src/dependencies/tutorial005_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Cookie, Depends, FastAPI + +app = FastAPI() + + +def query_extractor(q: Union[str, None] = None): + return q + + +def query_or_cookie_extractor( + q: Annotated[str, Depends(query_extractor)], + last_query: Annotated[Union[str, None], Cookie()] = None, +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query( + query_or_default: Annotated[str, Depends(query_or_cookie_extractor)] +): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial006_an.py b/docs_src/dependencies/tutorial006_an.py new file mode 100644 index 0000000000..5aaea04d16 --- /dev/null +++ b/docs_src/dependencies/tutorial006_an.py @@ -0,0 +1,20 @@ +from fastapi import Depends, FastAPI, Header, HTTPException +from typing_extensions import Annotated + +app = FastAPI() + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)]) +async def read_items(): + return [{"item": "Foo"}, {"item": "Bar"}] diff --git a/docs_src/dependencies/tutorial006_an_py39.py b/docs_src/dependencies/tutorial006_an_py39.py new file mode 100644 index 0000000000..11976ed6a2 --- /dev/null +++ b/docs_src/dependencies/tutorial006_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI, Header, HTTPException + +app = FastAPI() + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)]) +async def read_items(): + return [{"item": "Foo"}, {"item": "Bar"}] diff --git a/docs_src/dependencies/tutorial008_an.py b/docs_src/dependencies/tutorial008_an.py new file mode 100644 index 0000000000..2de86f042d --- /dev/null +++ b/docs_src/dependencies/tutorial008_an.py @@ -0,0 +1,26 @@ +from fastapi import Depends +from typing_extensions import Annotated + + +async def dependency_a(): + dep_a = generate_dep_a() + try: + yield dep_a + finally: + dep_a.close() + + +async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]): + dep_b = generate_dep_b() + try: + yield dep_b + finally: + dep_b.close(dep_a) + + +async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]): + dep_c = generate_dep_c() + try: + yield dep_c + finally: + dep_c.close(dep_b) diff --git a/docs_src/dependencies/tutorial008_an_py39.py b/docs_src/dependencies/tutorial008_an_py39.py new file mode 100644 index 0000000000..acc804c320 --- /dev/null +++ b/docs_src/dependencies/tutorial008_an_py39.py @@ -0,0 +1,27 @@ +from typing import Annotated + +from fastapi import Depends + + +async def dependency_a(): + dep_a = generate_dep_a() + try: + yield dep_a + finally: + dep_a.close() + + +async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]): + dep_b = generate_dep_b() + try: + yield dep_b + finally: + dep_b.close(dep_a) + + +async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]): + dep_c = generate_dep_c() + try: + yield dep_c + finally: + dep_c.close(dep_b) diff --git a/docs_src/dependencies/tutorial008b.py b/docs_src/dependencies/tutorial008b.py new file mode 100644 index 0000000000..163e96600f --- /dev/null +++ b/docs_src/dependencies/tutorial008b.py @@ -0,0 +1,30 @@ +from fastapi import Depends, FastAPI, HTTPException + +app = FastAPI() + + +data = { + "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"}, + "portal-gun": {"description": "Gun to create portals", "owner": "Rick"}, +} + + +class OwnerError(Exception): + pass + + +def get_username(): + try: + yield "Rick" + except OwnerError as e: + raise HTTPException(status_code=400, detail=f"Owner error: {e}") + + +@app.get("/items/{item_id}") +def get_item(item_id: str, username: str = Depends(get_username)): + if item_id not in data: + raise HTTPException(status_code=404, detail="Item not found") + item = data[item_id] + if item["owner"] != username: + raise OwnerError(username) + return item diff --git a/docs_src/dependencies/tutorial008b_an.py b/docs_src/dependencies/tutorial008b_an.py new file mode 100644 index 0000000000..84d8f12c14 --- /dev/null +++ b/docs_src/dependencies/tutorial008b_an.py @@ -0,0 +1,31 @@ +from fastapi import Depends, FastAPI, HTTPException +from typing_extensions import Annotated + +app = FastAPI() + + +data = { + "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"}, + "portal-gun": {"description": "Gun to create portals", "owner": "Rick"}, +} + + +class OwnerError(Exception): + pass + + +def get_username(): + try: + yield "Rick" + except OwnerError as e: + raise HTTPException(status_code=400, detail=f"Owner error: {e}") + + +@app.get("/items/{item_id}") +def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): + if item_id not in data: + raise HTTPException(status_code=404, detail="Item not found") + item = data[item_id] + if item["owner"] != username: + raise OwnerError(username) + return item diff --git a/docs_src/dependencies/tutorial008b_an_py39.py b/docs_src/dependencies/tutorial008b_an_py39.py new file mode 100644 index 0000000000..3b8434c816 --- /dev/null +++ b/docs_src/dependencies/tutorial008b_an_py39.py @@ -0,0 +1,32 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException + +app = FastAPI() + + +data = { + "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"}, + "portal-gun": {"description": "Gun to create portals", "owner": "Rick"}, +} + + +class OwnerError(Exception): + pass + + +def get_username(): + try: + yield "Rick" + except OwnerError as e: + raise HTTPException(status_code=400, detail=f"Owner error: {e}") + + +@app.get("/items/{item_id}") +def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): + if item_id not in data: + raise HTTPException(status_code=404, detail="Item not found") + item = data[item_id] + if item["owner"] != username: + raise OwnerError(username) + return item diff --git a/docs_src/dependencies/tutorial011_an.py b/docs_src/dependencies/tutorial011_an.py new file mode 100644 index 0000000000..6c13d9033f --- /dev/null +++ b/docs_src/dependencies/tutorial011_an.py @@ -0,0 +1,22 @@ +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +class FixedContentQueryChecker: + def __init__(self, fixed_content: str): + self.fixed_content = fixed_content + + def __call__(self, q: str = ""): + if q: + return self.fixed_content in q + return False + + +checker = FixedContentQueryChecker("bar") + + +@app.get("/query-checker/") +async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]): + return {"fixed_content_in_query": fixed_content_included} diff --git a/docs_src/dependencies/tutorial011_an_py39.py b/docs_src/dependencies/tutorial011_an_py39.py new file mode 100644 index 0000000000..68b7434ec2 --- /dev/null +++ b/docs_src/dependencies/tutorial011_an_py39.py @@ -0,0 +1,23 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +class FixedContentQueryChecker: + def __init__(self, fixed_content: str): + self.fixed_content = fixed_content + + def __call__(self, q: str = ""): + if q: + return self.fixed_content in q + return False + + +checker = FixedContentQueryChecker("bar") + + +@app.get("/query-checker/") +async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]): + return {"fixed_content_in_query": fixed_content_included} diff --git a/docs_src/dependencies/tutorial012_an.py b/docs_src/dependencies/tutorial012_an.py new file mode 100644 index 0000000000..7541e6bf41 --- /dev/null +++ b/docs_src/dependencies/tutorial012_an.py @@ -0,0 +1,26 @@ +from fastapi import Depends, FastAPI, Header, HTTPException +from typing_extensions import Annotated + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)]) + + +@app.get("/items/") +async def read_items(): + return [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +@app.get("/users/") +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] diff --git a/docs_src/dependencies/tutorial012_an_py39.py b/docs_src/dependencies/tutorial012_an_py39.py new file mode 100644 index 0000000000..7541e6bf41 --- /dev/null +++ b/docs_src/dependencies/tutorial012_an_py39.py @@ -0,0 +1,26 @@ +from fastapi import Depends, FastAPI, Header, HTTPException +from typing_extensions import Annotated + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)]) + + +@app.get("/items/") +async def read_items(): + return [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +@app.get("/users/") +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] diff --git a/docs_src/dependency_testing/tutorial001_an.py b/docs_src/dependency_testing/tutorial001_an.py new file mode 100644 index 0000000000..4c76a87ff5 --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_an.py @@ -0,0 +1,60 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: Union[str, None] = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/dependency_testing/tutorial001_an_py310.py b/docs_src/dependency_testing/tutorial001_an_py310.py new file mode 100644 index 0000000000..f866e7a1bb --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_an_py310.py @@ -0,0 +1,57 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: str | None = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/dependency_testing/tutorial001_an_py39.py b/docs_src/dependency_testing/tutorial001_an_py39.py new file mode 100644 index 0000000000..bccb0cdb1f --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_an_py39.py @@ -0,0 +1,59 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: Union[str, None] = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/dependency_testing/tutorial001_py310.py b/docs_src/dependency_testing/tutorial001_py310.py new file mode 100644 index 0000000000..d1f6d4374f --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_py310.py @@ -0,0 +1,55 @@ +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: dict = Depends(common_parameters)): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: dict = Depends(common_parameters)): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: str | None = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/events/tutorial003.py b/docs_src/events/tutorial003.py new file mode 100644 index 0000000000..2b650590b0 --- /dev/null +++ b/docs_src/events/tutorial003.py @@ -0,0 +1,28 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI + + +def fake_answer_to_everything_ml_model(x: float): + return x * 42 + + +ml_models = {} + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Load the ML model + ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model + yield + # Clean up the ML models and release the resources + ml_models.clear() + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/predict") +async def predict(x: float): + result = ml_models["answer_to_everything"](x) + return {"result": result} diff --git a/docs_src/extending_openapi/tutorial001.py b/docs_src/extending_openapi/tutorial001.py index 561e95898f..35e31c0e0c 100644 --- a/docs_src/extending_openapi/tutorial001.py +++ b/docs_src/extending_openapi/tutorial001.py @@ -15,7 +15,8 @@ def custom_openapi(): openapi_schema = get_openapi( title="Custom title", version="2.5.0", - description="This is a very custom OpenAPI schema", + summary="This is a very custom OpenAPI schema", + description="Here's a longer description of the custom **OpenAPI** schema", routes=app.routes, ) openapi_schema["info"]["x-logo"] = { diff --git a/docs_src/extra_data_types/tutorial001_an.py b/docs_src/extra_data_types/tutorial001_an.py new file mode 100644 index 0000000000..a4c074241a --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_an.py @@ -0,0 +1,29 @@ +from datetime import datetime, time, timedelta +from typing import Union +from uuid import UUID + +from fastapi import Body, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: Annotated[Union[datetime, None], Body()] = None, + end_datetime: Annotated[Union[datetime, None], Body()] = None, + repeat_at: Annotated[Union[time, None], Body()] = None, + process_after: Annotated[Union[timedelta, None], Body()] = None, +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/extra_data_types/tutorial001_an_py310.py b/docs_src/extra_data_types/tutorial001_an_py310.py new file mode 100644 index 0000000000..4f69c40d95 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_an_py310.py @@ -0,0 +1,28 @@ +from datetime import datetime, time, timedelta +from typing import Annotated +from uuid import UUID + +from fastapi import Body, FastAPI + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: Annotated[datetime | None, Body()] = None, + end_datetime: Annotated[datetime | None, Body()] = None, + repeat_at: Annotated[time | None, Body()] = None, + process_after: Annotated[timedelta | None, Body()] = None, +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/extra_data_types/tutorial001_an_py39.py b/docs_src/extra_data_types/tutorial001_an_py39.py new file mode 100644 index 0000000000..630d36ae31 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_an_py39.py @@ -0,0 +1,28 @@ +from datetime import datetime, time, timedelta +from typing import Annotated, Union +from uuid import UUID + +from fastapi import Body, FastAPI + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: Annotated[Union[datetime, None], Body()] = None, + end_datetime: Annotated[Union[datetime, None], Body()] = None, + repeat_at: Annotated[Union[time, None], Body()] = None, + process_after: Annotated[Union[timedelta, None], Body()] = None, +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/extra_models/tutorial003.py b/docs_src/extra_models/tutorial003.py index 065439acce..06675cbc09 100644 --- a/docs_src/extra_models/tutorial003.py +++ b/docs_src/extra_models/tutorial003.py @@ -12,11 +12,11 @@ class BaseItem(BaseModel): class CarItem(BaseItem): - type = "car" + type: str = "car" class PlaneItem(BaseItem): - type = "plane" + type: str = "plane" size: int diff --git a/docs_src/extra_models/tutorial003_py310.py b/docs_src/extra_models/tutorial003_py310.py index 065439acce..06675cbc09 100644 --- a/docs_src/extra_models/tutorial003_py310.py +++ b/docs_src/extra_models/tutorial003_py310.py @@ -12,11 +12,11 @@ class BaseItem(BaseModel): class CarItem(BaseItem): - type = "car" + type: str = "car" class PlaneItem(BaseItem): - type = "plane" + type: str = "plane" size: int diff --git a/docs_src/generate_clients/tutorial004.js b/docs_src/generate_clients/tutorial004.js new file mode 100644 index 0000000000..18dc38267b --- /dev/null +++ b/docs_src/generate_clients/tutorial004.js @@ -0,0 +1,29 @@ +import * as fs from "fs"; + +const filePath = "./openapi.json"; + +fs.readFile(filePath, (err, data) => { + const openapiContent = JSON.parse(data); + if (err) throw err; + + const paths = openapiContent.paths; + + Object.keys(paths).forEach((pathKey) => { + const pathData = paths[pathKey]; + Object.keys(pathData).forEach((method) => { + const operation = pathData[method]; + if (operation.tags && operation.tags.length > 0) { + const tag = operation.tags[0]; + const operationId = operation.operationId; + const toRemove = `${tag}-`; + if (operationId.startsWith(toRemove)) { + const newOperationId = operationId.substring(toRemove.length); + operation.operationId = newOperationId; + } + } + }); + }); + fs.writeFile(filePath, JSON.stringify(openapiContent, null, 2), (err) => { + if (err) throw err; + }); +}); diff --git a/docs_src/header_params/tutorial001_an.py b/docs_src/header_params/tutorial001_an.py new file mode 100644 index 0000000000..816c000862 --- /dev/null +++ b/docs_src/header_params/tutorial001_an.py @@ -0,0 +1,11 @@ +from typing import Union + +from fastapi import FastAPI, Header +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: Annotated[Union[str, None], Header()] = None): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial001_an_py310.py b/docs_src/header_params/tutorial001_an_py310.py new file mode 100644 index 0000000000..4ba5e1bfbe --- /dev/null +++ b/docs_src/header_params/tutorial001_an_py310.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: Annotated[str | None, Header()] = None): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial001_an_py39.py b/docs_src/header_params/tutorial001_an_py39.py new file mode 100644 index 0000000000..1fbe3bb99a --- /dev/null +++ b/docs_src/header_params/tutorial001_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: Annotated[Union[str, None], Header()] = None): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial002_an.py b/docs_src/header_params/tutorial002_an.py new file mode 100644 index 0000000000..82fe49ba2b --- /dev/null +++ b/docs_src/header_params/tutorial002_an.py @@ -0,0 +1,15 @@ +from typing import Union + +from fastapi import FastAPI, Header +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial002_an_py310.py b/docs_src/header_params/tutorial002_an_py310.py new file mode 100644 index 0000000000..b340647b60 --- /dev/null +++ b/docs_src/header_params/tutorial002_an_py310.py @@ -0,0 +1,12 @@ +from typing import Annotated + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: Annotated[str | None, Header(convert_underscores=False)] = None +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial002_an_py39.py b/docs_src/header_params/tutorial002_an_py39.py new file mode 100644 index 0000000000..008e4b6e1a --- /dev/null +++ b/docs_src/header_params/tutorial002_an_py39.py @@ -0,0 +1,14 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial003_an.py b/docs_src/header_params/tutorial003_an.py new file mode 100644 index 0000000000..5406fd1f88 --- /dev/null +++ b/docs_src/header_params/tutorial003_an.py @@ -0,0 +1,11 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Annotated[Union[List[str], None], Header()] = None): + return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_an_py310.py b/docs_src/header_params/tutorial003_an_py310.py new file mode 100644 index 0000000000..0e78cf0290 --- /dev/null +++ b/docs_src/header_params/tutorial003_an_py310.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Annotated[list[str] | None, Header()] = None): + return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_an_py39.py b/docs_src/header_params/tutorial003_an_py39.py new file mode 100644 index 0000000000..c1dd499611 --- /dev/null +++ b/docs_src/header_params/tutorial003_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated, List, Union + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Annotated[Union[List[str], None], Header()] = None): + return {"X-Token values": x_token} diff --git a/docs_src/metadata/tutorial001.py b/docs_src/metadata/tutorial001.py index 3fba9e7d1b..76656e81b4 100644 --- a/docs_src/metadata/tutorial001.py +++ b/docs_src/metadata/tutorial001.py @@ -18,6 +18,7 @@ You will be able to: app = FastAPI( title="ChimichangApp", description=description, + summary="Deadpool's favorite app. Nuff said.", version="0.0.1", terms_of_service="http://example.com/terms/", contact={ diff --git a/docs_src/metadata/tutorial001_1.py b/docs_src/metadata/tutorial001_1.py new file mode 100644 index 0000000000..a8f5b94588 --- /dev/null +++ b/docs_src/metadata/tutorial001_1.py @@ -0,0 +1,38 @@ +from fastapi import FastAPI + +description = """ +ChimichangApp API helps you do awesome stuff. 🚀 + +## Items + +You can **read items**. + +## Users + +You will be able to: + +* **Create users** (_not implemented_). +* **Read users** (_not implemented_). +""" + +app = FastAPI( + title="ChimichangApp", + description=description, + summary="Deadpool's favorite app. Nuff said.", + version="0.0.1", + terms_of_service="http://example.com/terms/", + contact={ + "name": "Deadpoolio the Amazing", + "url": "http://x-force.example.com/contact/", + "email": "dp@x-force.example.com", + }, + license_info={ + "name": "Apache 2.0", + "identifier": "MIT", + }, +) + + +@app.get("/items/") +async def read_items(): + return [{"name": "Katana"}] diff --git a/docs_src/openapi_webhooks/tutorial001.py b/docs_src/openapi_webhooks/tutorial001.py new file mode 100644 index 0000000000..55822bb48f --- /dev/null +++ b/docs_src/openapi_webhooks/tutorial001.py @@ -0,0 +1,25 @@ +from datetime import datetime + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Subscription(BaseModel): + username: str + monthly_fee: float + start_date: datetime + + +@app.webhooks.post("new-subscription") +def new_subscription(body: Subscription): + """ + When a new user subscribes to your service we'll send you a POST request with this + data to the URL that you register for the event `new-subscription` in the dashboard. + """ + + +@app.get("/users/") +def read_users(): + return ["Rick", "Morty"] diff --git a/docs_src/path_operation_advanced_configuration/tutorial007.py b/docs_src/path_operation_advanced_configuration/tutorial007.py index d51752bb87..972ddbd2cc 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial007.py +++ b/docs_src/path_operation_advanced_configuration/tutorial007.py @@ -16,7 +16,7 @@ class Item(BaseModel): "/items/", openapi_extra={ "requestBody": { - "content": {"application/x-yaml": {"schema": Item.schema()}}, + "content": {"application/x-yaml": {"schema": Item.model_json_schema()}}, "required": True, }, }, @@ -28,7 +28,7 @@ async def create_item(request: Request): except yaml.YAMLError: raise HTTPException(status_code=422, detail="Invalid YAML") try: - item = Item.parse_obj(data) + item = Item.model_validate(data) except ValidationError as e: raise HTTPException(status_code=422, detail=e.errors()) return item diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py b/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py new file mode 100644 index 0000000000..d51752bb87 --- /dev/null +++ b/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py @@ -0,0 +1,34 @@ +from typing import List + +import yaml +from fastapi import FastAPI, HTTPException, Request +from pydantic import BaseModel, ValidationError + +app = FastAPI() + + +class Item(BaseModel): + name: str + tags: List[str] + + +@app.post( + "/items/", + openapi_extra={ + "requestBody": { + "content": {"application/x-yaml": {"schema": Item.schema()}}, + "required": True, + }, + }, +) +async def create_item(request: Request): + raw_body = await request.body() + try: + data = yaml.safe_load(raw_body) + except yaml.YAMLError: + raise HTTPException(status_code=422, detail="Invalid YAML") + try: + item = Item.parse_obj(data) + except ValidationError as e: + raise HTTPException(status_code=422, detail=e.errors()) + return item diff --git a/docs_src/path_params_numeric_validations/tutorial001_an.py b/docs_src/path_params_numeric_validations/tutorial001_an.py new file mode 100644 index 0000000000..621be7b045 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_an.py @@ -0,0 +1,17 @@ +from typing import Union + +from fastapi import FastAPI, Path, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + q: Annotated[Union[str, None], Query(alias="item-query")] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial001_an_py310.py b/docs_src/path_params_numeric_validations/tutorial001_an_py310.py new file mode 100644 index 0000000000..c4db263f0e --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_an_py310.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + q: Annotated[str | None, Query(alias="item-query")] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial001_an_py39.py b/docs_src/path_params_numeric_validations/tutorial001_an_py39.py new file mode 100644 index 0000000000..b36315a46d --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_an_py39.py @@ -0,0 +1,16 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + q: Annotated[Union[str, None], Query(alias="item-query")] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial002_an.py b/docs_src/path_params_numeric_validations/tutorial002_an.py new file mode 100644 index 0000000000..322f8cf0bb --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial002_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + q: str, item_id: Annotated[int, Path(title="The ID of the item to get")] +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial002_an_py39.py b/docs_src/path_params_numeric_validations/tutorial002_an_py39.py new file mode 100644 index 0000000000..cd882abb2c --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial002_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + q: str, item_id: Annotated[int, Path(title="The ID of the item to get")] +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial003_an.py b/docs_src/path_params_numeric_validations/tutorial003_an.py new file mode 100644 index 0000000000..d0fa8b3db9 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial003_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial003_an_py39.py b/docs_src/path_params_numeric_validations/tutorial003_an_py39.py new file mode 100644 index 0000000000..1588556e78 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial003_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial004_an.py b/docs_src/path_params_numeric_validations/tutorial004_an.py new file mode 100644 index 0000000000..ffc50f6c5b --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial004_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=1)], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial004_an_py39.py b/docs_src/path_params_numeric_validations/tutorial004_an_py39.py new file mode 100644 index 0000000000..f67f6450e7 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial004_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=1)], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial005_an.py b/docs_src/path_params_numeric_validations/tutorial005_an.py new file mode 100644 index 0000000000..433c69129f --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial005_an.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", gt=0, le=1000)], + q: str, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial005_an_py39.py b/docs_src/path_params_numeric_validations/tutorial005_an_py39.py new file mode 100644 index 0000000000..571dd583ce --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial005_an_py39.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", gt=0, le=1000)], + q: str, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial006.py b/docs_src/path_params_numeric_validations/tutorial006.py index 85bd6e8b4d..0ea32694ae 100644 --- a/docs_src/path_params_numeric_validations/tutorial006.py +++ b/docs_src/path_params_numeric_validations/tutorial006.py @@ -8,7 +8,7 @@ async def read_items( *, item_id: int = Path(title="The ID of the item to get", ge=0, le=1000), q: str, - size: float = Query(gt=0, lt=10.5) + size: float = Query(gt=0, lt=10.5), ): results = {"item_id": item_id} if q: diff --git a/docs_src/path_params_numeric_validations/tutorial006_an.py b/docs_src/path_params_numeric_validations/tutorial006_an.py new file mode 100644 index 0000000000..22a1436236 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial006_an.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Path, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + *, + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: str, + size: Annotated[float, Query(gt=0, lt=10.5)], +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial006_an_py39.py b/docs_src/path_params_numeric_validations/tutorial006_an_py39.py new file mode 100644 index 0000000000..804751893c --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial006_an_py39.py @@ -0,0 +1,18 @@ +from typing import Annotated + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + *, + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: str, + size: Annotated[float, Query(gt=0, lt=10.5)], +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/python_types/tutorial011.py b/docs_src/python_types/tutorial011.py index c8634cbff5..297a84db68 100644 --- a/docs_src/python_types/tutorial011.py +++ b/docs_src/python_types/tutorial011.py @@ -6,7 +6,7 @@ from pydantic import BaseModel class User(BaseModel): id: int - name = "John Doe" + name: str = "John Doe" signup_ts: Union[datetime, None] = None friends: List[int] = [] diff --git a/docs_src/python_types/tutorial011_py310.py b/docs_src/python_types/tutorial011_py310.py index 7f173880f5..842760c60d 100644 --- a/docs_src/python_types/tutorial011_py310.py +++ b/docs_src/python_types/tutorial011_py310.py @@ -5,7 +5,7 @@ from pydantic import BaseModel class User(BaseModel): id: int - name = "John Doe" + name: str = "John Doe" signup_ts: datetime | None = None friends: list[int] = [] diff --git a/docs_src/python_types/tutorial011_py39.py b/docs_src/python_types/tutorial011_py39.py index 468496f519..4eb40b405f 100644 --- a/docs_src/python_types/tutorial011_py39.py +++ b/docs_src/python_types/tutorial011_py39.py @@ -6,7 +6,7 @@ from pydantic import BaseModel class User(BaseModel): id: int - name = "John Doe" + name: str = "John Doe" signup_ts: Union[datetime, None] = None friends: list[int] = [] diff --git a/docs_src/python_types/tutorial013.py b/docs_src/python_types/tutorial013.py new file mode 100644 index 0000000000..0ec7735199 --- /dev/null +++ b/docs_src/python_types/tutorial013.py @@ -0,0 +1,5 @@ +from typing_extensions import Annotated + + +def say_hello(name: Annotated[str, "this is just metadata"]) -> str: + return f"Hello {name}" diff --git a/docs_src/python_types/tutorial013_py39.py b/docs_src/python_types/tutorial013_py39.py new file mode 100644 index 0000000000..65a0eaa939 --- /dev/null +++ b/docs_src/python_types/tutorial013_py39.py @@ -0,0 +1,5 @@ +from typing import Annotated + + +def say_hello(name: Annotated[str, "this is just metadata"]) -> str: + return f"Hello {name}" diff --git a/docs_src/query_params_str_validations/tutorial002_an.py b/docs_src/query_params_str_validations/tutorial002_an.py new file mode 100644 index 0000000000..cb1b38940c --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial002_an.py @@ -0,0 +1,14 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(max_length=50)] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial002_an_py310.py b/docs_src/query_params_str_validations/tutorial002_an_py310.py new file mode 100644 index 0000000000..66ee7451b0 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial002_an_py310.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query(max_length=50)] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_an.py b/docs_src/query_params_str_validations/tutorial003_an.py new file mode 100644 index 0000000000..a3665f6a85 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_an.py @@ -0,0 +1,16 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(min_length=3, max_length=50)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_an_py310.py b/docs_src/query_params_str_validations/tutorial003_an_py310.py new file mode 100644 index 0000000000..836af04de0 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_an_py310.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[str | None, Query(min_length=3, max_length=50)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_an_py39.py b/docs_src/query_params_str_validations/tutorial003_an_py39.py new file mode 100644 index 0000000000..87a4268394 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(min_length=3, max_length=50)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004.py b/docs_src/query_params_str_validations/tutorial004.py index 5a7129816c..64a647a16a 100644 --- a/docs_src/query_params_str_validations/tutorial004.py +++ b/docs_src/query_params_str_validations/tutorial004.py @@ -8,8 +8,8 @@ app = FastAPI() @app.get("/items/") async def read_items( q: Union[str, None] = Query( - default=None, min_length=3, max_length=50, regex="^fixedquery$" - ) + default=None, min_length=3, max_length=50, pattern="^fixedquery$" + ), ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial004_an.py b/docs_src/query_params_str_validations/tutorial004_an.py new file mode 100644 index 0000000000..c75d45d63e --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an.py @@ -0,0 +1,18 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$") + ] = None, +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an_py310.py b/docs_src/query_params_str_validations/tutorial004_an_py310.py new file mode 100644 index 0000000000..20cf1988fd --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an_py310.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, Query(min_length=3, max_length=50, pattern="^fixedquery$") + ] = None, +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py b/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py new file mode 100644 index 0000000000..21e0d3eb85 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, Query(min_length=3, max_length=50, regex="^fixedquery$") + ] = None, +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an_py39.py b/docs_src/query_params_str_validations/tutorial004_an_py39.py new file mode 100644 index 0000000000..de27097b38 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$") + ] = None, +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_py310.py b/docs_src/query_params_str_validations/tutorial004_py310.py index 180a2e5112..7801e75000 100644 --- a/docs_src/query_params_str_validations/tutorial004_py310.py +++ b/docs_src/query_params_str_validations/tutorial004_py310.py @@ -5,8 +5,9 @@ app = FastAPI() @app.get("/items/") async def read_items( - q: str - | None = Query(default=None, min_length=3, max_length=50, regex="^fixedquery$") + q: str | None = Query( + default=None, min_length=3, max_length=50, pattern="^fixedquery$" + ), ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial005_an.py b/docs_src/query_params_str_validations/tutorial005_an.py new file mode 100644 index 0000000000..452d4d38d7 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial005_an.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial005_an_py39.py b/docs_src/query_params_str_validations/tutorial005_an_py39.py new file mode 100644 index 0000000000..b1f6046b50 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial005_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006_an.py b/docs_src/query_params_str_validations/tutorial006_an.py new file mode 100644 index 0000000000..559480d2bf --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006_an.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)]): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006_an_py39.py b/docs_src/query_params_str_validations/tutorial006_an_py39.py new file mode 100644 index 0000000000..3b4a676d2e --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)]): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006b_an.py b/docs_src/query_params_str_validations/tutorial006b_an.py new file mode 100644 index 0000000000..ea3b02583a --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006b_an.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006b_an_py39.py b/docs_src/query_params_str_validations/tutorial006b_an_py39.py new file mode 100644 index 0000000000..687a9f5446 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006b_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an.py b/docs_src/query_params_str_validations/tutorial006c_an.py new file mode 100644 index 0000000000..10bf26a577 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_an.py @@ -0,0 +1,14 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an_py310.py b/docs_src/query_params_str_validations/tutorial006c_an_py310.py new file mode 100644 index 0000000000..1ab0a7d53d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_an_py310.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an_py39.py b/docs_src/query_params_str_validations/tutorial006c_an_py39.py new file mode 100644 index 0000000000..ac12733318 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006d_an.py b/docs_src/query_params_str_validations/tutorial006d_an.py new file mode 100644 index 0000000000..bc8283e153 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006d_an.py @@ -0,0 +1,13 @@ +from fastapi import FastAPI, Query +from pydantic import Required +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = Required): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py39.py b/docs_src/query_params_str_validations/tutorial006d_an_py39.py new file mode 100644 index 0000000000..035d9e3bdd --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006d_an_py39.py @@ -0,0 +1,14 @@ +from typing import Annotated + +from fastapi import FastAPI, Query +from pydantic import Required + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = Required): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_an.py b/docs_src/query_params_str_validations/tutorial007_an.py new file mode 100644 index 0000000000..3bc85cc0cf --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_an.py @@ -0,0 +1,16 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(title="Query string", min_length=3)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_an_py310.py b/docs_src/query_params_str_validations/tutorial007_an_py310.py new file mode 100644 index 0000000000..5933911fdf --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_an_py310.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[str | None, Query(title="Query string", min_length=3)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_an_py39.py b/docs_src/query_params_str_validations/tutorial007_an_py39.py new file mode 100644 index 0000000000..dafa1c5c94 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(title="Query string", min_length=3)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008.py b/docs_src/query_params_str_validations/tutorial008.py index d112a9ab8a..e3e0b50aa1 100644 --- a/docs_src/query_params_str_validations/tutorial008.py +++ b/docs_src/query_params_str_validations/tutorial008.py @@ -12,7 +12,7 @@ async def read_items( title="Query string", description="Query string for the items to search in the database that have a good match", min_length=3, - ) + ), ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial008_an.py b/docs_src/query_params_str_validations/tutorial008_an.py new file mode 100644 index 0000000000..01606a9203 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_an.py @@ -0,0 +1,23 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ), + ] = None, +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_an_py310.py b/docs_src/query_params_str_validations/tutorial008_an_py310.py new file mode 100644 index 0000000000..44b3082b63 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_an_py310.py @@ -0,0 +1,22 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, + Query( + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ), + ] = None, +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_an_py39.py b/docs_src/query_params_str_validations/tutorial008_an_py39.py new file mode 100644 index 0000000000..f3f2f2c0e7 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_an_py39.py @@ -0,0 +1,22 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ), + ] = None, +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_py310.py b/docs_src/query_params_str_validations/tutorial008_py310.py index 489f631d5e..5743852726 100644 --- a/docs_src/query_params_str_validations/tutorial008_py310.py +++ b/docs_src/query_params_str_validations/tutorial008_py310.py @@ -5,13 +5,12 @@ app = FastAPI() @app.get("/items/") async def read_items( - q: str - | None = Query( + q: str | None = Query( default=None, title="Query string", description="Query string for the items to search in the database that have a good match", min_length=3, - ) + ), ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial009_an.py b/docs_src/query_params_str_validations/tutorial009_an.py new file mode 100644 index 0000000000..2894e2d51a --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_an.py @@ -0,0 +1,14 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(alias="item-query")] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial009_an_py310.py b/docs_src/query_params_str_validations/tutorial009_an_py310.py new file mode 100644 index 0000000000..d11753362b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_an_py310.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query(alias="item-query")] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial009_an_py39.py b/docs_src/query_params_str_validations/tutorial009_an_py39.py new file mode 100644 index 0000000000..70a89e6137 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(alias="item-query")] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010.py b/docs_src/query_params_str_validations/tutorial010.py index 35443d1947..ff29176fe5 100644 --- a/docs_src/query_params_str_validations/tutorial010.py +++ b/docs_src/query_params_str_validations/tutorial010.py @@ -14,9 +14,9 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, - regex="^fixedquery$", + pattern="^fixedquery$", deprecated=True, - ) + ), ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial010_an.py b/docs_src/query_params_str_validations/tutorial010_an.py new file mode 100644 index 0000000000..ed343230f4 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_an.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + pattern="^fixedquery$", + deprecated=True, + ), + ] = None, +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_an_py310.py b/docs_src/query_params_str_validations/tutorial010_an_py310.py new file mode 100644 index 0000000000..775095bda8 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_an_py310.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, + Query( + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + pattern="^fixedquery$", + deprecated=True, + ), + ] = None, +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_an_py39.py b/docs_src/query_params_str_validations/tutorial010_an_py39.py new file mode 100644 index 0000000000..b126c116f0 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_an_py39.py @@ -0,0 +1,26 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + pattern="^fixedquery$", + deprecated=True, + ), + ] = None, +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_py310.py b/docs_src/query_params_str_validations/tutorial010_py310.py index f2839516e6..530e6cf5b6 100644 --- a/docs_src/query_params_str_validations/tutorial010_py310.py +++ b/docs_src/query_params_str_validations/tutorial010_py310.py @@ -5,17 +5,16 @@ app = FastAPI() @app.get("/items/") async def read_items( - q: str - | None = Query( + q: str | None = Query( default=None, alias="item-query", title="Query string", description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, - regex="^fixedquery$", + pattern="^fixedquery$", deprecated=True, - ) + ), ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial011_an.py b/docs_src/query_params_str_validations/tutorial011_an.py new file mode 100644 index 0000000000..8ed699337d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_an.py @@ -0,0 +1,12 @@ +from typing import List, Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[List[str], None], Query()] = None): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_an_py310.py b/docs_src/query_params_str_validations/tutorial011_an_py310.py new file mode 100644 index 0000000000..5dad4bfde1 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_an_py310.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list[str] | None, Query()] = None): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_an_py39.py b/docs_src/query_params_str_validations/tutorial011_an_py39.py new file mode 100644 index 0000000000..416e3990dc --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[list[str], None], Query()] = None): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial012_an.py b/docs_src/query_params_str_validations/tutorial012_an.py new file mode 100644 index 0000000000..261af250a1 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial012_an.py @@ -0,0 +1,12 @@ +from typing import List + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[List[str], Query()] = ["foo", "bar"]): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial012_an_py39.py b/docs_src/query_params_str_validations/tutorial012_an_py39.py new file mode 100644 index 0000000000..9b5a9c2fb2 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial012_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list[str], Query()] = ["foo", "bar"]): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial013_an.py b/docs_src/query_params_str_validations/tutorial013_an.py new file mode 100644 index 0000000000..f12a250559 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial013_an.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list, Query()] = []): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial013_an_py39.py b/docs_src/query_params_str_validations/tutorial013_an_py39.py new file mode 100644 index 0000000000..602734145d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial013_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list, Query()] = []): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial014_an.py b/docs_src/query_params_str_validations/tutorial014_an.py new file mode 100644 index 0000000000..a9a9c44270 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_an.py @@ -0,0 +1,16 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Annotated[Union[str, None], Query(include_in_schema=False)] = None +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/query_params_str_validations/tutorial014_an_py310.py b/docs_src/query_params_str_validations/tutorial014_an_py310.py new file mode 100644 index 0000000000..5fba54150b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_an_py310.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Annotated[str | None, Query(include_in_schema=False)] = None +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/query_params_str_validations/tutorial014_an_py39.py b/docs_src/query_params_str_validations/tutorial014_an_py39.py new file mode 100644 index 0000000000..b079852100 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Annotated[Union[str, None], Query(include_in_schema=False)] = None +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/request_files/tutorial001_02_an.py b/docs_src/request_files/tutorial001_02_an.py new file mode 100644 index 0000000000..5007fef159 --- /dev/null +++ b/docs_src/request_files/tutorial001_02_an.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import FastAPI, File, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[Union[bytes, None], File()] = None): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: Union[UploadFile, None] = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02_an_py310.py b/docs_src/request_files/tutorial001_02_an_py310.py new file mode 100644 index 0000000000..9b80321b45 --- /dev/null +++ b/docs_src/request_files/tutorial001_02_an_py310.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes | None, File()] = None): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile | None = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02_an_py39.py b/docs_src/request_files/tutorial001_02_an_py39.py new file mode 100644 index 0000000000..bb090ff6ca --- /dev/null +++ b/docs_src/request_files/tutorial001_02_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[Union[bytes, None], File()] = None): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: Union[UploadFile, None] = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_03_an.py b/docs_src/request_files/tutorial001_03_an.py new file mode 100644 index 0000000000..8a6b0a2455 --- /dev/null +++ b/docs_src/request_files/tutorial001_03_an.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI, File, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File(description="A file read as bytes")]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file( + file: Annotated[UploadFile, File(description="A file read as UploadFile")], +): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_03_an_py39.py b/docs_src/request_files/tutorial001_03_an_py39.py new file mode 100644 index 0000000000..93098a677a --- /dev/null +++ b/docs_src/request_files/tutorial001_03_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File(description="A file read as bytes")]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file( + file: Annotated[UploadFile, File(description="A file read as UploadFile")], +): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_an.py b/docs_src/request_files/tutorial001_an.py new file mode 100644 index 0000000000..ca2f76d5c6 --- /dev/null +++ b/docs_src/request_files/tutorial001_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, File, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File()]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_an_py39.py b/docs_src/request_files/tutorial001_an_py39.py new file mode 100644 index 0000000000..26a7672216 --- /dev/null +++ b/docs_src/request_files/tutorial001_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File()]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial002_an.py b/docs_src/request_files/tutorial002_an.py new file mode 100644 index 0000000000..eaa90da2b6 --- /dev/null +++ b/docs_src/request_files/tutorial002_an.py @@ -0,0 +1,34 @@ +from typing import List + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_files(files: Annotated[List[bytes], File()]): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files(files: List[UploadFile]): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial002_an_py39.py b/docs_src/request_files/tutorial002_an_py39.py new file mode 100644 index 0000000000..db524ceab6 --- /dev/null +++ b/docs_src/request_files/tutorial002_an_py39.py @@ -0,0 +1,33 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files(files: Annotated[list[bytes], File()]): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files(files: list[UploadFile]): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial003_an.py b/docs_src/request_files/tutorial003_an.py new file mode 100644 index 0000000000..2238e3c94b --- /dev/null +++ b/docs_src/request_files/tutorial003_an.py @@ -0,0 +1,40 @@ +from typing import List + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_files( + files: Annotated[List[bytes], File(description="Multiple files as bytes")], +): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files( + files: Annotated[ + List[UploadFile], File(description="Multiple files as UploadFile") + ], +): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial003_an_py39.py b/docs_src/request_files/tutorial003_an_py39.py new file mode 100644 index 0000000000..5a8c5dab5c --- /dev/null +++ b/docs_src/request_files/tutorial003_an_py39.py @@ -0,0 +1,39 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files( + files: Annotated[list[bytes], File(description="Multiple files as bytes")], +): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files( + files: Annotated[ + list[UploadFile], File(description="Multiple files as UploadFile") + ], +): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_forms/tutorial001_an.py b/docs_src/request_forms/tutorial001_an.py new file mode 100644 index 0000000000..677fbf2db8 --- /dev/null +++ b/docs_src/request_forms/tutorial001_an.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI, Form +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/login/") +async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]): + return {"username": username} diff --git a/docs_src/request_forms/tutorial001_an_py39.py b/docs_src/request_forms/tutorial001_an_py39.py new file mode 100644 index 0000000000..8e9d2ea53a --- /dev/null +++ b/docs_src/request_forms/tutorial001_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import FastAPI, Form + +app = FastAPI() + + +@app.post("/login/") +async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]): + return {"username": username} diff --git a/docs_src/request_forms_and_files/tutorial001_an.py b/docs_src/request_forms_and_files/tutorial001_an.py new file mode 100644 index 0000000000..0ea285ac87 --- /dev/null +++ b/docs_src/request_forms_and_files/tutorial001_an.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, File, Form, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file( + file: Annotated[bytes, File()], + fileb: Annotated[UploadFile, File()], + token: Annotated[str, Form()], +): + return { + "file_size": len(file), + "token": token, + "fileb_content_type": fileb.content_type, + } diff --git a/docs_src/request_forms_and_files/tutorial001_an_py39.py b/docs_src/request_forms_and_files/tutorial001_an_py39.py new file mode 100644 index 0000000000..12cc43e50a --- /dev/null +++ b/docs_src/request_forms_and_files/tutorial001_an_py39.py @@ -0,0 +1,18 @@ +from typing import Annotated + +from fastapi import FastAPI, File, Form, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file( + file: Annotated[bytes, File()], + fileb: Annotated[UploadFile, File()], + token: Annotated[str, Form()], +): + return { + "file_size": len(file), + "token": token, + "fileb_content_type": fileb.content_type, + } diff --git a/docs_src/response_model/tutorial003_02.py b/docs_src/response_model/tutorial003_02.py new file mode 100644 index 0000000000..df6a09646d --- /dev/null +++ b/docs_src/response_model/tutorial003_02.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Response +from fastapi.responses import JSONResponse, RedirectResponse + +app = FastAPI() + + +@app.get("/portal") +async def get_portal(teleport: bool = False) -> Response: + if teleport: + return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") + return JSONResponse(content={"message": "Here's your interdimensional portal."}) diff --git a/docs_src/response_model/tutorial003_03.py b/docs_src/response_model/tutorial003_03.py new file mode 100644 index 0000000000..0d4bd8de57 --- /dev/null +++ b/docs_src/response_model/tutorial003_03.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI +from fastapi.responses import RedirectResponse + +app = FastAPI() + + +@app.get("/teleport") +async def get_teleport() -> RedirectResponse: + return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") diff --git a/docs_src/response_model/tutorial003_04.py b/docs_src/response_model/tutorial003_04.py new file mode 100644 index 0000000000..b13a926929 --- /dev/null +++ b/docs_src/response_model/tutorial003_04.py @@ -0,0 +1,13 @@ +from typing import Union + +from fastapi import FastAPI, Response +from fastapi.responses import RedirectResponse + +app = FastAPI() + + +@app.get("/portal") +async def get_portal(teleport: bool = False) -> Union[Response, dict]: + if teleport: + return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") + return {"message": "Here's your interdimensional portal."} diff --git a/docs_src/response_model/tutorial003_04_py310.py b/docs_src/response_model/tutorial003_04_py310.py new file mode 100644 index 0000000000..cee49b83e0 --- /dev/null +++ b/docs_src/response_model/tutorial003_04_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Response +from fastapi.responses import RedirectResponse + +app = FastAPI() + + +@app.get("/portal") +async def get_portal(teleport: bool = False) -> Response | dict: + if teleport: + return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") + return {"message": "Here's your interdimensional portal."} diff --git a/docs_src/response_model/tutorial003_05.py b/docs_src/response_model/tutorial003_05.py new file mode 100644 index 0000000000..0962061a60 --- /dev/null +++ b/docs_src/response_model/tutorial003_05.py @@ -0,0 +1,13 @@ +from typing import Union + +from fastapi import FastAPI, Response +from fastapi.responses import RedirectResponse + +app = FastAPI() + + +@app.get("/portal", response_model=None) +async def get_portal(teleport: bool = False) -> Union[Response, dict]: + if teleport: + return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") + return {"message": "Here's your interdimensional portal."} diff --git a/docs_src/response_model/tutorial003_05_py310.py b/docs_src/response_model/tutorial003_05_py310.py new file mode 100644 index 0000000000..f1c0f8e126 --- /dev/null +++ b/docs_src/response_model/tutorial003_05_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Response +from fastapi.responses import RedirectResponse + +app = FastAPI() + + +@app.get("/portal", response_model=None) +async def get_portal(teleport: bool = False) -> Response | dict: + if teleport: + return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") + return {"message": "Here's your interdimensional portal."} diff --git a/docs_src/schema_extra_example/tutorial001.py b/docs_src/schema_extra_example/tutorial001.py index a5ae281274..32a66db3a9 100644 --- a/docs_src/schema_extra_example/tutorial001.py +++ b/docs_src/schema_extra_example/tutorial001.py @@ -12,15 +12,18 @@ class Item(BaseModel): price: float tax: Union[float, None] = None - class Config: - schema_extra = { - "example": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } + model_config = { + "json_schema_extra": { + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ] } + } @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial001_pv1.py b/docs_src/schema_extra_example/tutorial001_pv1.py new file mode 100644 index 0000000000..6ab96ff859 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial001_pv1.py @@ -0,0 +1,31 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + class Config: + schema_extra = { + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ] + } + + +@app.put("/items/{item_id}") +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/tutorial001_py310.py b/docs_src/schema_extra_example/tutorial001_py310.py index 77ceedd60d..84aa5fc122 100644 --- a/docs_src/schema_extra_example/tutorial001_py310.py +++ b/docs_src/schema_extra_example/tutorial001_py310.py @@ -10,15 +10,18 @@ class Item(BaseModel): price: float tax: float | None = None - class Config: - schema_extra = { - "example": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } + model_config = { + "json_schema_extra": { + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ] } + } @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial001_py310_pv1.py b/docs_src/schema_extra_example/tutorial001_py310_pv1.py new file mode 100644 index 0000000000..ec83f1112f --- /dev/null +++ b/docs_src/schema_extra_example/tutorial001_py310_pv1.py @@ -0,0 +1,29 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + class Config: + schema_extra = { + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ] + } + + +@app.put("/items/{item_id}") +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 6de434f81d..70f06567c3 100644 --- a/docs_src/schema_extra_example/tutorial002.py +++ b/docs_src/schema_extra_example/tutorial002.py @@ -7,10 +7,10 @@ app = FastAPI() class Item(BaseModel): - name: str = Field(example="Foo") - description: Union[str, None] = Field(default=None, example="A very nice Item") - price: float = Field(example=35.4) - tax: Union[float, None] = Field(default=None, example=3.2) + name: str = Field(examples=["Foo"]) + description: Union[str, None] = Field(default=None, examples=["A very nice Item"]) + price: float = Field(examples=[35.4]) + tax: Union[float, None] = Field(default=None, examples=[3.2]) @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial002_py310.py b/docs_src/schema_extra_example/tutorial002_py310.py index e84928bb11..27d7868676 100644 --- a/docs_src/schema_extra_example/tutorial002_py310.py +++ b/docs_src/schema_extra_example/tutorial002_py310.py @@ -5,10 +5,10 @@ app = FastAPI() class Item(BaseModel): - name: str = Field(example="Foo") - description: str | None = Field(default=None, example="A very nice Item") - price: float = Field(example=35.4) - tax: float | None = Field(default=None, example=3.2) + name: str = Field(examples=["Foo"]) + description: str | None = Field(default=None, examples=["A very nice Item"]) + price: float = Field(examples=[35.4]) + tax: float | None = Field(default=None, examples=[3.2]) @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial003.py b/docs_src/schema_extra_example/tutorial003.py index ce1736bbaf..385f3de8a1 100644 --- a/docs_src/schema_extra_example/tutorial003.py +++ b/docs_src/schema_extra_example/tutorial003.py @@ -17,12 +17,14 @@ class Item(BaseModel): async def update_item( item_id: int, item: Item = Body( - example={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], ), ): results = {"item_id": item_id, "item": item} diff --git a/docs_src/schema_extra_example/tutorial003_an.py b/docs_src/schema_extra_example/tutorial003_an.py new file mode 100644 index 0000000000..23675aba14 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_an.py @@ -0,0 +1,35 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Annotated[ + Item, + Body( + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial003_an_py310.py b/docs_src/schema_extra_example/tutorial003_an_py310.py new file mode 100644 index 0000000000..bbd2e171ec --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_an_py310.py @@ -0,0 +1,34 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Annotated[ + Item, + Body( + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial003_an_py39.py b/docs_src/schema_extra_example/tutorial003_an_py39.py new file mode 100644 index 0000000000..4728085617 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_an_py39.py @@ -0,0 +1,34 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Annotated[ + Item, + Body( + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial003_py310.py b/docs_src/schema_extra_example/tutorial003_py310.py index 1e137101d9..2d31619be2 100644 --- a/docs_src/schema_extra_example/tutorial003_py310.py +++ b/docs_src/schema_extra_example/tutorial003_py310.py @@ -15,12 +15,14 @@ class Item(BaseModel): async def update_item( item_id: int, item: Item = Body( - example={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], ), ): results = {"item_id": item_id, "item": item} diff --git a/docs_src/schema_extra_example/tutorial004.py b/docs_src/schema_extra_example/tutorial004.py index b67edf30cd..75514a3e91 100644 --- a/docs_src/schema_extra_example/tutorial004.py +++ b/docs_src/schema_extra_example/tutorial004.py @@ -18,33 +18,22 @@ async def update_item( *, item_id: int, item: Item = Body( - examples={ - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, + { + "name": "Bar", + "price": "35.4", }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + { + "name": "Baz", + "price": "thirty five point four", }, - }, + ], ), ): results = {"item_id": item_id, "item": item} diff --git a/docs_src/schema_extra_example/tutorial004_an.py b/docs_src/schema_extra_example/tutorial004_an.py new file mode 100644 index 0000000000..e817302a22 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_an.py @@ -0,0 +1,44 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + { + "name": "Bar", + "price": "35.4", + }, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_an_py310.py b/docs_src/schema_extra_example/tutorial004_an_py310.py new file mode 100644 index 0000000000..650da3187e --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_an_py310.py @@ -0,0 +1,43 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + { + "name": "Bar", + "price": "35.4", + }, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_an_py39.py b/docs_src/schema_extra_example/tutorial004_an_py39.py new file mode 100644 index 0000000000..dc5a8fe49c --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_an_py39.py @@ -0,0 +1,43 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + { + "name": "Bar", + "price": "35.4", + }, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_py310.py b/docs_src/schema_extra_example/tutorial004_py310.py index 100a30860b..05996ac2a2 100644 --- a/docs_src/schema_extra_example/tutorial004_py310.py +++ b/docs_src/schema_extra_example/tutorial004_py310.py @@ -16,33 +16,22 @@ async def update_item( *, item_id: int, item: Item = Body( - examples={ - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, + { + "name": "Bar", + "price": "35.4", }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + { + "name": "Baz", + "price": "thirty five point four", }, - }, + ], ), ): results = {"item_id": item_id, "item": item} diff --git a/docs_src/schema_extra_example/tutorial005.py b/docs_src/schema_extra_example/tutorial005.py new file mode 100644 index 0000000000..b8217c27e9 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial005.py @@ -0,0 +1,51 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item = Body( + openapi_examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial005_an.py b/docs_src/schema_extra_example/tutorial005_an.py new file mode 100644 index 0000000000..4b2d9c662b --- /dev/null +++ b/docs_src/schema_extra_example/tutorial005_an.py @@ -0,0 +1,55 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + openapi_examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial005_an_py310.py b/docs_src/schema_extra_example/tutorial005_an_py310.py new file mode 100644 index 0000000000..64dc2cf90a --- /dev/null +++ b/docs_src/schema_extra_example/tutorial005_an_py310.py @@ -0,0 +1,54 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + openapi_examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial005_an_py39.py b/docs_src/schema_extra_example/tutorial005_an_py39.py new file mode 100644 index 0000000000..edeb1affce --- /dev/null +++ b/docs_src/schema_extra_example/tutorial005_an_py39.py @@ -0,0 +1,54 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + openapi_examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial005_py310.py b/docs_src/schema_extra_example/tutorial005_py310.py new file mode 100644 index 0000000000..eef973343d --- /dev/null +++ b/docs_src/schema_extra_example/tutorial005_py310.py @@ -0,0 +1,49 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item = Body( + openapi_examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/security/tutorial001_an.py b/docs_src/security/tutorial001_an.py new file mode 100644 index 0000000000..dac915b7ca --- /dev/null +++ b/docs_src/security/tutorial001_an.py @@ -0,0 +1,12 @@ +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from typing_extensions import Annotated + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +@app.get("/items/") +async def read_items(token: Annotated[str, Depends(oauth2_scheme)]): + return {"token": token} diff --git a/docs_src/security/tutorial001_an_py39.py b/docs_src/security/tutorial001_an_py39.py new file mode 100644 index 0000000000..de110402ef --- /dev/null +++ b/docs_src/security/tutorial001_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +@app.get("/items/") +async def read_items(token: Annotated[str, Depends(oauth2_scheme)]): + return {"token": token} diff --git a/docs_src/security/tutorial002_an.py b/docs_src/security/tutorial002_an.py new file mode 100644 index 0000000000..291b3bf530 --- /dev/null +++ b/docs_src/security/tutorial002_an.py @@ -0,0 +1,33 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): + return current_user diff --git a/docs_src/security/tutorial002_an_py310.py b/docs_src/security/tutorial002_an_py310.py new file mode 100644 index 0000000000..c7b761e459 --- /dev/null +++ b/docs_src/security/tutorial002_an_py310.py @@ -0,0 +1,32 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): + return current_user diff --git a/docs_src/security/tutorial002_an_py39.py b/docs_src/security/tutorial002_an_py39.py new file mode 100644 index 0000000000..7ff1c470bb --- /dev/null +++ b/docs_src/security/tutorial002_an_py39.py @@ -0,0 +1,32 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): + return current_user diff --git a/docs_src/security/tutorial003_an.py b/docs_src/security/tutorial003_an.py new file mode 100644 index 0000000000..261cb4857d --- /dev/null +++ b/docs_src/security/tutorial003_an.py @@ -0,0 +1,95 @@ +from typing import Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel +from typing_extensions import Annotated + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user diff --git a/docs_src/security/tutorial003_an_py310.py b/docs_src/security/tutorial003_an_py310.py new file mode 100644 index 0000000000..a03f4f8bf5 --- /dev/null +++ b/docs_src/security/tutorial003_an_py310.py @@ -0,0 +1,94 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user diff --git a/docs_src/security/tutorial003_an_py39.py b/docs_src/security/tutorial003_an_py39.py new file mode 100644 index 0000000000..308dbe798f --- /dev/null +++ b/docs_src/security/tutorial003_an_py39.py @@ -0,0 +1,94 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user diff --git a/docs_src/security/tutorial004.py b/docs_src/security/tutorial004.py index 64099abe9c..044eec7003 100644 --- a/docs_src/security/tutorial004.py +++ b/docs_src/security/tutorial004.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Union from fastapi import Depends, FastAPI, HTTPException, status @@ -78,9 +78,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt @@ -112,8 +112,10 @@ async def get_current_active_user(current_user: User = Depends(get_current_user) return current_user -@app.post("/token", response_model=Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): +@app.post("/token") +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends() +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException( @@ -125,7 +127,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial004_an.py b/docs_src/security/tutorial004_an.py new file mode 100644 index 0000000000..c78e8496c6 --- /dev/null +++ b/docs_src/security/tutorial004_an.py @@ -0,0 +1,147 @@ +from datetime import datetime, timedelta, timezone +from typing import Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel +from typing_extensions import Annotated + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +) -> Token: + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return Token(access_token=access_token, token_type="bearer") + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial004_an_py310.py b/docs_src/security/tutorial004_an_py310.py new file mode 100644 index 0000000000..36dbc677e0 --- /dev/null +++ b/docs_src/security/tutorial004_an_py310.py @@ -0,0 +1,146 @@ +from datetime import datetime, timedelta, timezone +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +) -> Token: + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return Token(access_token=access_token, token_type="bearer") + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial004_an_py39.py b/docs_src/security/tutorial004_an_py39.py new file mode 100644 index 0000000000..23fc04a721 --- /dev/null +++ b/docs_src/security/tutorial004_an_py39.py @@ -0,0 +1,146 @@ +from datetime import datetime, timedelta, timezone +from typing import Annotated, Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +) -> Token: + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return Token(access_token=access_token, token_type="bearer") + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial004_py310.py b/docs_src/security/tutorial004_py310.py index 797d56d043..8363d45ab5 100644 --- a/docs_src/security/tutorial004_py310.py +++ b/docs_src/security/tutorial004_py310.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm @@ -77,9 +77,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt @@ -111,8 +111,10 @@ async def get_current_active_user(current_user: User = Depends(get_current_user) return current_user -@app.post("/token", response_model=Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): +@app.post("/token") +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends() +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException( @@ -124,7 +126,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial005.py b/docs_src/security/tutorial005.py index bd0a33581c..b16bf440a5 100644 --- a/docs_src/security/tutorial005.py +++ b/docs_src/security/tutorial005.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import List, Union from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -93,9 +93,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt @@ -143,8 +143,10 @@ async def get_current_active_user( return current_user -@app.post("/token", response_model=Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): +@app.post("/token") +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends() +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException(status_code=400, detail="Incorrect username or password") @@ -153,7 +155,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( data={"sub": user.username, "scopes": form_data.scopes}, expires_delta=access_token_expires, ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial005_an.py b/docs_src/security/tutorial005_an.py new file mode 100644 index 0000000000..95e406b32f --- /dev/null +++ b/docs_src/security/tutorial005_an.py @@ -0,0 +1,178 @@ +from datetime import datetime, timedelta, timezone +from typing import List, Union + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError +from typing_extensions import Annotated + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + scopes: List[str] = [] + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = "Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Security(get_current_user, scopes=["me"])] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +) -> Token: + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return Token(access_token=access_token, token_type="bearer") + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): + return {"status": "ok"} diff --git a/docs_src/security/tutorial005_an_py310.py b/docs_src/security/tutorial005_an_py310.py new file mode 100644 index 0000000000..c6116a5ed1 --- /dev/null +++ b/docs_src/security/tutorial005_an_py310.py @@ -0,0 +1,177 @@ +from datetime import datetime, timedelta, timezone +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None + scopes: list[str] = [] + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = "Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Security(get_current_user, scopes=["me"])] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +) -> Token: + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return Token(access_token=access_token, token_type="bearer") + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): + return {"status": "ok"} diff --git a/docs_src/security/tutorial005_an_py39.py b/docs_src/security/tutorial005_an_py39.py new file mode 100644 index 0000000000..af51c08b50 --- /dev/null +++ b/docs_src/security/tutorial005_an_py39.py @@ -0,0 +1,177 @@ +from datetime import datetime, timedelta, timezone +from typing import Annotated, List, Union + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + scopes: List[str] = [] + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = "Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Security(get_current_user, scopes=["me"])] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +) -> Token: + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return Token(access_token=access_token, token_type="bearer") + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): + return {"status": "ok"} diff --git a/docs_src/security/tutorial005_py310.py b/docs_src/security/tutorial005_py310.py index ba756ef4f4..37a22c7090 100644 --- a/docs_src/security/tutorial005_py310.py +++ b/docs_src/security/tutorial005_py310.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from fastapi import Depends, FastAPI, HTTPException, Security, status from fastapi.security import ( @@ -92,9 +92,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt @@ -142,8 +142,10 @@ async def get_current_active_user( return current_user -@app.post("/token", response_model=Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): +@app.post("/token") +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends() +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException(status_code=400, detail="Incorrect username or password") @@ -152,7 +154,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( data={"sub": user.username, "scopes": form_data.scopes}, expires_delta=access_token_expires, ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial005_py39.py b/docs_src/security/tutorial005_py39.py index 9e4dbcffba..c275807636 100644 --- a/docs_src/security/tutorial005_py39.py +++ b/docs_src/security/tutorial005_py39.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Union from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -93,9 +93,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt @@ -143,8 +143,10 @@ async def get_current_active_user( return current_user -@app.post("/token", response_model=Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): +@app.post("/token") +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends() +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException(status_code=400, detail="Incorrect username or password") @@ -153,7 +155,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( data={"sub": user.username, "scopes": form_data.scopes}, expires_delta=access_token_expires, ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial006_an.py b/docs_src/security/tutorial006_an.py new file mode 100644 index 0000000000..985e4b2ad2 --- /dev/null +++ b/docs_src/security/tutorial006_an.py @@ -0,0 +1,12 @@ +from fastapi import Depends, FastAPI +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from typing_extensions import Annotated + +app = FastAPI() + +security = HTTPBasic() + + +@app.get("/users/me") +def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): + return {"username": credentials.username, "password": credentials.password} diff --git a/docs_src/security/tutorial006_an_py39.py b/docs_src/security/tutorial006_an_py39.py new file mode 100644 index 0000000000..03c696a4b6 --- /dev/null +++ b/docs_src/security/tutorial006_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.security import HTTPBasic, HTTPBasicCredentials + +app = FastAPI() + +security = HTTPBasic() + + +@app.get("/users/me") +def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): + return {"username": credentials.username, "password": credentials.password} diff --git a/docs_src/security/tutorial007.py b/docs_src/security/tutorial007.py index 790ee10bc6..ac816eb0c1 100644 --- a/docs_src/security/tutorial007.py +++ b/docs_src/security/tutorial007.py @@ -22,7 +22,7 @@ def get_current_username(credentials: HTTPBasicCredentials = Depends(security)): if not (is_correct_username and is_correct_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect email or password", + detail="Incorrect username or password", headers={"WWW-Authenticate": "Basic"}, ) return credentials.username diff --git a/docs_src/security/tutorial007_an.py b/docs_src/security/tutorial007_an.py new file mode 100644 index 0000000000..9e9c3cd702 --- /dev/null +++ b/docs_src/security/tutorial007_an.py @@ -0,0 +1,36 @@ +import secrets + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from typing_extensions import Annotated + +app = FastAPI() + +security = HTTPBasic() + + +def get_current_username( + credentials: Annotated[HTTPBasicCredentials, Depends(security)] +): + current_username_bytes = credentials.username.encode("utf8") + correct_username_bytes = b"stanleyjobson" + is_correct_username = secrets.compare_digest( + current_username_bytes, correct_username_bytes + ) + current_password_bytes = credentials.password.encode("utf8") + correct_password_bytes = b"swordfish" + is_correct_password = secrets.compare_digest( + current_password_bytes, correct_password_bytes + ) + if not (is_correct_username and is_correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials.username + + +@app.get("/users/me") +def read_current_user(username: Annotated[str, Depends(get_current_username)]): + return {"username": username} diff --git a/docs_src/security/tutorial007_an_py39.py b/docs_src/security/tutorial007_an_py39.py new file mode 100644 index 0000000000..3d9ea27269 --- /dev/null +++ b/docs_src/security/tutorial007_an_py39.py @@ -0,0 +1,36 @@ +import secrets +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPBasic, HTTPBasicCredentials + +app = FastAPI() + +security = HTTPBasic() + + +def get_current_username( + credentials: Annotated[HTTPBasicCredentials, Depends(security)] +): + current_username_bytes = credentials.username.encode("utf8") + correct_username_bytes = b"stanleyjobson" + is_correct_username = secrets.compare_digest( + current_username_bytes, correct_username_bytes + ) + current_password_bytes = credentials.password.encode("utf8") + correct_password_bytes = b"swordfish" + is_correct_password = secrets.compare_digest( + current_password_bytes, correct_password_bytes + ) + if not (is_correct_username and is_correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials.username + + +@app.get("/users/me") +def read_current_user(username: Annotated[str, Depends(get_current_username)]): + return {"username": username} diff --git a/docs_src/separate_openapi_schemas/tutorial001.py b/docs_src/separate_openapi_schemas/tutorial001.py new file mode 100644 index 0000000000..415eef8e28 --- /dev/null +++ b/docs_src/separate_openapi_schemas/tutorial001.py @@ -0,0 +1,28 @@ +from typing import List, Union + +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + + +app = FastAPI() + + +@app.post("/items/") +def create_item(item: Item): + return item + + +@app.get("/items/") +def read_items() -> List[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + ), + Item(name="Plumbus"), + ] diff --git a/docs_src/separate_openapi_schemas/tutorial001_py310.py b/docs_src/separate_openapi_schemas/tutorial001_py310.py new file mode 100644 index 0000000000..289cb54eda --- /dev/null +++ b/docs_src/separate_openapi_schemas/tutorial001_py310.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + + +app = FastAPI() + + +@app.post("/items/") +def create_item(item: Item): + return item + + +@app.get("/items/") +def read_items() -> list[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + ), + Item(name="Plumbus"), + ] diff --git a/docs_src/separate_openapi_schemas/tutorial001_py39.py b/docs_src/separate_openapi_schemas/tutorial001_py39.py new file mode 100644 index 0000000000..63cffd1e30 --- /dev/null +++ b/docs_src/separate_openapi_schemas/tutorial001_py39.py @@ -0,0 +1,28 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: Optional[str] = None + + +app = FastAPI() + + +@app.post("/items/") +def create_item(item: Item): + return item + + +@app.get("/items/") +def read_items() -> list[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + ), + Item(name="Plumbus"), + ] diff --git a/docs_src/separate_openapi_schemas/tutorial002.py b/docs_src/separate_openapi_schemas/tutorial002.py new file mode 100644 index 0000000000..7df93783b9 --- /dev/null +++ b/docs_src/separate_openapi_schemas/tutorial002.py @@ -0,0 +1,28 @@ +from typing import List, Union + +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + + +app = FastAPI(separate_input_output_schemas=False) + + +@app.post("/items/") +def create_item(item: Item): + return item + + +@app.get("/items/") +def read_items() -> List[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + ), + Item(name="Plumbus"), + ] diff --git a/docs_src/separate_openapi_schemas/tutorial002_py310.py b/docs_src/separate_openapi_schemas/tutorial002_py310.py new file mode 100644 index 0000000000..5db2108729 --- /dev/null +++ b/docs_src/separate_openapi_schemas/tutorial002_py310.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + + +app = FastAPI(separate_input_output_schemas=False) + + +@app.post("/items/") +def create_item(item: Item): + return item + + +@app.get("/items/") +def read_items() -> list[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + ), + Item(name="Plumbus"), + ] diff --git a/docs_src/separate_openapi_schemas/tutorial002_py39.py b/docs_src/separate_openapi_schemas/tutorial002_py39.py new file mode 100644 index 0000000000..50d997d92a --- /dev/null +++ b/docs_src/separate_openapi_schemas/tutorial002_py39.py @@ -0,0 +1,28 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: Optional[str] = None + + +app = FastAPI(separate_input_output_schemas=False) + + +@app.post("/items/") +def create_item(item: Item): + return item + + +@app.get("/items/") +def read_items() -> list[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + ), + Item(name="Plumbus"), + ] diff --git a/docs_src/settings/app01/config.py b/docs_src/settings/app01/config.py index defede9db4..b31b8811d6 100644 --- a/docs_src/settings/app01/config.py +++ b/docs_src/settings/app01/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app02/config.py b/docs_src/settings/app02/config.py index 9a78291356..e17b5035dc 100644 --- a/docs_src/settings/app02/config.py +++ b/docs_src/settings/app02/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app02/main.py b/docs_src/settings/app02/main.py index 163aa26142..941f82e6b3 100644 --- a/docs_src/settings/app02/main.py +++ b/docs_src/settings/app02/main.py @@ -7,7 +7,7 @@ from .config import Settings app = FastAPI() -@lru_cache() +@lru_cache def get_settings(): return Settings() diff --git a/docs/ja/overrides/.gitignore b/docs_src/settings/app02_an/__init__.py similarity index 100% rename from docs/ja/overrides/.gitignore rename to docs_src/settings/app02_an/__init__.py diff --git a/docs_src/settings/app02_an/config.py b/docs_src/settings/app02_an/config.py new file mode 100644 index 0000000000..e17b5035dc --- /dev/null +++ b/docs_src/settings/app02_an/config.py @@ -0,0 +1,7 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 diff --git a/docs_src/settings/app02_an/main.py b/docs_src/settings/app02_an/main.py new file mode 100644 index 0000000000..3a578cc338 --- /dev/null +++ b/docs_src/settings/app02_an/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +from .config import Settings + +app = FastAPI() + + +@lru_cache +def get_settings(): + return Settings() + + +@app.get("/info") +async def info(settings: Annotated[Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/app02_an/test_main.py b/docs_src/settings/app02_an/test_main.py new file mode 100644 index 0000000000..7a04d7e8ee --- /dev/null +++ b/docs_src/settings/app02_an/test_main.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from .config import Settings +from .main import app, get_settings + +client = TestClient(app) + + +def get_settings_override(): + return Settings(admin_email="testing_admin@example.com") + + +app.dependency_overrides[get_settings] = get_settings_override + + +def test_app(): + response = client.get("/info") + data = response.json() + assert data == { + "app_name": "Awesome API", + "admin_email": "testing_admin@example.com", + "items_per_user": 50, + } diff --git a/docs/ko/overrides/.gitignore b/docs_src/settings/app02_an_py39/__init__.py similarity index 100% rename from docs/ko/overrides/.gitignore rename to docs_src/settings/app02_an_py39/__init__.py diff --git a/docs_src/settings/app02_an_py39/config.py b/docs_src/settings/app02_an_py39/config.py new file mode 100644 index 0000000000..e17b5035dc --- /dev/null +++ b/docs_src/settings/app02_an_py39/config.py @@ -0,0 +1,7 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 diff --git a/docs_src/settings/app02_an_py39/main.py b/docs_src/settings/app02_an_py39/main.py new file mode 100644 index 0000000000..6d5db12a87 --- /dev/null +++ b/docs_src/settings/app02_an_py39/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache +from typing import Annotated + +from fastapi import Depends, FastAPI + +from .config import Settings + +app = FastAPI() + + +@lru_cache +def get_settings(): + return Settings() + + +@app.get("/info") +async def info(settings: Annotated[Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/app02_an_py39/test_main.py b/docs_src/settings/app02_an_py39/test_main.py new file mode 100644 index 0000000000..7a04d7e8ee --- /dev/null +++ b/docs_src/settings/app02_an_py39/test_main.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from .config import Settings +from .main import app, get_settings + +client = TestClient(app) + + +def get_settings_override(): + return Settings(admin_email="testing_admin@example.com") + + +app.dependency_overrides[get_settings] = get_settings_override + + +def test_app(): + response = client.get("/info") + data = response.json() + assert data == { + "app_name": "Awesome API", + "admin_email": "testing_admin@example.com", + "items_per_user": 50, + } diff --git a/docs_src/settings/app03/config.py b/docs_src/settings/app03/config.py index e1c3ee3006..942aea3e58 100644 --- a/docs_src/settings/app03/config.py +++ b/docs_src/settings/app03/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app03/main.py b/docs_src/settings/app03/main.py index 69bc8c6e0e..ea64a5709c 100644 --- a/docs_src/settings/app03/main.py +++ b/docs_src/settings/app03/main.py @@ -7,7 +7,7 @@ from . import config app = FastAPI() -@lru_cache() +@lru_cache def get_settings(): return config.Settings() diff --git a/docs/nl/overrides/.gitignore b/docs_src/settings/app03_an/__init__.py similarity index 100% rename from docs/nl/overrides/.gitignore rename to docs_src/settings/app03_an/__init__.py diff --git a/docs_src/settings/app03_an/config.py b/docs_src/settings/app03_an/config.py new file mode 100644 index 0000000000..08f8f88c28 --- /dev/null +++ b/docs_src/settings/app03_an/config.py @@ -0,0 +1,9 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + model_config = SettingsConfigDict(env_file=".env") diff --git a/docs_src/settings/app03_an/config_pv1.py b/docs_src/settings/app03_an/config_pv1.py new file mode 100644 index 0000000000..e1c3ee3006 --- /dev/null +++ b/docs_src/settings/app03_an/config_pv1.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" diff --git a/docs_src/settings/app03_an/main.py b/docs_src/settings/app03_an/main.py new file mode 100644 index 0000000000..2f64b9cd17 --- /dev/null +++ b/docs_src/settings/app03_an/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache +from typing import Annotated + +from fastapi import Depends, FastAPI + +from . import config + +app = FastAPI() + + +@lru_cache +def get_settings(): + return config.Settings() + + +@app.get("/info") +async def info(settings: Annotated[config.Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs/pl/overrides/.gitignore b/docs_src/settings/app03_an_py39/__init__.py similarity index 100% rename from docs/pl/overrides/.gitignore rename to docs_src/settings/app03_an_py39/__init__.py diff --git a/docs_src/settings/app03_an_py39/config.py b/docs_src/settings/app03_an_py39/config.py new file mode 100644 index 0000000000..942aea3e58 --- /dev/null +++ b/docs_src/settings/app03_an_py39/config.py @@ -0,0 +1,10 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" diff --git a/docs_src/settings/app03_an_py39/main.py b/docs_src/settings/app03_an_py39/main.py new file mode 100644 index 0000000000..62f3476396 --- /dev/null +++ b/docs_src/settings/app03_an_py39/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +from . import config + +app = FastAPI() + + +@lru_cache +def get_settings(): + return config.Settings() + + +@app.get("/info") +async def info(settings: Annotated[config.Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/tutorial001.py b/docs_src/settings/tutorial001.py index 0cfd1b6632..d48c4c060c 100644 --- a/docs_src/settings/tutorial001.py +++ b/docs_src/settings/tutorial001.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/tutorial001_pv1.py b/docs_src/settings/tutorial001_pv1.py new file mode 100644 index 0000000000..0cfd1b6632 --- /dev/null +++ b/docs_src/settings/tutorial001_pv1.py @@ -0,0 +1,21 @@ +from fastapi import FastAPI +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + +settings = Settings() +app = FastAPI() + + +@app.get("/info") +async def info(): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/sql_databases/sql_app/models.py b/docs_src/sql_databases/sql_app/models.py index 62d8ab4aaa..09ae2a8077 100644 --- a/docs_src/sql_databases/sql_app/models.py +++ b/docs_src/sql_databases/sql_app/models.py @@ -7,7 +7,7 @@ from .database import Base class User(Base): __tablename__ = "users" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) email = Column(String, unique=True, index=True) hashed_password = Column(String) is_active = Column(Boolean, default=True) @@ -18,7 +18,7 @@ class User(Base): class Item(Base): __tablename__ = "items" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) title = Column(String, index=True) description = Column(String, index=True) owner_id = Column(Integer, ForeignKey("users.id")) diff --git a/docs_src/sql_databases/sql_app/tests/test_sql_app.py b/docs_src/sql_databases/sql_app/tests/test_sql_app.py index c60c3356f8..5f55add0a9 100644 --- a/docs_src/sql_databases/sql_app/tests/test_sql_app.py +++ b/docs_src/sql_databases/sql_app/tests/test_sql_app.py @@ -1,14 +1,17 @@ from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool from ..database import Base from ..main import app, get_db -SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" +SQLALCHEMY_DATABASE_URL = "sqlite://" engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, ) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/docs_src/sql_databases/sql_app_py310/models.py b/docs_src/sql_databases/sql_app_py310/models.py index 62d8ab4aaa..09ae2a8077 100644 --- a/docs_src/sql_databases/sql_app_py310/models.py +++ b/docs_src/sql_databases/sql_app_py310/models.py @@ -7,7 +7,7 @@ from .database import Base class User(Base): __tablename__ = "users" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) email = Column(String, unique=True, index=True) hashed_password = Column(String) is_active = Column(Boolean, default=True) @@ -18,7 +18,7 @@ class User(Base): class Item(Base): __tablename__ = "items" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) title = Column(String, index=True) description = Column(String, index=True) owner_id = Column(Integer, ForeignKey("users.id")) diff --git a/docs_src/sql_databases/sql_app_py39/models.py b/docs_src/sql_databases/sql_app_py39/models.py index 62d8ab4aaa..09ae2a8077 100644 --- a/docs_src/sql_databases/sql_app_py39/models.py +++ b/docs_src/sql_databases/sql_app_py39/models.py @@ -7,7 +7,7 @@ from .database import Base class User(Base): __tablename__ = "users" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) email = Column(String, unique=True, index=True) hashed_password = Column(String) is_active = Column(Boolean, default=True) @@ -18,7 +18,7 @@ class User(Base): class Item(Base): __tablename__ = "items" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) title = Column(String, index=True) description = Column(String, index=True) owner_id = Column(Integer, ForeignKey("users.id")) diff --git a/docs_src/templates/templates/item.html b/docs_src/templates/templates/item.html index a70287e77d..27994ca994 100644 --- a/docs_src/templates/templates/item.html +++ b/docs_src/templates/templates/item.html @@ -4,6 +4,6 @@ -

Item ID: {{ id }}

+

Item ID: {{ id }}

diff --git a/docs_src/templates/tutorial001.py b/docs_src/templates/tutorial001.py index 245e7110b1..81ccc8d4d0 100644 --- a/docs_src/templates/tutorial001.py +++ b/docs_src/templates/tutorial001.py @@ -13,4 +13,6 @@ templates = Jinja2Templates(directory="templates") @app.get("/items/{id}", response_class=HTMLResponse) async def read_item(request: Request, id: str): - return templates.TemplateResponse("item.html", {"request": request, "id": id}) + return templates.TemplateResponse( + request=request, name="item.html", context={"id": id} + ) diff --git a/docs_src/websockets/tutorial002_an.py b/docs_src/websockets/tutorial002_an.py new file mode 100644 index 0000000000..c838fbd306 --- /dev/null +++ b/docs_src/websockets/tutorial002_an.py @@ -0,0 +1,93 @@ +from typing import Union + +from fastapi import ( + Cookie, + Depends, + FastAPI, + Query, + WebSocket, + WebSocketException, + status, +) +from fastapi.responses import HTMLResponse +from typing_extensions import Annotated + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

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

WebSocket Chat

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

WebSocket Chat

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

WebSocket Chat

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

WebSocket Chat

+

Your ID:

+
+ + +
+
    +
+ + + +""" + + +class ConnectionManager: + def __init__(self): + self.active_connections: list[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def send_personal_message(self, message: str, websocket: WebSocket): + await websocket.send_text(message) + + async def broadcast(self, message: str): + for connection in self.active_connections: + await connection.send_text(message) + + +manager = ConnectionManager() + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +@app.websocket("/ws/{client_id}") +async def websocket_endpoint(websocket: WebSocket, client_id: int): + await manager.connect(websocket) + try: + while True: + data = await websocket.receive_text() + await manager.send_personal_message(f"You wrote: {data}", websocket) + await manager.broadcast(f"Client #{client_id} says: {data}") + except WebSocketDisconnect: + manager.disconnect(websocket) + await manager.broadcast(f"Client #{client_id} left the chat") diff --git a/docs_src/wsgi/tutorial001.py b/docs_src/wsgi/tutorial001.py index 500ecf883e..7f27a85a19 100644 --- a/docs_src/wsgi/tutorial001.py +++ b/docs_src/wsgi/tutorial001.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from fastapi.middleware.wsgi import WSGIMiddleware -from flask import Flask, escape, request +from flask import Flask, request +from markupsafe import escape flask_app = Flask(__name__) diff --git a/fastapi/__init__.py b/fastapi/__init__.py index bda10f0436..f457fafd4a 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.89.0" +__version__ = "0.109.0" from starlette import status as status diff --git a/fastapi/_compat.py b/fastapi/_compat.py new file mode 100644 index 0000000000..35d4a87231 --- /dev/null +++ b/fastapi/_compat.py @@ -0,0 +1,634 @@ +from collections import deque +from copy import copy +from dataclasses import dataclass, is_dataclass +from enum import Enum +from typing import ( + Any, + Callable, + Deque, + Dict, + FrozenSet, + List, + Mapping, + Sequence, + Set, + Tuple, + Type, + Union, +) + +from fastapi.exceptions import RequestErrorModel +from fastapi.types import IncEx, ModelNameMap, UnionType +from pydantic import BaseModel, create_model +from pydantic.version import VERSION as PYDANTIC_VERSION +from starlette.datastructures import UploadFile +from typing_extensions import Annotated, Literal, get_args, get_origin + +PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") + + +sequence_annotation_to_type = { + Sequence: list, + List: list, + list: list, + Tuple: tuple, + tuple: tuple, + Set: set, + set: set, + FrozenSet: frozenset, + frozenset: frozenset, + Deque: deque, + deque: deque, +} + +sequence_types = tuple(sequence_annotation_to_type.keys()) + +if PYDANTIC_V2: + from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError + from pydantic import TypeAdapter + from pydantic import ValidationError as ValidationError + from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined] + GetJsonSchemaHandler as GetJsonSchemaHandler, + ) + from pydantic._internal._typing_extra import eval_type_lenient + from pydantic._internal._utils import lenient_issubclass as lenient_issubclass + from pydantic.fields import FieldInfo + from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema + from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue + from pydantic_core import CoreSchema as CoreSchema + from pydantic_core import PydanticUndefined, PydanticUndefinedType + from pydantic_core import Url as Url + + try: + from pydantic_core.core_schema import ( + with_info_plain_validator_function as with_info_plain_validator_function, + ) + except ImportError: # pragma: no cover + from pydantic_core.core_schema import ( + general_plain_validator_function as with_info_plain_validator_function, # noqa: F401 + ) + + Required = PydanticUndefined + Undefined = PydanticUndefined + UndefinedType = PydanticUndefinedType + evaluate_forwardref = eval_type_lenient + Validator = Any + + class BaseConfig: + pass + + class ErrorWrapper(Exception): + pass + + @dataclass + class ModelField: + field_info: FieldInfo + name: str + mode: Literal["validation", "serialization"] = "validation" + + @property + def alias(self) -> str: + a = self.field_info.alias + return a if a is not None else self.name + + @property + def required(self) -> bool: + return self.field_info.is_required() + + @property + def default(self) -> Any: + return self.get_default() + + @property + def type_(self) -> Any: + return self.field_info.annotation + + def __post_init__(self) -> None: + self._type_adapter: TypeAdapter[Any] = TypeAdapter( + Annotated[self.field_info.annotation, self.field_info] + ) + + def get_default(self) -> Any: + if self.field_info.is_required(): + return Undefined + return self.field_info.get_default(call_default_factory=True) + + def validate( + self, + value: Any, + values: Dict[str, Any] = {}, # noqa: B006 + *, + loc: Tuple[Union[int, str], ...] = (), + ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: + try: + return ( + self._type_adapter.validate_python(value, from_attributes=True), + None, + ) + except ValidationError as exc: + return None, _regenerate_error_with_loc( + errors=exc.errors(), loc_prefix=loc + ) + + def serialize( + self, + value: Any, + *, + mode: Literal["json", "python"] = "json", + include: Union[IncEx, None] = None, + exclude: Union[IncEx, None] = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> Any: + # What calls this code passes a value that already called + # self._type_adapter.validate_python(value) + return self._type_adapter.dump_python( + value, + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + def __hash__(self) -> int: + # Each ModelField is unique for our purposes, to allow making a dict from + # ModelField to its JSON Schema. + return id(self) + + def get_annotation_from_field_info( + annotation: Any, field_info: FieldInfo, field_name: str + ) -> Any: + return annotation + + def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: + return errors # type: ignore[return-value] + + def _model_rebuild(model: Type[BaseModel]) -> None: + model.model_rebuild() + + def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any + ) -> Any: + return model.model_dump(mode=mode, **kwargs) + + def _get_model_config(model: BaseModel) -> Any: + return model.model_config + + def get_schema_from_model_field( + *, + field: ModelField, + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + separate_input_output_schemas: bool = True, + ) -> Dict[str, Any]: + override_mode: Union[Literal["validation"], None] = ( + None if separate_input_output_schemas else "validation" + ) + # This expects that GenerateJsonSchema was already used to generate the definitions + json_schema = field_mapping[(field, override_mode or field.mode)] + if "$ref" not in json_schema: + # TODO remove when deprecating Pydantic v1 + # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 + json_schema["title"] = ( + field.field_info.title or field.alias.title().replace("_", " ") + ) + return json_schema + + def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: + return {} + + def get_definitions( + *, + fields: List[ModelField], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, + ) -> Tuple[ + Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + Dict[str, Dict[str, Any]], + ]: + override_mode: Union[Literal["validation"], None] = ( + None if separate_input_output_schemas else "validation" + ) + inputs = [ + (field, override_mode or field.mode, field._type_adapter.core_schema) + for field in fields + ] + field_mapping, definitions = schema_generator.generate_definitions( + inputs=inputs + ) + return field_mapping, definitions # type: ignore[return-value] + + def is_scalar_field(field: ModelField) -> bool: + from fastapi import params + + return field_annotation_is_scalar( + field.field_info.annotation + ) and not isinstance(field.field_info, params.Body) + + def is_sequence_field(field: ModelField) -> bool: + return field_annotation_is_sequence(field.field_info.annotation) + + def is_scalar_sequence_field(field: ModelField) -> bool: + return field_annotation_is_scalar_sequence(field.field_info.annotation) + + def is_bytes_field(field: ModelField) -> bool: + return is_bytes_or_nonable_bytes_annotation(field.type_) + + def is_bytes_sequence_field(field: ModelField) -> bool: + return is_bytes_sequence_annotation(field.type_) + + def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + cls = type(field_info) + merged_field_info = cls.from_annotation(annotation) + new_field_info = copy(field_info) + new_field_info.metadata = merged_field_info.metadata + new_field_info.annotation = merged_field_info.annotation + return new_field_info + + def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + origin_type = ( + get_origin(field.field_info.annotation) or field.field_info.annotation + ) + assert issubclass(origin_type, sequence_types) # type: ignore[arg-type] + return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return] + + def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: + error = ValidationError.from_exception_data( + "Field required", [{"type": "missing", "loc": loc, "input": {}}] + ).errors()[0] + error["input"] = None + return error # type: ignore[return-value] + + def create_body_model( + *, fields: Sequence[ModelField], model_name: str + ) -> Type[BaseModel]: + field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields} + BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] + return BodyModel + +else: + from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX + from pydantic import AnyUrl as Url # noqa: F401 + from pydantic import ( # type: ignore[assignment] + BaseConfig as BaseConfig, # noqa: F401 + ) + from pydantic import ValidationError as ValidationError # noqa: F401 + from pydantic.class_validators import ( # type: ignore[no-redef] + Validator as Validator, # noqa: F401 + ) + from pydantic.error_wrappers import ( # type: ignore[no-redef] + ErrorWrapper as ErrorWrapper, # noqa: F401 + ) + from pydantic.errors import MissingError + from pydantic.fields import ( # type: ignore[attr-defined] + SHAPE_FROZENSET, + SHAPE_LIST, + SHAPE_SEQUENCE, + SHAPE_SET, + SHAPE_SINGLETON, + SHAPE_TUPLE, + SHAPE_TUPLE_ELLIPSIS, + ) + from pydantic.fields import FieldInfo as FieldInfo + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + ModelField as ModelField, # noqa: F401 + ) + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + Required as Required, # noqa: F401 + ) + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + Undefined as Undefined, + ) + from pydantic.fields import ( # type: ignore[no-redef, attr-defined] + UndefinedType as UndefinedType, # noqa: F401 + ) + from pydantic.schema import ( + field_schema, + get_flat_models_from_fields, + get_model_name_map, + model_process_schema, + ) + from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401 + get_annotation_from_field_info as get_annotation_from_field_info, + ) + from pydantic.typing import ( # type: ignore[no-redef] + evaluate_forwardref as evaluate_forwardref, # noqa: F401 + ) + from pydantic.utils import ( # type: ignore[no-redef] + lenient_issubclass as lenient_issubclass, # noqa: F401 + ) + + GetJsonSchemaHandler = Any # type: ignore[assignment,misc] + JsonSchemaValue = Dict[str, Any] # type: ignore[misc] + CoreSchema = Any # type: ignore[assignment,misc] + + sequence_shapes = { + SHAPE_LIST, + SHAPE_SET, + SHAPE_FROZENSET, + SHAPE_TUPLE, + SHAPE_SEQUENCE, + SHAPE_TUPLE_ELLIPSIS, + } + sequence_shape_to_type = { + SHAPE_LIST: list, + SHAPE_SET: set, + SHAPE_TUPLE: tuple, + SHAPE_SEQUENCE: list, + SHAPE_TUPLE_ELLIPSIS: list, + } + + @dataclass + class GenerateJsonSchema: # type: ignore[no-redef] + ref_template: str + + class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef] + pass + + def with_info_plain_validator_function( # type: ignore[misc] + function: Callable[..., Any], + *, + ref: Union[str, None] = None, + metadata: Any = None, + serialization: Any = None, + ) -> Any: + return {} + + def get_model_definitions( + *, + flat_models: Set[Union[Type[BaseModel], Type[Enum]]], + model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], + ) -> Dict[str, Any]: + definitions: Dict[str, Dict[str, Any]] = {} + for model in flat_models: + m_schema, m_definitions, m_nested_models = model_process_schema( + model, model_name_map=model_name_map, ref_prefix=REF_PREFIX + ) + definitions.update(m_definitions) + model_name = model_name_map[model] + if "description" in m_schema: + m_schema["description"] = m_schema["description"].split("\f")[0] + definitions[model_name] = m_schema + return definitions + + def is_pv1_scalar_field(field: ModelField) -> bool: + from fastapi import params + + field_info = field.field_info + if not ( + field.shape == SHAPE_SINGLETON # type: ignore[attr-defined] + and not lenient_issubclass(field.type_, BaseModel) + and not lenient_issubclass(field.type_, dict) + and not field_annotation_is_sequence(field.type_) + and not is_dataclass(field.type_) + and not isinstance(field_info, params.Body) + ): + return False + if field.sub_fields: # type: ignore[attr-defined] + if not all( + is_pv1_scalar_field(f) + for f in field.sub_fields # type: ignore[attr-defined] + ): + return False + return True + + def is_pv1_scalar_sequence_field(field: ModelField) -> bool: + if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined] + field.type_, BaseModel + ): + if field.sub_fields is not None: # type: ignore[attr-defined] + for sub_field in field.sub_fields: # type: ignore[attr-defined] + if not is_pv1_scalar_field(sub_field): + return False + return True + if _annotation_is_sequence(field.type_): + return True + return False + + def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: + use_errors: List[Any] = [] + for error in errors: + if isinstance(error, ErrorWrapper): + new_errors = ValidationError( # type: ignore[call-arg] + errors=[error], model=RequestErrorModel + ).errors() + use_errors.extend(new_errors) + elif isinstance(error, list): + use_errors.extend(_normalize_errors(error)) + else: + use_errors.append(error) + return use_errors + + def _model_rebuild(model: Type[BaseModel]) -> None: + model.update_forward_refs() + + def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any + ) -> Any: + return model.dict(**kwargs) + + def _get_model_config(model: BaseModel) -> Any: + return model.__config__ # type: ignore[attr-defined] + + def get_schema_from_model_field( + *, + field: ModelField, + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + separate_input_output_schemas: bool = True, + ) -> Dict[str, Any]: + # This expects that GenerateJsonSchema was already used to generate the definitions + return field_schema( # type: ignore[no-any-return] + field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + )[0] + + def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: + models = get_flat_models_from_fields(fields, known_models=set()) + return get_model_name_map(models) # type: ignore[no-any-return] + + def get_definitions( + *, + fields: List[ModelField], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, + ) -> Tuple[ + Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + Dict[str, Dict[str, Any]], + ]: + models = get_flat_models_from_fields(fields, known_models=set()) + return {}, get_model_definitions( + flat_models=models, model_name_map=model_name_map + ) + + def is_scalar_field(field: ModelField) -> bool: + return is_pv1_scalar_field(field) + + def is_sequence_field(field: ModelField) -> bool: + return field.shape in sequence_shapes or _annotation_is_sequence(field.type_) # type: ignore[attr-defined] + + def is_scalar_sequence_field(field: ModelField) -> bool: + return is_pv1_scalar_sequence_field(field) + + def is_bytes_field(field: ModelField) -> bool: + return lenient_issubclass(field.type_, bytes) + + def is_bytes_sequence_field(field: ModelField) -> bool: + return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined] + + def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + return copy(field_info) + + def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined] + + def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: + missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg] + new_error = ValidationError([missing_field_error], RequestErrorModel) + return new_error.errors()[0] # type: ignore[return-value] + + def create_body_model( + *, fields: Sequence[ModelField], model_name: str + ) -> Type[BaseModel]: + BodyModel = create_model(model_name) + for f in fields: + BodyModel.__fields__[f.name] = f # type: ignore[index] + return BodyModel + + +def _regenerate_error_with_loc( + *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...] +) -> List[Dict[str, Any]]: + updated_loc_errors: List[Any] = [ + {**err, "loc": loc_prefix + err.get("loc", ())} + for err in _normalize_errors(errors) + ] + + return updated_loc_errors + + +def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + if lenient_issubclass(annotation, (str, bytes)): + return False + return lenient_issubclass(annotation, sequence_types) + + +def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + return _annotation_is_sequence(annotation) or _annotation_is_sequence( + get_origin(annotation) + ) + + +def value_is_sequence(value: Any) -> bool: + return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type] + + +def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: + return ( + lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile)) + or _annotation_is_sequence(annotation) + or is_dataclass(annotation) + ) + + +def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + return any(field_annotation_is_complex(arg) for arg in get_args(annotation)) + + return ( + _annotation_is_complex(annotation) + or _annotation_is_complex(origin) + or hasattr(origin, "__pydantic_core_schema__") + or hasattr(origin, "__get_pydantic_core_schema__") + ) + + +def field_annotation_is_scalar(annotation: Any) -> bool: + # handle Ellipsis here to make tuple[int, ...] work nicely + return annotation is Ellipsis or not field_annotation_is_complex(annotation) + + +def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one_scalar_sequence = False + for arg in get_args(annotation): + if field_annotation_is_scalar_sequence(arg): + at_least_one_scalar_sequence = True + continue + elif not field_annotation_is_scalar(arg): + return False + return at_least_one_scalar_sequence + return field_annotation_is_sequence(annotation) and all( + field_annotation_is_scalar(sub_annotation) + for sub_annotation in get_args(annotation) + ) + + +def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool: + if lenient_issubclass(annotation, bytes): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, bytes): + return True + return False + + +def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool: + if lenient_issubclass(annotation, UploadFile): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, UploadFile): + return True + return False + + +def is_bytes_sequence_annotation(annotation: Any) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one = False + for arg in get_args(annotation): + if is_bytes_sequence_annotation(arg): + at_least_one = True + continue + return at_least_one + return field_annotation_is_sequence(annotation) and all( + is_bytes_or_nonable_bytes_annotation(sub_annotation) + for sub_annotation in get_args(annotation) + ) + + +def is_uploadfile_sequence_annotation(annotation: Any) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one = False + for arg in get_args(annotation): + if is_uploadfile_sequence_annotation(arg): + at_least_one = True + continue + return at_least_one + return field_annotation_is_sequence(annotation) and all( + is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation) + for sub_annotation in get_args(annotation) + ) diff --git a/fastapi/applications.py b/fastapi/applications.py index 36dc2605d5..597c60a567 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -9,19 +9,19 @@ from typing import ( Optional, Sequence, Type, + TypeVar, Union, ) from fastapi import routing from fastapi.datastructures import Default, DefaultPlaceholder -from fastapi.encoders import DictIntStrAny, SetIntStr from fastapi.exception_handlers import ( http_exception_handler, request_validation_exception_handler, + websocket_request_validation_exception_handler, ) -from fastapi.exceptions import RequestValidationError +from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError from fastapi.logger import logger -from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware from fastapi.openapi.docs import ( get_redoc_html, get_swagger_ui_html, @@ -29,65 +29,800 @@ from fastapi.openapi.docs import ( ) from fastapi.openapi.utils import get_openapi from fastapi.params import Depends -from fastapi.types import DecoratedCallable +from fastapi.types import DecoratedCallable, IncEx from fastapi.utils import generate_unique_id from starlette.applications import Starlette from starlette.datastructures import State from starlette.exceptions import HTTPException from starlette.middleware import Middleware -from starlette.middleware.errors import ServerErrorMiddleware -from starlette.middleware.exceptions import ExceptionMiddleware +from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse, Response from starlette.routing import BaseRoute -from starlette.types import ASGIApp, Receive, Scope, Send +from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send +from typing_extensions import Annotated, Doc, deprecated # type: ignore [attr-defined] + +AppType = TypeVar("AppType", bound="FastAPI") class FastAPI(Starlette): + """ + `FastAPI` app class, the main entrypoint to use FastAPI. + + Read more in the + [FastAPI docs for First Steps](https://fastapi.tiangolo.com/tutorial/first-steps/). + + ## Example + + ```python + from fastapi import FastAPI + + app = FastAPI() + ``` + """ + def __init__( - self, + self: AppType, *, - debug: bool = False, - routes: Optional[List[BaseRoute]] = None, - title: str = "FastAPI", - description: str = "", - version: str = "0.1.0", - openapi_url: Optional[str] = "/openapi.json", - openapi_tags: Optional[List[Dict[str, Any]]] = None, - servers: Optional[List[Dict[str, Union[str, Any]]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - default_response_class: Type[Response] = Default(JSONResponse), - docs_url: Optional[str] = "/docs", - redoc_url: Optional[str] = "/redoc", - swagger_ui_oauth2_redirect_url: Optional[str] = "/docs/oauth2-redirect", - swagger_ui_init_oauth: Optional[Dict[str, Any]] = None, - middleware: Optional[Sequence[Middleware]] = None, - exception_handlers: Optional[ - Dict[ - Union[int, Type[Exception]], - Callable[[Request, Any], Coroutine[Any, Any, Response]], - ] + debug: Annotated[ + bool, + Doc( + """ + Boolean indicating if debug tracebacks should be returned on server + errors. + + Read more in the + [Starlette docs for Applications](https://www.starlette.io/applications/#instantiating-the-application). + """ + ), + ] = False, + routes: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + **Note**: you probably shouldn't use this parameter, it is inherited + from Starlette and supported for compatibility. + + --- + + A list of routes to serve incoming HTTP and WebSocket requests. + """ + ), + deprecated( + """ + You normally wouldn't use this parameter with FastAPI, it is inherited + from Starlette and supported for compatibility. + + In FastAPI, you normally would use the *path operation methods*, + like `app.get()`, `app.post()`, etc. + """ + ), ] = None, - on_startup: Optional[Sequence[Callable[[], Any]]] = None, - on_shutdown: Optional[Sequence[Callable[[], Any]]] = None, - terms_of_service: Optional[str] = None, - contact: Optional[Dict[str, Union[str, Any]]] = None, - license_info: Optional[Dict[str, Union[str, Any]]] = None, - openapi_prefix: str = "", - root_path: str = "", - root_path_in_servers: bool = True, - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - callbacks: Optional[List[BaseRoute]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - swagger_ui_parameters: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), - **extra: Any, + title: Annotated[ + str, + Doc( + """ + The title of the API. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(title="ChimichangApp") + ``` + """ + ), + ] = "FastAPI", + summary: Annotated[ + Optional[str], + Doc( + """ + A short summary of the API. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(summary="Deadpond's favorite app. Nuff said.") + ``` + """ + ), + ] = None, + description: Annotated[ + str, + Doc( + ''' + A description of the API. Supports Markdown (using + [CommonMark syntax](https://commonmark.org/)). + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI( + description=""" + ChimichangApp API helps you do awesome stuff. 🚀 + + ## Items + + You can **read items**. + + ## Users + + You will be able to: + + * **Create users** (_not implemented_). + * **Read users** (_not implemented_). + + """ + ) + ``` + ''' + ), + ] = "", + version: Annotated[ + str, + Doc( + """ + The version of the API. + + **Note** This is the version of your application, not the version of + the OpenAPI specification nor the version of FastAPI being used. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(version="0.0.1") + ``` + """ + ), + ] = "0.1.0", + openapi_url: Annotated[ + Optional[str], + Doc( + """ + The URL where the OpenAPI schema will be served from. + + If you set it to `None`, no OpenAPI schema will be served publicly, and + the default automatic endpoints `/docs` and `/redoc` will also be + disabled. + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#openapi-url). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(openapi_url="/api/v1/openapi.json") + ``` + """ + ), + ] = "/openapi.json", + openapi_tags: Annotated[ + Optional[List[Dict[str, Any]]], + Doc( + """ + A list of tags used by OpenAPI, these are the same `tags` you can set + in the *path operations*, like: + + * `@app.get("/users/", tags=["users"])` + * `@app.get("/items/", tags=["items"])` + + The order of the tags can be used to specify the order shown in + tools like Swagger UI, used in the automatic path `/docs`. + + It's not required to specify all the tags used. + + The tags that are not declared MAY be organized randomly or based + on the tools' logic. Each tag name in the list MUST be unique. + + The value of each item is a `dict` containing: + + * `name`: The name of the tag. + * `description`: A short description of the tag. + [CommonMark syntax](https://commonmark.org/) MAY be used for rich + text representation. + * `externalDocs`: Additional external documentation for this tag. If + provided, it would contain a `dict` with: + * `description`: A short description of the target documentation. + [CommonMark syntax](https://commonmark.org/) MAY be used for + rich text representation. + * `url`: The URL for the target documentation. Value MUST be in + the form of a URL. + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-tags). + + **Example** + + ```python + 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) + ``` + """ + ), + ] = None, + servers: Annotated[ + Optional[List[Dict[str, Union[str, Any]]]], + Doc( + """ + A `list` of `dict`s with connectivity information to a target server. + + You would use it, for example, if your application is served from + different domains and you want to use the same Swagger UI in the + browser to interact with each of them (instead of having multiple + browser tabs open). Or if you want to leave fixed the possible URLs. + + If the servers `list` is not provided, or is an empty `list`, the + default value would be a a `dict` with a `url` value of `/`. + + Each item in the `list` is a `dict` containing: + + * `url`: A URL to the target host. This URL supports Server Variables + and MAY be relative, to indicate that the host location is relative + to the location where the OpenAPI document is being served. Variable + substitutions will be made when a variable is named in `{`brackets`}`. + * `description`: An optional string describing the host designated by + the URL. [CommonMark syntax](https://commonmark.org/) MAY be used for + rich text representation. + * `variables`: A `dict` between a variable name and its value. The value + is used for substitution in the server's URL template. + + Read more in the + [FastAPI docs for Behind a Proxy](https://fastapi.tiangolo.com/advanced/behind-a-proxy/#additional-servers). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI( + servers=[ + {"url": "https://stag.example.com", "description": "Staging environment"}, + {"url": "https://prod.example.com", "description": "Production environment"}, + ] + ) + ``` + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of global dependencies, they will be applied to each + *path operation*, including in sub-routers. + + Read more about it in the + [FastAPI docs for Global Dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/global-dependencies/). + + **Example** + + ```python + from fastapi import Depends, FastAPI + + from .dependencies import func_dep_1, func_dep_2 + + app = FastAPI(dependencies=[Depends(func_dep_1), Depends(func_dep_2)]) + ``` + """ + ), + ] = None, + default_response_class: Annotated[ + Type[Response], + Doc( + """ + The default response class to be used. + + Read more in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). + + **Example** + + ```python + from fastapi import FastAPI + from fastapi.responses import ORJSONResponse + + app = FastAPI(default_response_class=ORJSONResponse) + ``` + """ + ), + ] = Default(JSONResponse), + redirect_slashes: Annotated[ + bool, + Doc( + """ + Whether to detect and redirect slashes in URLs when the client doesn't + use the same format. + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(redirect_slashes=True) # the default + + @app.get("/items/") + async def read_items(): + return [{"item_id": "Foo"}] + ``` + + With this app, if a client goes to `/items` (without a trailing slash), + they will be automatically redirected with an HTTP status code of 307 + to `/items/`. + """ + ), + ] = True, + docs_url: Annotated[ + Optional[str], + Doc( + """ + The path to the automatic interactive API documentation. + It is handled in the browser by Swagger UI. + + The default URL is `/docs`. You can disable it by setting it to `None`. + + If `openapi_url` is set to `None`, this will be automatically disabled. + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#docs-urls). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(docs_url="/documentation", redoc_url=None) + ``` + """ + ), + ] = "/docs", + redoc_url: Annotated[ + Optional[str], + Doc( + """ + The path to the alternative automatic interactive API documentation + provided by ReDoc. + + The default URL is `/redoc`. You can disable it by setting it to `None`. + + If `openapi_url` is set to `None`, this will be automatically disabled. + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#docs-urls). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(docs_url="/documentation", redoc_url="redocumentation") + ``` + """ + ), + ] = "/redoc", + swagger_ui_oauth2_redirect_url: Annotated[ + Optional[str], + Doc( + """ + The OAuth2 redirect endpoint for the Swagger UI. + + By default it is `/docs/oauth2-redirect`. + + This is only used if you use OAuth2 (with the "Authorize" button) + with Swagger UI. + """ + ), + ] = "/docs/oauth2-redirect", + swagger_ui_init_oauth: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + OAuth2 configuration for the Swagger UI, by default shown at `/docs`. + + Read more about the available configuration options in the + [Swagger UI docs](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/). + """ + ), + ] = None, + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to be added when creating the application. + + In FastAPI you would normally do this with `app.add_middleware()` + instead. + + Read more in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/tutorial/middleware/). + """ + ), + ] = None, + exception_handlers: Annotated[ + Optional[ + Dict[ + Union[int, Type[Exception]], + Callable[[Request, Any], Coroutine[Any, Any, Response]], + ] + ], + Doc( + """ + A dictionary with handlers for exceptions. + + In FastAPI, you would normally use the decorator + `@app.exception_handler()`. + + Read more in the + [FastAPI docs for Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/). + """ + ), + ] = None, + on_startup: Annotated[ + Optional[Sequence[Callable[[], Any]]], + Doc( + """ + A list of startup event handler functions. + + You should instead use the `lifespan` handlers. + + Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """ + ), + ] = None, + on_shutdown: Annotated[ + Optional[Sequence[Callable[[], Any]]], + Doc( + """ + A list of shutdown event handler functions. + + You should instead use the `lifespan` handlers. + + Read more in the + [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """ + ), + ] = None, + lifespan: Annotated[ + Optional[Lifespan[AppType]], + Doc( + """ + A `Lifespan` context manager handler. This replaces `startup` and + `shutdown` functions with a single context manager. + + Read more in the + [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """ + ), + ] = None, + terms_of_service: Annotated[ + Optional[str], + Doc( + """ + A URL to the Terms of Service for your API. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more at the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + app = FastAPI(terms_of_service="http://example.com/terms/") + ``` + """ + ), + ] = None, + contact: Annotated[ + Optional[Dict[str, Union[str, Any]]], + Doc( + """ + A dictionary with the contact information for the exposed API. + + It can contain several fields. + + * `name`: (`str`) The name of the contact person/organization. + * `url`: (`str`) A URL pointing to the contact information. MUST be in + the format of a URL. + * `email`: (`str`) The email address of the contact person/organization. + MUST be in the format of an email address. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more at the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + app = FastAPI( + contact={ + "name": "Deadpoolio the Amazing", + "url": "http://x-force.example.com/contact/", + "email": "dp@x-force.example.com", + } + ) + ``` + """ + ), + ] = None, + license_info: Annotated[ + Optional[Dict[str, Union[str, Any]]], + Doc( + """ + A dictionary with the license information for the exposed API. + + It can contain several fields. + + * `name`: (`str`) **REQUIRED** (if a `license_info` is set). The + license name used for the API. + * `identifier`: (`str`) An [SPDX](https://spdx.dev/) license expression + for the API. The `identifier` field is mutually exclusive of the `url` + field. Available since OpenAPI 3.1.0, FastAPI 0.99.0. + * `url`: (`str`) A URL to the license used for the API. This MUST be + the format of a URL. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more at the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + app = FastAPI( + license_info={ + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + } + ) + ``` + """ + ), + ] = None, + openapi_prefix: Annotated[ + str, + Doc( + """ + A URL prefix for the OpenAPI URL. + """ + ), + deprecated( + """ + "openapi_prefix" has been deprecated in favor of "root_path", which + follows more closely the ASGI standard, is simpler, and more + automatic. + """ + ), + ] = "", + root_path: Annotated[ + str, + Doc( + """ + A path prefix handled by a proxy that is not seen by the application + but is seen by external clients, which affects things like Swagger UI. + + Read more about it at the + [FastAPI docs for Behind a Proxy](https://fastapi.tiangolo.com/advanced/behind-a-proxy/). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(root_path="/api/v1") + ``` + """ + ), + ] = "", + root_path_in_servers: Annotated[ + bool, + Doc( + """ + To disable automatically generating the URLs in the `servers` field + in the autogenerated OpenAPI using the `root_path`. + + Read more about it in the + [FastAPI docs for Behind a Proxy](https://fastapi.tiangolo.com/advanced/behind-a-proxy/#disable-automatic-server-from-root_path). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(root_path_in_servers=False) + ``` + """ + ), + ] = True, + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses to be shown in OpenAPI. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/). + + And in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + OpenAPI callbacks that should apply to all *path operations*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + webhooks: Annotated[ + Optional[routing.APIRouter], + Doc( + """ + Add OpenAPI webhooks. This is similar to `callbacks` but it doesn't + depend on specific *path operations*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + **Note**: This is available since OpenAPI 3.1.0, FastAPI 0.99.0. + + Read more about it in the + [FastAPI docs for OpenAPI Webhooks](https://fastapi.tiangolo.com/advanced/openapi-webhooks/). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark all *path operations* as deprecated. You probably don't need it, + but it's available. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) all the *path operations* in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + swagger_ui_parameters: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Parameters to configure Swagger UI, the autogenerated interactive API + documentation (by default at `/docs`). + + Read more about it in the + [FastAPI docs about how to Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), + separate_input_output_schemas: Annotated[ + bool, + Doc( + """ + Whether to generate separate OpenAPI schemas for request body and + response body when the results would be more precise. + + This is particularly useful when automatically generating clients. + + For example, if you have a model like: + + ```python + from pydantic import BaseModel + + class Item(BaseModel): + name: str + tags: list[str] = [] + ``` + + When `Item` is used for input, a request body, `tags` is not required, + the client doesn't have to provide it. + + But when using `Item` for output, for a response body, `tags` is always + available because it has a default value, even if it's just an empty + list. So, the client should be able to always expect it. + + In this case, there would be two different schemas, one for input and + another one for output. + """ + ), + ] = True, + **extra: Annotated[ + Any, + Doc( + """ + Extra keyword arguments to be stored in the app, not used by FastAPI + anywhere. + """ + ), + ], ) -> None: - self._debug: bool = debug + self.debug = debug self.title = title + self.summary = summary self.description = description self.version = version self.terms_of_service = terms_of_service @@ -102,8 +837,39 @@ class FastAPI(Starlette): self.swagger_ui_init_oauth = swagger_ui_init_oauth self.swagger_ui_parameters = swagger_ui_parameters self.servers = servers or [] + self.separate_input_output_schemas = separate_input_output_schemas self.extra = extra - self.openapi_version = "3.0.2" + self.openapi_version: Annotated[ + str, + Doc( + """ + The version string of OpenAPI. + + FastAPI will generate OpenAPI version 3.1.0, and will output that as + the OpenAPI version. But some tools, even though they might be + compatible with OpenAPI 3.1.0, might not recognize it as a valid. + + So you could override this value to trick those tools into using + the generated OpenAPI. Have in mind that this is a hack. But if you + avoid using features added in OpenAPI 3.1.0, it might work for your + use case. + + This is not passed as a parameter to the `FastAPI` class to avoid + giving the false idea that FastAPI would generate a different OpenAPI + schema. It is only available as an attribute. + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI() + + app.openapi_version = "3.0.2" + ``` + """ + ), + ] = "3.1.0" self.openapi_schema: Optional[Dict[str, Any]] = None if self.openapi_url: assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'" @@ -116,14 +882,60 @@ class FastAPI(Starlette): "automatic. Check the docs at " "https://fastapi.tiangolo.com/advanced/sub-applications/" ) + self.webhooks: Annotated[ + routing.APIRouter, + Doc( + """ + The `app.webhooks` attribute is an `APIRouter` with the *path + operations* that will be used just for documentation of webhooks. + + Read more about it in the + [FastAPI docs for OpenAPI Webhooks](https://fastapi.tiangolo.com/advanced/openapi-webhooks/). + """ + ), + ] = webhooks or routing.APIRouter() self.root_path = root_path or openapi_prefix - self.state: State = State() - self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {} + self.state: Annotated[ + State, + Doc( + """ + A state object for the application. This is the same object for the + entire application, it doesn't change from request to request. + + You normally woudln't use this in FastAPI, for most of the cases you + would instead use FastAPI dependencies. + + This is simply inherited from Starlette. + + Read more about it in the + [Starlette docs for Applications](https://www.starlette.io/applications/#storing-state-on-the-app-instance). + """ + ), + ] = State() + self.dependency_overrides: Annotated[ + Dict[Callable[..., Any], Callable[..., Any]], + Doc( + """ + A dictionary with overrides for the dependencies. + + Each key is the original dependency callable, and the value is the + actual dependency that should be called. + + This is for testing, to replace expensive dependencies with testing + versions. + + Read more about it in the + [FastAPI docs for Testing Dependencies with Overrides](https://fastapi.tiangolo.com/advanced/testing-dependencies/). + """ + ), + ] = {} self.router: routing.APIRouter = routing.APIRouter( routes=routes, + redirect_slashes=redirect_slashes, dependency_overrides_provider=self, on_startup=on_startup, on_shutdown=on_shutdown, + lifespan=lifespan, default_response_class=default_response_class, dependencies=dependencies, callbacks=callbacks, @@ -134,80 +946,52 @@ class FastAPI(Starlette): ) self.exception_handlers: Dict[ Any, Callable[[Request, Any], Union[Response, Awaitable[Response]]] - ] = ({} if exception_handlers is None else dict(exception_handlers)) + ] = {} if exception_handlers is None else dict(exception_handlers) self.exception_handlers.setdefault(HTTPException, http_exception_handler) self.exception_handlers.setdefault( RequestValidationError, request_validation_exception_handler ) + self.exception_handlers.setdefault( + WebSocketRequestValidationError, + # Starlette still has incorrect type specification for the handlers + websocket_request_validation_exception_handler, # type: ignore + ) self.user_middleware: List[Middleware] = ( [] if middleware is None else list(middleware) ) - self.middleware_stack: ASGIApp = self.build_middleware_stack() + self.middleware_stack: Union[ASGIApp, None] = None self.setup() - def build_middleware_stack(self) -> ASGIApp: - # Duplicate/override from Starlette to add AsyncExitStackMiddleware - # inside of ExceptionMiddleware, inside of custom user middlewares - debug = self.debug - error_handler = None - exception_handlers = {} - - for key, value in self.exception_handlers.items(): - if key in (500, Exception): - error_handler = value - else: - exception_handlers[key] = value - - middleware = ( - [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)] - + self.user_middleware - + [ - Middleware( - ExceptionMiddleware, handlers=exception_handlers, debug=debug - ), - # Add FastAPI-specific AsyncExitStackMiddleware for dependencies with - # contextvars. - # This needs to happen after user middlewares because those create a - # new contextvars context copy by using a new AnyIO task group. - # The initial part of dependencies with yield is executed in the - # FastAPI code, inside all the middlewares, but the teardown part - # (after yield) is executed in the AsyncExitStack in this middleware, - # if the AsyncExitStack lived outside of the custom middlewares and - # contextvars were set in a dependency with yield in that internal - # contextvars context, the values would not be available in the - # outside context of the AsyncExitStack. - # By putting the middleware and the AsyncExitStack here, inside all - # user middlewares, the code before and after yield in dependencies - # with yield is executed in the same contextvars context, so all values - # set in contextvars before yield is still available after yield as - # would be expected. - # Additionally, by having this AsyncExitStack here, after the - # ExceptionMiddleware, now dependencies can catch handled exceptions, - # e.g. HTTPException, to customize the teardown code (e.g. DB session - # rollback). - Middleware(AsyncExitStackMiddleware), - ] - ) - - app = self.router - for cls, options in reversed(middleware): - app = cls(app=app, **options) - return app - def openapi(self) -> Dict[str, Any]: + """ + Generate the OpenAPI schema of the application. This is called by FastAPI + internally. + + The first time it is called it stores the result in the attribute + `app.openapi_schema`, and next times it is called, it just returns that same + result. To avoid the cost of generating the schema every time. + + If you need to modify the generated OpenAPI schema, you could modify it. + + Read more in the + [FastAPI docs for OpenAPI](https://fastapi.tiangolo.com/how-to/extending-openapi/). + """ if not self.openapi_schema: self.openapi_schema = get_openapi( title=self.title, version=self.version, openapi_version=self.openapi_version, + summary=self.summary, description=self.description, terms_of_service=self.terms_of_service, contact=self.contact, license_info=self.license_info, routes=self.routes, + webhooks=self.webhooks.routes, tags=self.openapi_tags, servers=self.servers, + separate_input_output_schemas=self.separate_input_output_schemas, ) return self.openapi_schema @@ -285,8 +1069,8 @@ class FastAPI(Starlette): deprecated: Optional[bool] = None, methods: Optional[List[str]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -343,8 +1127,8 @@ class FastAPI(Starlette): deprecated: Optional[bool] = None, methods: Optional[List[str]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -389,35 +1173,277 @@ class FastAPI(Starlette): return decorator def add_api_websocket_route( - self, path: str, endpoint: Callable[..., Any], name: Optional[str] = None + self, + path: str, + endpoint: Callable[..., Any], + name: Optional[str] = None, + *, + dependencies: Optional[Sequence[Depends]] = None, ) -> None: - self.router.add_api_websocket_route(path, endpoint, name=name) + self.router.add_api_websocket_route( + path, + endpoint, + name=name, + dependencies=dependencies, + ) def websocket( - self, path: str, name: Optional[str] = None + self, + path: Annotated[ + str, + Doc( + """ + WebSocket path. + """ + ), + ], + name: Annotated[ + Optional[str], + Doc( + """ + A name for the WebSocket. Only used internally. + """ + ), + ] = None, + *, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be used for this + WebSocket. + + Read more about it in the + [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/). + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Decorate a WebSocket function. + + Read more about it in the + [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/). + + **Example** + + ```python + from fastapi import FastAPI, WebSocket + + app = FastAPI() + + @app.websocket("/ws") + async def websocket_endpoint(websocket: WebSocket): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text(f"Message text was: {data}") + ``` + """ + def decorator(func: DecoratedCallable) -> DecoratedCallable: - self.add_api_websocket_route(path, func, name=name) + self.add_api_websocket_route( + path, + func, + name=name, + dependencies=dependencies, + ) return func return decorator def include_router( self, - router: routing.APIRouter, + router: Annotated[routing.APIRouter, Doc("The `APIRouter` to include.")], *, - prefix: str = "", - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - default_response_class: Type[Response] = Default(JSONResponse), - callbacks: Optional[List[BaseRoute]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "", + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to all the *path operations* in this + router. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to all the + *path operations* in this router. + + Read more about it in the + [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + + **Example** + + ```python + from fastapi import Depends, FastAPI + + from .dependencies import get_token_header + from .internal import admin + + app = FastAPI() + + app.include_router( + admin.router, + dependencies=[Depends(get_token_header)], + ) + ``` + """ + ), + ] = None, + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses to be shown in OpenAPI. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/). + + And in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark all the *path operations* in this router as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + **Example** + + ```python + from fastapi import FastAPI + + from .internal import old_api + + app = FastAPI() + + app.include_router( + old_api.router, + deprecated=True, + ) + ``` + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include (or not) all the *path operations* in this router in the + generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + **Example** + + ```python + from fastapi import FastAPI + + from .internal import old_api + + app = FastAPI() + + app.include_router( + old_api.router, + include_in_schema=False, + ) + ``` + """ + ), + ] = True, + default_response_class: Annotated[ + Type[Response], + Doc( + """ + Default response class to be used for the *path operations* in this + router. + + Read more in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). + + **Example** + + ```python + from fastapi import FastAPI + from fastapi.responses import ORJSONResponse + + from .internal import old_api + + app = FastAPI() + + app.include_router( + old_api.router, + default_response_class=ORJSONResponse, + ) + ``` + """ + ), + ] = Default(JSONResponse), + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> None: + """ + Include an `APIRouter` in the same app. + + Read more about it in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/). + + ## Example + + ```python + from fastapi import FastAPI + + from .users import users_router + + app = FastAPI() + + app.include_router(users_router) + ``` + """ self.router.include_router( router, prefix=prefix, @@ -433,33 +1459,351 @@ class FastAPI(Starlette): def get( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP GET operation. + + ## Example + + ```python + from fastapi import FastAPI + + app = FastAPI() + + @app.get("/items/") + def read_items(): + return [{"name": "Empanada"}, {"name": "Arepa"}] + ``` + """ return self.router.get( path, response_model=response_model, @@ -488,33 +1832,356 @@ class FastAPI(Starlette): def put( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP PUT operation. + + ## Example + + ```python + from fastapi import FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + + @app.put("/items/{item_id}") + def replace_item(item_id: str, item: Item): + return {"message": "Item replaced", "id": item_id} + ``` + """ return self.router.put( path, response_model=response_model, @@ -543,33 +2210,356 @@ class FastAPI(Starlette): def post( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP POST operation. + + ## Example + + ```python + from fastapi import FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + + @app.post("/items/") + def create_item(item: Item): + return {"message": "Item created"} + ``` + """ return self.router.post( path, response_model=response_model, @@ -598,33 +2588,351 @@ class FastAPI(Starlette): def delete( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP DELETE operation. + + ## Example + + ```python + from fastapi import FastAPI + + app = FastAPI() + + @app.delete("/items/{item_id}") + def delete_item(item_id: str): + return {"message": "Item deleted"} + ``` + """ return self.router.delete( path, response_model=response_model, @@ -653,33 +2961,351 @@ class FastAPI(Starlette): def options( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP OPTIONS operation. + + ## Example + + ```python + from fastapi import FastAPI + + app = FastAPI() + + @app.options("/items/") + def get_item_options(): + return {"additions": ["Aji", "Guacamole"]} + ``` + """ return self.router.options( path, response_model=response_model, @@ -708,33 +3334,351 @@ class FastAPI(Starlette): def head( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP HEAD operation. + + ## Example + + ```python + from fastapi import FastAPI, Response + + app = FastAPI() + + @app.head("/items/", status_code=204) + def get_items_headers(response: Response): + response.headers["X-Cat-Dog"] = "Alone in the world" + ``` + """ return self.router.head( path, response_model=response_model, @@ -763,33 +3707,356 @@ class FastAPI(Starlette): def patch( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP PATCH operation. + + ## Example + + ```python + from fastapi import FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + + @app.patch("/items/") + def update_item(item: Item): + return {"message": "Item updated in place"} + ``` + """ return self.router.patch( path, response_model=response_model, @@ -818,33 +4085,351 @@ class FastAPI(Starlette): def trace( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP TRACE operation. + + ## Example + + ```python + from fastapi import FastAPI + + app = FastAPI() + + @app.put("/items/{item_id}") + def trace_item(item_id: str): + return None + ``` + """ return self.router.trace( path, response_model=response_model, @@ -870,3 +4455,131 @@ class FastAPI(Starlette): openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, ) + + def websocket_route( + self, path: str, name: Union[str, None] = None + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.router.add_websocket_route(path, func, name=name) + return func + + return decorator + + @deprecated( + """ + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + """ + ) + def on_event( + self, + event_type: Annotated[ + str, + Doc( + """ + The type of event. `startup` or `shutdown`. + """ + ), + ], + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add an event handler for the application. + + `on_event` is deprecated, use `lifespan` event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/#alternative-events-deprecated). + """ + return self.router.on_event(event_type) + + def middleware( + self, + middleware_type: Annotated[ + str, + Doc( + """ + The type of middleware. Currently only supports `http`. + """ + ), + ], + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a middleware to the application. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/tutorial/middleware/). + + ## Example + + ```python + import time + + from fastapi import FastAPI, Request + + app = FastAPI() + + + @app.middleware("http") + async def add_process_time_header(request: Request, call_next): + start_time = time.time() + response = await call_next(request) + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response + ``` + """ + + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_middleware(BaseHTTPMiddleware, dispatch=func) + return func + + return decorator + + def exception_handler( + self, + exc_class_or_status_code: Annotated[ + Union[int, Type[Exception]], + Doc( + """ + The Exception class this would handle, or a status code. + """ + ), + ], + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add an exception handler to the app. + + Read more about it in the + [FastAPI docs for Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/). + + ## Example + + ```python + from fastapi import FastAPI, Request + from fastapi.responses import JSONResponse + + + class UnicornException(Exception): + def __init__(self, name: str): + self.name = name + + + app = FastAPI() + + + @app.exception_handler(UnicornException) + async def unicorn_exception_handler(request: Request, exc: UnicornException): + return JSONResponse( + status_code=418, + content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."}, + ) + ``` + """ + + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_exception_handler(exc_class_or_status_code, func) + return func + + return decorator diff --git a/fastapi/background.py b/fastapi/background.py index dd3bbe2491..35ab1b2270 100644 --- a/fastapi/background.py +++ b/fastapi/background.py @@ -1 +1,59 @@ -from starlette.background import BackgroundTasks as BackgroundTasks # noqa +from typing import Any, Callable + +from starlette.background import BackgroundTasks as StarletteBackgroundTasks +from typing_extensions import Annotated, Doc, ParamSpec # type: ignore [attr-defined] + +P = ParamSpec("P") + + +class BackgroundTasks(StarletteBackgroundTasks): + """ + A collection of background tasks that will be called after a response has been + sent to the client. + + Read more about it in the + [FastAPI docs for Background Tasks](https://fastapi.tiangolo.com/tutorial/background-tasks/). + + ## Example + + ```python + from fastapi import BackgroundTasks, FastAPI + + app = FastAPI() + + + def write_notification(email: str, message=""): + with open("log.txt", mode="w") as email_file: + content = f"notification for {email}: {message}" + email_file.write(content) + + + @app.post("/send-notification/{email}") + async def send_notification(email: str, background_tasks: BackgroundTasks): + background_tasks.add_task(write_notification, email, message="some notification") + return {"message": "Notification sent in the background"} + ``` + """ + + def add_task( + self, + func: Annotated[ + Callable[P, Any], + Doc( + """ + The function to call after the response is sent. + + It can be a regular `def` function or an `async def` function. + """ + ), + ], + *args: P.args, + **kwargs: P.kwargs, + ) -> None: + """ + Add a function to be called in the background after the response is sent. + + Read more about it in the + [FastAPI docs for Background Tasks](https://fastapi.tiangolo.com/tutorial/background-tasks/). + """ + return super().add_task(func, *args, **kwargs) diff --git a/fastapi/concurrency.py b/fastapi/concurrency.py index 31b878d5df..894bd3ed11 100644 --- a/fastapi/concurrency.py +++ b/fastapi/concurrency.py @@ -1,4 +1,3 @@ -from contextlib import AsyncExitStack as AsyncExitStack # noqa from contextlib import asynccontextmanager as asynccontextmanager from typing import AsyncGenerator, ContextManager, TypeVar @@ -19,7 +18,7 @@ async def contextmanager_in_threadpool( ) -> AsyncGenerator[_T, None]: # blocking __exit__ from running waiting on a free thread # can create race conditions/deadlocks if the context manager itself - # has it's own internal pool (e.g. a database connection pool) + # has its own internal pool (e.g. a database connection pool) # to avoid this we let __exit__ run without a capacity limit # since we're creating a new limiter for each call, any non-zero limit # works (1 is arbitrary) diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index b20a25ab6e..ce03e3ce47 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -1,5 +1,22 @@ -from typing import Any, Callable, Dict, Iterable, Type, TypeVar +from typing import ( + Any, + BinaryIO, + Callable, + Dict, + Iterable, + Optional, + Type, + TypeVar, + cast, +) +from fastapi._compat import ( + PYDANTIC_V2, + CoreSchema, + GetJsonSchemaHandler, + JsonSchemaValue, + with_info_plain_validator_function, +) from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401 from starlette.datastructures import FormData as FormData # noqa: F401 @@ -7,9 +24,120 @@ from starlette.datastructures import Headers as Headers # noqa: F401 from starlette.datastructures import QueryParams as QueryParams # noqa: F401 from starlette.datastructures import State as State # noqa: F401 from starlette.datastructures import UploadFile as StarletteUploadFile +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] class UploadFile(StarletteUploadFile): + """ + A file uploaded in a request. + + Define it as a *path operation function* (or dependency) parameter. + + If you are using a regular `def` function, you can use the `upload_file.file` + attribute to access the raw standard Python file (blocking, not async), useful and + needed for non-async code. + + Read more about it in the + [FastAPI docs for Request Files](https://fastapi.tiangolo.com/tutorial/request-files/). + + ## Example + + ```python + from typing import Annotated + + from fastapi import FastAPI, File, UploadFile + + app = FastAPI() + + + @app.post("/files/") + async def create_file(file: Annotated[bytes, File()]): + return {"file_size": len(file)} + + + @app.post("/uploadfile/") + async def create_upload_file(file: UploadFile): + return {"filename": file.filename} + ``` + """ + + file: Annotated[ + BinaryIO, + Doc("The standard Python file object (non-async)."), + ] + filename: Annotated[Optional[str], Doc("The original file name.")] + size: Annotated[Optional[int], Doc("The size of the file in bytes.")] + headers: Annotated[Headers, Doc("The headers of the request.")] + content_type: Annotated[ + Optional[str], Doc("The content type of the request, from the headers.") + ] + + async def write( + self, + data: Annotated[ + bytes, + Doc( + """ + The bytes to write to the file. + """ + ), + ], + ) -> None: + """ + Write some bytes to the file. + + You normally wouldn't use this from a file you read in a request. + + To be awaitable, compatible with async, this is run in threadpool. + """ + return await super().write(data) + + async def read( + self, + size: Annotated[ + int, + Doc( + """ + The number of bytes to read from the file. + """ + ), + ] = -1, + ) -> bytes: + """ + Read some bytes from the file. + + To be awaitable, compatible with async, this is run in threadpool. + """ + return await super().read(size) + + async def seek( + self, + offset: Annotated[ + int, + Doc( + """ + The position in bytes to seek to in the file. + """ + ), + ], + ) -> None: + """ + Move to a position in the file. + + Any next read or write will be done from that position. + + To be awaitable, compatible with async, this is run in threadpool. + """ + return await super().seek(offset) + + async def close(self) -> None: + """ + Close the file. + + To be awaitable, compatible with async, this is run in threadpool. + """ + return await super().close() + @classmethod def __get_validators__(cls: Type["UploadFile"]) -> Iterable[Callable[..., Any]]: yield cls.validate @@ -21,8 +149,28 @@ class UploadFile(StarletteUploadFile): return v @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - field_schema.update({"type": "string", "format": "binary"}) + def _validate(cls, __input_value: Any, _: Any) -> "UploadFile": + if not isinstance(__input_value, StarletteUploadFile): + raise ValueError(f"Expected UploadFile, received: {type(__input_value)}") + return cast(UploadFile, __input_value) + + if not PYDANTIC_V2: + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update({"type": "string", "format": "binary"}) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + return {"type": "string", "format": "binary"} + + @classmethod + def __get_pydantic_core_schema__( + cls, source: Type[Any], handler: Callable[[Any], CoreSchema] + ) -> CoreSchema: + return with_info_plain_validator_function(cls._validate) class DefaultPlaceholder: diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 443590b9c8..61ef006387 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -1,7 +1,7 @@ from typing import Any, Callable, List, Optional, Sequence +from fastapi._compat import ModelField from fastapi.security.base import SecurityBase -from pydantic.fields import ModelField class SecurityRequirement: diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index a551b46857..daedd4ee3c 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,6 +1,5 @@ -import dataclasses import inspect -from contextlib import contextmanager +from contextlib import AsyncExitStack, contextmanager from copy import deepcopy from typing import ( Any, @@ -20,8 +19,33 @@ from typing import ( import anyio from fastapi import params +from fastapi._compat import ( + PYDANTIC_V2, + ErrorWrapper, + ModelField, + Required, + Undefined, + _regenerate_error_with_loc, + copy_field_info, + create_body_model, + evaluate_forwardref, + field_annotation_is_scalar, + get_annotation_from_field_info, + get_missing_field_error, + is_bytes_field, + is_bytes_sequence_field, + is_scalar_field, + is_scalar_sequence_field, + is_sequence_field, + is_uploadfile_or_nonable_uploadfile_annotation, + is_uploadfile_sequence_annotation, + lenient_issubclass, + sequence_types, + serialize_sequence_value, + value_is_sequence, +) +from fastapi.background import BackgroundTasks from fastapi.concurrency import ( - AsyncExitStack, asynccontextmanager, contextmanager_in_threadpool, ) @@ -31,49 +55,14 @@ from fastapi.security.base import SecurityBase from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.utils import create_response_field, get_path_param_names -from pydantic import BaseModel, create_model -from pydantic.error_wrappers import ErrorWrapper -from pydantic.errors import MissingError -from pydantic.fields import ( - SHAPE_FROZENSET, - SHAPE_LIST, - SHAPE_SEQUENCE, - SHAPE_SET, - SHAPE_SINGLETON, - SHAPE_TUPLE, - SHAPE_TUPLE_ELLIPSIS, - FieldInfo, - ModelField, - Required, - Undefined, -) -from pydantic.schema import get_annotation_from_field_info -from pydantic.typing import evaluate_forwardref -from pydantic.utils import lenient_issubclass -from starlette.background import BackgroundTasks +from pydantic.fields import FieldInfo +from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool from starlette.datastructures import FormData, Headers, QueryParams, UploadFile from starlette.requests import HTTPConnection, Request from starlette.responses import Response from starlette.websockets import WebSocket - -sequence_shapes = { - SHAPE_LIST, - SHAPE_SET, - SHAPE_FROZENSET, - SHAPE_TUPLE, - SHAPE_SEQUENCE, - SHAPE_TUPLE_ELLIPSIS, -} -sequence_types = (list, set, tuple) -sequence_shape_to_type = { - SHAPE_LIST: list, - SHAPE_SET: set, - SHAPE_TUPLE: tuple, - SHAPE_SEQUENCE: list, - SHAPE_TUPLE_ELLIPSIS: list, -} - +from typing_extensions import Annotated, get_args, get_origin multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' @@ -112,18 +101,18 @@ def check_file_field(field: ModelField) -> None: def get_param_sub_dependant( - *, param: inspect.Parameter, path: str, security_scopes: Optional[List[str]] = None + *, + param_name: str, + depends: params.Depends, + path: str, + security_scopes: Optional[List[str]] = None, ) -> Dependant: - depends: params.Depends = param.default - if depends.dependency: - dependency = depends.dependency - else: - dependency = param.annotation + assert depends.dependency return get_sub_dependant( depends=depends, - dependency=dependency, + dependency=depends.dependency, path=path, - name=param.name, + name=param_name, security_scopes=security_scopes, ) @@ -215,36 +204,6 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]: ) -def is_scalar_field(field: ModelField) -> bool: - field_info = field.field_info - if not ( - field.shape == SHAPE_SINGLETON - and not lenient_issubclass(field.type_, BaseModel) - and not lenient_issubclass(field.type_, sequence_types + (dict,)) - and not dataclasses.is_dataclass(field.type_) - and not isinstance(field_info, params.Body) - ): - return False - if field.sub_fields: - if not all(is_scalar_field(f) for f in field.sub_fields): - return False - return True - - -def is_scalar_sequence_field(field: ModelField) -> bool: - if (field.shape in sequence_shapes) and not lenient_issubclass( - field.type_, BaseModel - ): - if field.sub_fields is not None: - for sub_field in field.sub_fields: - if not is_scalar_field(sub_field): - return False - return True - if lenient_issubclass(field.type_, sequence_types): - return True - return False - - def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: signature = inspect.signature(call) globalns = getattr(call, "__globals__", {}) @@ -303,135 +262,231 @@ def get_dependant( use_cache=use_cache, ) for param_name, param in signature_params.items(): - if isinstance(param.default, params.Depends): + is_path_param = param_name in path_param_names + type_annotation, depends, param_field = analyze_param( + param_name=param_name, + annotation=param.annotation, + value=param.default, + is_path_param=is_path_param, + ) + if depends is not None: sub_dependant = get_param_sub_dependant( - param=param, path=path, security_scopes=security_scopes + param_name=param_name, + depends=depends, + path=path, + security_scopes=security_scopes, ) dependant.dependencies.append(sub_dependant) continue - if add_non_field_param_to_dependency(param=param, dependant=dependant): + if add_non_field_param_to_dependency( + param_name=param_name, + type_annotation=type_annotation, + dependant=dependant, + ): + assert ( + param_field is None + ), f"Cannot specify multiple FastAPI annotations for {param_name!r}" continue - param_field = get_param_field( - param=param, default_field_info=params.Query, param_name=param_name - ) - if param_name in path_param_names: - assert is_scalar_field( - field=param_field - ), "Path params must be of one of the supported types" - ignore_default = not isinstance(param.default, params.Path) - param_field = get_param_field( - param=param, - param_name=param_name, - default_field_info=params.Path, - force_type=params.ParamTypes.path, - ignore_default=ignore_default, - ) - add_param_to_fields(field=param_field, dependant=dependant) - elif is_scalar_field(field=param_field): - add_param_to_fields(field=param_field, dependant=dependant) - elif isinstance( - param.default, (params.Query, params.Header) - ) and is_scalar_sequence_field(param_field): - add_param_to_fields(field=param_field, dependant=dependant) - else: - field_info = param_field.field_info - assert isinstance( - field_info, params.Body - ), f"Param: {param_field.name} can only be a request body, using Body()" + assert param_field is not None + if is_body_param(param_field=param_field, is_path_param=is_path_param): dependant.body_params.append(param_field) + else: + add_param_to_fields(field=param_field, dependant=dependant) return dependant def add_non_field_param_to_dependency( - *, param: inspect.Parameter, dependant: Dependant + *, param_name: str, type_annotation: Any, dependant: Dependant ) -> Optional[bool]: - if lenient_issubclass(param.annotation, Request): - dependant.request_param_name = param.name + if lenient_issubclass(type_annotation, Request): + dependant.request_param_name = param_name return True - elif lenient_issubclass(param.annotation, WebSocket): - dependant.websocket_param_name = param.name + elif lenient_issubclass(type_annotation, WebSocket): + dependant.websocket_param_name = param_name return True - elif lenient_issubclass(param.annotation, HTTPConnection): - dependant.http_connection_param_name = param.name + elif lenient_issubclass(type_annotation, HTTPConnection): + dependant.http_connection_param_name = param_name return True - elif lenient_issubclass(param.annotation, Response): - dependant.response_param_name = param.name + elif lenient_issubclass(type_annotation, Response): + dependant.response_param_name = param_name return True - elif lenient_issubclass(param.annotation, BackgroundTasks): - dependant.background_tasks_param_name = param.name + elif lenient_issubclass(type_annotation, StarletteBackgroundTasks): + dependant.background_tasks_param_name = param_name return True - elif lenient_issubclass(param.annotation, SecurityScopes): - dependant.security_scopes_param_name = param.name + elif lenient_issubclass(type_annotation, SecurityScopes): + dependant.security_scopes_param_name = param_name return True return None -def get_param_field( +def analyze_param( *, - param: inspect.Parameter, param_name: str, - default_field_info: Type[params.Param] = params.Param, - force_type: Optional[params.ParamTypes] = None, - ignore_default: bool = False, -) -> ModelField: - default_value: Any = Undefined - had_schema = False - if not param.default == param.empty and ignore_default is False: - default_value = param.default - if isinstance(default_value, FieldInfo): - had_schema = True - field_info = default_value - default_value = field_info.default - if ( + annotation: Any, + value: Any, + is_path_param: bool, +) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]: + field_info = None + depends = None + type_annotation: Any = Any + use_annotation: Any = Any + if annotation is not inspect.Signature.empty: + use_annotation = annotation + type_annotation = annotation + if get_origin(use_annotation) is Annotated: + annotated_args = get_args(annotation) + type_annotation = annotated_args[0] + fastapi_annotations = [ + arg + for arg in annotated_args[1:] + if isinstance(arg, (FieldInfo, params.Depends)) + ] + fastapi_specific_annotations = [ + arg + for arg in fastapi_annotations + if isinstance(arg, (params.Param, params.Body, params.Depends)) + ] + if fastapi_specific_annotations: + fastapi_annotation: Union[ + FieldInfo, params.Depends, None + ] = fastapi_specific_annotations[-1] + else: + fastapi_annotation = None + if isinstance(fastapi_annotation, FieldInfo): + # Copy `field_info` because we mutate `field_info.default` below. + field_info = copy_field_info( + field_info=fastapi_annotation, annotation=use_annotation + ) + assert field_info.default is Undefined or field_info.default is Required, ( + f"`{field_info.__class__.__name__}` default value cannot be set in" + f" `Annotated` for {param_name!r}. Set the default value with `=` instead." + ) + if value is not inspect.Signature.empty: + assert not is_path_param, "Path parameters cannot have default values" + field_info.default = value + else: + field_info.default = Required + elif isinstance(fastapi_annotation, params.Depends): + depends = fastapi_annotation + + if isinstance(value, params.Depends): + assert depends is None, ( + "Cannot specify `Depends` in `Annotated` and default value" + f" together for {param_name!r}" + ) + assert field_info is None, ( + "Cannot specify a FastAPI annotation in `Annotated` and `Depends` as a" + f" default value together for {param_name!r}" + ) + depends = value + elif isinstance(value, FieldInfo): + assert field_info is None, ( + "Cannot specify FastAPI annotations in `Annotated` and default value" + f" together for {param_name!r}" + ) + field_info = value + if PYDANTIC_V2: + field_info.annotation = type_annotation + + if depends is not None and depends.dependency is None: + depends.dependency = type_annotation + + if lenient_issubclass( + type_annotation, + ( + Request, + WebSocket, + HTTPConnection, + Response, + StarletteBackgroundTasks, + SecurityScopes, + ), + ): + assert depends is None, f"Cannot specify `Depends` for type {type_annotation!r}" + assert ( + field_info is None + ), f"Cannot specify FastAPI annotation for type {type_annotation!r}" + elif field_info is None and depends is None: + default_value = value if value is not inspect.Signature.empty else Required + if is_path_param: + # We might check here that `default_value is Required`, but the fact is that the same + # parameter might sometimes be a path parameter and sometimes not. See + # `tests/test_infer_param_optionality.py` for an example. + field_info = params.Path(annotation=use_annotation) + elif is_uploadfile_or_nonable_uploadfile_annotation( + type_annotation + ) or is_uploadfile_sequence_annotation(type_annotation): + field_info = params.File(annotation=use_annotation, default=default_value) + elif not field_annotation_is_scalar(annotation=type_annotation): + field_info = params.Body(annotation=use_annotation, default=default_value) + else: + field_info = params.Query(annotation=use_annotation, default=default_value) + + field = None + if field_info is not None: + if is_path_param: + assert isinstance(field_info, params.Path), ( + f"Cannot use `{field_info.__class__.__name__}` for path param" + f" {param_name!r}" + ) + elif ( isinstance(field_info, params.Param) and getattr(field_info, "in_", None) is None ): - field_info.in_ = default_field_info.in_ - if force_type: - field_info.in_ = force_type # type: ignore - else: - field_info = default_field_info(default=default_value) - required = True - if default_value is Required or ignore_default: - required = True - default_value = None - elif default_value is not Undefined: - required = False - annotation: Any = Any - if not param.annotation == param.empty: - annotation = param.annotation - annotation = get_annotation_from_field_info(annotation, field_info, param_name) - if not field_info.alias and getattr(field_info, "convert_underscores", None): - alias = param.name.replace("_", "-") - else: - alias = field_info.alias or param.name - field = create_response_field( - name=param.name, - type_=annotation, - default=default_value, - alias=alias, - required=required, - field_info=field_info, - ) - if not had_schema and not is_scalar_field(field=field): - field.field_info = params.Body(field_info.default) - if not had_schema and lenient_issubclass(field.type_, UploadFile): - field.field_info = params.File(field_info.default) + field_info.in_ = params.ParamTypes.query + use_annotation_from_field_info = get_annotation_from_field_info( + use_annotation, + field_info, + param_name, + ) + if not field_info.alias and getattr(field_info, "convert_underscores", None): + alias = param_name.replace("_", "-") + else: + alias = field_info.alias or param_name + field_info.alias = alias + field = create_response_field( + name=param_name, + type_=use_annotation_from_field_info, + default=field_info.default, + alias=alias, + required=field_info.default in (Required, Undefined), + field_info=field_info, + ) - return field + return type_annotation, depends, field + + +def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool: + if is_path_param: + assert is_scalar_field( + field=param_field + ), "Path params must be of one of the supported types" + return False + elif is_scalar_field(field=param_field): + return False + elif isinstance( + param_field.field_info, (params.Query, params.Header) + ) and is_scalar_sequence_field(param_field): + return False + else: + assert isinstance( + param_field.field_info, params.Body + ), f"Param: {param_field.name} can only be a request body, using Body()" + return True def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None: - field_info = cast(params.Param, field.field_info) - if field_info.in_ == params.ParamTypes.path: + field_info = field.field_info + field_info_in = getattr(field_info, "in_", None) + if field_info_in == params.ParamTypes.path: dependant.path_params.append(field) - elif field_info.in_ == params.ParamTypes.query: + elif field_info_in == params.ParamTypes.query: dependant.query_params.append(field) - elif field_info.in_ == params.ParamTypes.header: + elif field_info_in == params.ParamTypes.header: dependant.header_params.append(field) else: assert ( - field_info.in_ == params.ParamTypes.cookie + field_info_in == params.ParamTypes.cookie ), f"non-body parameters must be in path, query, header or cookie: {field.name}" dependant.cookie_params.append(field) @@ -474,19 +529,20 @@ async def solve_dependencies( request: Union[Request, WebSocket], dependant: Dependant, body: Optional[Union[Dict[str, Any], FormData]] = None, - background_tasks: Optional[BackgroundTasks] = None, + background_tasks: Optional[StarletteBackgroundTasks] = None, response: Optional[Response] = None, dependency_overrides_provider: Optional[Any] = None, dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, + async_exit_stack: AsyncExitStack, ) -> Tuple[ Dict[str, Any], - List[ErrorWrapper], - Optional[BackgroundTasks], + List[Any], + Optional[StarletteBackgroundTasks], Response, Dict[Tuple[Callable[..., Any], Tuple[str]], Any], ]: values: Dict[str, Any] = {} - errors: List[ErrorWrapper] = [] + errors: List[Any] = [] if response is None: response = Response() del response.headers["content-length"] @@ -524,6 +580,7 @@ async def solve_dependencies( response=response, dependency_overrides_provider=dependency_overrides_provider, dependency_cache=dependency_cache, + async_exit_stack=async_exit_stack, ) ( sub_values, @@ -539,10 +596,8 @@ async def solve_dependencies( if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache: solved = dependency_cache[sub_dependant.cache_key] elif is_gen_callable(call) or is_async_gen_callable(call): - stack = request.scope.get("fastapi_astack") - assert isinstance(stack, AsyncExitStack) solved = await solve_generator( - call=call, stack=stack, sub_values=sub_values + call=call, stack=async_exit_stack, sub_values=sub_values ) elif is_coroutine_callable(call): solved = await call(**sub_values) @@ -600,7 +655,7 @@ async def solve_dependencies( def request_params_to_args( required_params: Sequence[ModelField], received_params: Union[Mapping[str, Any], QueryParams, Headers], -) -> Tuple[Dict[str, Any], List[ErrorWrapper]]: +) -> Tuple[Dict[str, Any], List[Any]]: values = {} errors = [] for field in required_params: @@ -614,23 +669,19 @@ def request_params_to_args( assert isinstance( field_info, params.Param ), "Params must be subclasses of Param" + loc = (field_info.in_.value, field.alias) if value is None: if field.required: - errors.append( - ErrorWrapper( - MissingError(), loc=(field_info.in_.value, field.alias) - ) - ) + errors.append(get_missing_field_error(loc=loc)) else: values[field.name] = deepcopy(field.default) continue - v_, errors_ = field.validate( - value, values, loc=(field_info.in_.value, field.alias) - ) + v_, errors_ = field.validate(value, values, loc=loc) if isinstance(errors_, ErrorWrapper): errors.append(errors_) elif isinstance(errors_, list): - errors.extend(errors_) + new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) + errors.extend(new_errors) else: values[field.name] = v_ return values, errors @@ -639,9 +690,9 @@ def request_params_to_args( async def request_body_to_args( required_params: List[ModelField], received_body: Optional[Union[Dict[str, Any], FormData]], -) -> Tuple[Dict[str, Any], List[ErrorWrapper]]: +) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: values = {} - errors = [] + errors: List[Dict[str, Any]] = [] if required_params: field = required_params[0] field_info = field.field_info @@ -659,9 +710,7 @@ async def request_body_to_args( value: Optional[Any] = None if received_body is not None: - if ( - field.shape in sequence_shapes or field.type_ in sequence_types - ) and isinstance(received_body, FormData): + if (is_sequence_field(field)) and isinstance(received_body, FormData): value = received_body.getlist(field.alias) else: try: @@ -674,7 +723,7 @@ async def request_body_to_args( or (isinstance(field_info, params.Form) and value == "") or ( isinstance(field_info, params.Form) - and field.shape in sequence_shapes + and is_sequence_field(field) and len(value) == 0 ) ): @@ -685,45 +734,41 @@ async def request_body_to_args( continue if ( isinstance(field_info, params.File) - and lenient_issubclass(field.type_, bytes) + and is_bytes_field(field) and isinstance(value, UploadFile) ): value = await value.read() elif ( - field.shape in sequence_shapes + is_bytes_sequence_field(field) and isinstance(field_info, params.File) - and lenient_issubclass(field.type_, bytes) - and isinstance(value, sequence_types) + and value_is_sequence(value) ): + # For types + assert isinstance(value, sequence_types) # type: ignore[arg-type] results: List[Union[bytes, str]] = [] async def process_fn( fn: Callable[[], Coroutine[Any, Any, Any]] ) -> None: result = await fn() - results.append(result) + results.append(result) # noqa: B023 async with anyio.create_task_group() as tg: for sub_value in value: tg.start_soon(process_fn, sub_value.read) - value = sequence_shape_to_type[field.shape](results) + value = serialize_sequence_value(field=field, value=results) v_, errors_ = field.validate(value, values, loc=loc) - if isinstance(errors_, ErrorWrapper): - errors.append(errors_) - elif isinstance(errors_, list): + if isinstance(errors_, list): errors.extend(errors_) + elif errors_: + errors.append(errors_) else: values[field.name] = v_ return values, errors -def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper: - missing_field_error = ErrorWrapper(MissingError(), loc=loc) - return missing_field_error - - def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: flat_dependant = get_flat_dependant(dependant) if not flat_dependant.body_params: @@ -741,12 +786,16 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: for param in flat_dependant.body_params: setattr(param.field_info, "embed", True) # noqa: B010 model_name = "Body_" + name - BodyModel: Type[BaseModel] = create_model(model_name) - for f in flat_dependant.body_params: - BodyModel.__fields__[f.name] = f + BodyModel = create_body_model( + fields=flat_dependant.body_params, model_name=model_name + ) required = any(True for f in flat_dependant.body_params if f.required) - - BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None} + BodyFieldInfo_kwargs: Dict[str, Any] = { + "annotation": BodyModel, + "alias": "body", + } + if not required: + BodyFieldInfo_kwargs["default"] = None if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params): BodyFieldInfo: Type[params.Body] = params.File elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params): diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 2f95bcbf66..e501713931 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -1,15 +1,88 @@ import dataclasses -from collections import defaultdict +import datetime +from collections import defaultdict, deque +from decimal import Decimal from enum import Enum -from pathlib import PurePath +from ipaddress import ( + IPv4Address, + IPv4Interface, + IPv4Network, + IPv6Address, + IPv6Interface, + IPv6Network, +) +from pathlib import Path, PurePath +from re import Pattern from types import GeneratorType -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union +from uuid import UUID +from fastapi.types import IncEx from pydantic import BaseModel -from pydantic.json import ENCODERS_BY_TYPE +from pydantic.color import Color +from pydantic.networks import AnyUrl, NameEmail +from pydantic.types import SecretBytes, SecretStr +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] -SetIntStr = Set[Union[int, str]] -DictIntStrAny = Dict[Union[int, str], Any] +from ._compat import PYDANTIC_V2, Url, _model_dump + + +# Taken from Pydantic v1 as is +def isoformat(o: Union[datetime.date, datetime.time]) -> str: + return o.isoformat() + + +# Taken from Pydantic v1 as is +# TODO: pv2 should this return strings instead? +def decimal_encoder(dec_value: Decimal) -> Union[int, float]: + """ + Encodes a Decimal as int of there's no exponent, otherwise float + + This is useful when we use ConstrainedDecimal to represent Numeric(x,0) + where a integer (but not int typed) is used. Encoding this as a float + results in failed round-tripping between encode and parse. + Our Id type is a prime example of this. + + >>> decimal_encoder(Decimal("1.0")) + 1.0 + + >>> decimal_encoder(Decimal("1")) + 1 + """ + if dec_value.as_tuple().exponent >= 0: # type: ignore[operator] + return int(dec_value) + else: + return float(dec_value) + + +ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { + bytes: lambda o: o.decode(), + Color: str, + datetime.date: isoformat, + datetime.datetime: isoformat, + datetime.time: isoformat, + datetime.timedelta: lambda td: td.total_seconds(), + Decimal: decimal_encoder, + Enum: lambda o: o.value, + frozenset: list, + deque: list, + GeneratorType: list, + IPv4Address: str, + IPv4Interface: str, + IPv4Network: str, + IPv6Address: str, + IPv6Interface: str, + IPv6Network: str, + NameEmail: str, + Path: str, + Pattern: lambda o: o.pattern, + SecretBytes: str, + SecretStr: str, + set: list, + UUID: str, + Url: str, + AnyUrl: str, +} def generate_encoders_by_class_tuples( @@ -27,16 +100,107 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE) def jsonable_encoder( - obj: Any, - include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - by_alias: bool = True, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None, - sqlalchemy_safe: bool = True, + obj: Annotated[ + Any, + Doc( + """ + The input object to convert to JSON. + """ + ), + ], + include: Annotated[ + Optional[IncEx], + Doc( + """ + Pydantic's `include` parameter, passed to Pydantic models to set the + fields to include. + """ + ), + ] = None, + exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Pydantic's `exclude` parameter, passed to Pydantic models to set the + fields to exclude. + """ + ), + ] = None, + by_alias: Annotated[ + bool, + Doc( + """ + Pydantic's `by_alias` parameter, passed to Pydantic models to define if + the output should use the alias names (when provided) or the Python + attribute names. In an API, if you set an alias, it's probably because you + want to use it in the result, so you probably want to leave this set to + `True`. + """ + ), + ] = True, + exclude_unset: Annotated[ + bool, + Doc( + """ + Pydantic's `exclude_unset` parameter, passed to Pydantic models to define + if it should exclude from the output the fields that were not explicitly + set (and that only had their default values). + """ + ), + ] = False, + exclude_defaults: Annotated[ + bool, + Doc( + """ + Pydantic's `exclude_defaults` parameter, passed to Pydantic models to define + if it should exclude from the output the fields that had the same default + value, even when they were explicitly set. + """ + ), + ] = False, + exclude_none: Annotated[ + bool, + Doc( + """ + Pydantic's `exclude_none` parameter, passed to Pydantic models to define + if it should exclude from the output any fields that have a `None` value. + """ + ), + ] = False, + custom_encoder: Annotated[ + Optional[Dict[Any, Callable[[Any], Any]]], + Doc( + """ + Pydantic's `custom_encoder` parameter, passed to Pydantic models to define + a custom encoder. + """ + ), + ] = None, + sqlalchemy_safe: Annotated[ + bool, + Doc( + """ + Exclude from the output any fields that start with the name `_sa`. + + This is mainly a hack for compatibility with SQLAlchemy objects, they + store internal SQLAlchemy-specific state in attributes named with `_sa`, + and those objects can't (and shouldn't be) serialized to JSON. + """ + ), + ] = True, ) -> Any: + """ + Convert any object to something that can be encoded in JSON. + + This is used internally by FastAPI to make sure anything you return can be + encoded as JSON before it is sent to the client. + + You can also use it yourself, for example to convert objects before saving them + in a database that supports only JSON. + + Read more about it in the + [FastAPI docs for JSON Compatible Encoder](https://fastapi.tiangolo.com/tutorial/encoder/). + """ custom_encoder = custom_encoder or {} if custom_encoder: if type(obj) in custom_encoder: @@ -50,10 +214,15 @@ def jsonable_encoder( if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) if isinstance(obj, BaseModel): - encoder = getattr(obj.__config__, "json_encoders", {}) - if custom_encoder: - encoder.update(custom_encoder) - obj_dict = obj.dict( + # TODO: remove when deprecating Pydantic v1 + encoders: Dict[Any, Any] = {} + if not PYDANTIC_V2: + encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined] + if custom_encoder: + encoders.update(custom_encoder) + obj_dict = _model_dump( + obj, + mode="json", include=include, exclude=exclude, by_alias=by_alias, @@ -67,7 +236,8 @@ def jsonable_encoder( obj_dict, exclude_none=exclude_none, exclude_defaults=exclude_defaults, - custom_encoder=encoder, + # TODO: remove when deprecating Pydantic v1 + custom_encoder=encoders, sqlalchemy_safe=sqlalchemy_safe, ) if dataclasses.is_dataclass(obj): @@ -124,7 +294,7 @@ def jsonable_encoder( ) encoded_dict[encoded_key] = encoded_value return encoded_dict - if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)): encoded_list = [] for item in obj: encoded_list.append( diff --git a/fastapi/exception_handlers.py b/fastapi/exception_handlers.py index 4d7ea5ec2e..6c2ba7fedf 100644 --- a/fastapi/exception_handlers.py +++ b/fastapi/exception_handlers.py @@ -1,10 +1,11 @@ from fastapi.encoders import jsonable_encoder -from fastapi.exceptions import RequestValidationError +from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError from fastapi.utils import is_body_allowed_for_status_code +from fastapi.websockets import WebSocket from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.responses import JSONResponse, Response -from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY +from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, WS_1008_POLICY_VIOLATION async def http_exception_handler(request: Request, exc: HTTPException) -> Response: @@ -23,3 +24,11 @@ async def request_validation_exception_handler( status_code=HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": jsonable_encoder(exc.errors())}, ) + + +async def websocket_request_validation_exception_handler( + websocket: WebSocket, exc: WebSocketRequestValidationError +) -> None: + await websocket.close( + code=WS_1008_POLICY_VIOLATION, reason=jsonable_encoder(exc.errors()) + ) diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index ca097b1cef..680d288e4d 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -1,21 +1,141 @@ -from typing import Any, Dict, Optional, Sequence, Type +from typing import Any, Dict, Optional, Sequence, Type, Union -from pydantic import BaseModel, ValidationError, create_model -from pydantic.error_wrappers import ErrorList +from pydantic import BaseModel, create_model from starlette.exceptions import HTTPException as StarletteHTTPException -from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401 +from starlette.exceptions import WebSocketException as StarletteWebSocketException +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] class HTTPException(StarletteHTTPException): + """ + An HTTP exception you can raise in your own code to show errors to the client. + + This is for client errors, invalid authentication, invalid data, etc. Not for server + errors in your code. + + Read more about it in the + [FastAPI docs for Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/). + + ## Example + + ```python + from fastapi import FastAPI, HTTPException + + app = FastAPI() + + items = {"foo": "The Foo Wrestlers"} + + + @app.get("/items/{item_id}") + async def read_item(item_id: str): + if item_id not in items: + raise HTTPException(status_code=404, detail="Item not found") + return {"item": items[item_id]} + ``` + """ + def __init__( self, - status_code: int, - detail: Any = None, - headers: Optional[Dict[str, Any]] = None, + status_code: Annotated[ + int, + Doc( + """ + HTTP status code to send to the client. + """ + ), + ], + detail: Annotated[ + Any, + Doc( + """ + Any data to be sent to the client in the `detail` key of the JSON + response. + """ + ), + ] = None, + headers: Annotated[ + Optional[Dict[str, str]], + Doc( + """ + Any headers to send to the client in the response. + """ + ), + ] = None, ) -> None: super().__init__(status_code=status_code, detail=detail, headers=headers) +class WebSocketException(StarletteWebSocketException): + """ + A WebSocket exception you can raise in your own code to show errors to the client. + + This is for client errors, invalid authentication, invalid data, etc. Not for server + errors in your code. + + Read more about it in the + [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/). + + ## Example + + ```python + from typing import Annotated + + from fastapi import ( + Cookie, + FastAPI, + WebSocket, + WebSocketException, + status, + ) + + app = FastAPI() + + @app.websocket("/items/{item_id}/ws") + async def websocket_endpoint( + *, + websocket: WebSocket, + session: Annotated[str | None, Cookie()] = None, + item_id: str, + ): + if session is None: + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text(f"Session cookie is: {session}") + await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}") + ``` + """ + + def __init__( + self, + code: Annotated[ + int, + Doc( + """ + A closing code from the + [valid codes defined in the specification](https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1). + """ + ), + ], + reason: Annotated[ + Union[str, None], + Doc( + """ + The reason to close the WebSocket connection. + + It is UTF-8-encoded data. The interpretation of the reason is up to the + application, it is not specified by the WebSocket specification. + + It could contain text that could be human-readable or interpretable + by the client code, etc. + """ + ), + ] = None, + ) -> None: + super().__init__(code=code, reason=reason) + + RequestErrorModel: Type[BaseModel] = create_model("Request") WebSocketErrorModel: Type[BaseModel] = create_model("WebSocket") @@ -26,12 +146,31 @@ class FastAPIError(RuntimeError): """ -class RequestValidationError(ValidationError): - def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None: +class ValidationException(Exception): + def __init__(self, errors: Sequence[Any]) -> None: + self._errors = errors + + def errors(self) -> Sequence[Any]: + return self._errors + + +class RequestValidationError(ValidationException): + def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: + super().__init__(errors) self.body = body - super().__init__(errors, RequestErrorModel) -class WebSocketRequestValidationError(ValidationError): - def __init__(self, errors: Sequence[ErrorList]) -> None: - super().__init__(errors, WebSocketErrorModel) +class WebSocketRequestValidationError(ValidationException): + pass + + +class ResponseValidationError(ValidationException): + def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: + super().__init__(errors) + self.body = body + + def __str__(self) -> str: + message = f"{len(self._errors)} validation errors:\n" + for err in self._errors: + message += f" {err}\n" + return message diff --git a/fastapi/middleware/asyncexitstack.py b/fastapi/middleware/asyncexitstack.py deleted file mode 100644 index 503a68ac73..0000000000 --- a/fastapi/middleware/asyncexitstack.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Optional - -from fastapi.concurrency import AsyncExitStack -from starlette.types import ASGIApp, Receive, Scope, Send - - -class AsyncExitStackMiddleware: - def __init__(self, app: ASGIApp, context_name: str = "fastapi_astack") -> None: - self.app = app - self.context_name = context_name - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - if AsyncExitStack: - dependency_exception: Optional[Exception] = None - async with AsyncExitStack() as stack: - scope[self.context_name] = stack - try: - await self.app(scope, receive, send) - except Exception as e: - dependency_exception = e - raise e - if dependency_exception: - # This exception was possibly handled by the dependency but it should - # still bubble up so that the ServerErrorMiddleware can return a 500 - # or the ExceptionMiddleware can catch and handle any other exceptions - raise dependency_exception - else: - await self.app(scope, receive, send) # pragma: no cover diff --git a/fastapi/openapi/constants.py b/fastapi/openapi/constants.py index 1897ad7509..d724ee3cfd 100644 --- a/fastapi/openapi/constants.py +++ b/fastapi/openapi/constants.py @@ -1,2 +1,3 @@ METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"} REF_PREFIX = "#/components/schemas/" +REF_TEMPLATE = "#/components/schemas/{model}" diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index bf335118fc..69473d19cb 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -3,8 +3,18 @@ from typing import Any, Dict, Optional from fastapi.encoders import jsonable_encoder from starlette.responses import HTMLResponse +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] -swagger_ui_default_parameters = { +swagger_ui_default_parameters: Annotated[ + Dict[str, Any], + Doc( + """ + Default configurations for Swagger UI. + + You can use it as a template to add any other configurations needed. + """ + ), +] = { "dom_id": "#swagger-ui", "layout": "BaseLayout", "deepLinking": True, @@ -15,15 +25,91 @@ swagger_ui_default_parameters = { def get_swagger_ui_html( *, - openapi_url: str, - title: str, - swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui-bundle.js", - swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui.css", - swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png", - oauth2_redirect_url: Optional[str] = None, - init_oauth: Optional[Dict[str, Any]] = None, - swagger_ui_parameters: Optional[Dict[str, Any]] = None, + openapi_url: Annotated[ + str, + Doc( + """ + The OpenAPI URL that Swagger UI should load and use. + + This is normally done automatically by FastAPI using the default URL + `/openapi.json`. + """ + ), + ], + title: Annotated[ + str, + Doc( + """ + The HTML `` content, normally shown in the browser tab. + """ + ), + ], + swagger_js_url: Annotated[ + str, + Doc( + """ + The URL to use to load the Swagger UI JavaScript. + + It is normally set to a CDN URL. + """ + ), + ] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js", + swagger_css_url: Annotated[ + str, + Doc( + """ + The URL to use to load the Swagger UI CSS. + + It is normally set to a CDN URL. + """ + ), + ] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css", + swagger_favicon_url: Annotated[ + str, + Doc( + """ + The URL of the favicon to use. It is normally shown in the browser tab. + """ + ), + ] = "https://fastapi.tiangolo.com/img/favicon.png", + oauth2_redirect_url: Annotated[ + Optional[str], + Doc( + """ + The OAuth2 redirect URL, it is normally automatically handled by FastAPI. + """ + ), + ] = None, + init_oauth: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + A dictionary with Swagger UI OAuth2 initialization configurations. + """ + ), + ] = None, + swagger_ui_parameters: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Configuration parameters for Swagger UI. + + It defaults to [swagger_ui_default_parameters][fastapi.openapi.docs.swagger_ui_default_parameters]. + """ + ), + ] = None, ) -> HTMLResponse: + """ + Generate and return the HTML that loads Swagger UI for the interactive + API docs (normally served at `/docs`). + + You would only call this function yourself if you needed to override some parts, + for example the URLs to use to load Swagger UI's JavaScript and CSS. + + Read more about it in the + [FastAPI docs for Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/) + and the [FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/). + """ current_swagger_ui_parameters = swagger_ui_default_parameters.copy() if swagger_ui_parameters: current_swagger_ui_parameters.update(swagger_ui_parameters) @@ -74,12 +160,62 @@ def get_swagger_ui_html( def get_redoc_html( *, - openapi_url: str, - title: str, - redoc_js_url: str = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js", - redoc_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png", - with_google_fonts: bool = True, + openapi_url: Annotated[ + str, + Doc( + """ + The OpenAPI URL that ReDoc should load and use. + + This is normally done automatically by FastAPI using the default URL + `/openapi.json`. + """ + ), + ], + title: Annotated[ + str, + Doc( + """ + The HTML `<title>` content, normally shown in the browser tab. + """ + ), + ], + redoc_js_url: Annotated[ + str, + Doc( + """ + The URL to use to load the ReDoc JavaScript. + + It is normally set to a CDN URL. + """ + ), + ] = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js", + redoc_favicon_url: Annotated[ + str, + Doc( + """ + The URL of the favicon to use. It is normally shown in the browser tab. + """ + ), + ] = "https://fastapi.tiangolo.com/img/favicon.png", + with_google_fonts: Annotated[ + bool, + Doc( + """ + Load and use Google Fonts. + """ + ), + ] = True, ) -> HTMLResponse: + """ + Generate and return the HTML response that loads ReDoc for the alternative + API docs (normally served at `/redoc`). + + You would only call this function yourself if you needed to override some parts, + for example the URLs to use to load ReDoc's JavaScript and CSS. + + Read more about it in the + [FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/). + """ html = f""" <!DOCTYPE html> <html> @@ -118,6 +254,11 @@ def get_redoc_html( def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse: + """ + Generate the HTML response with the OAuth2 redirection for Swagger UI. + + You normally don't need to use or change this. + """ # copied from https://github.com/swagger-api/swagger-ui/blob/v4.14.0/dist/oauth2-redirect.html html = """ <!doctype html> diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 35aa1672b3..5f3bdbb206 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -1,11 +1,21 @@ from enum import Enum -from typing import Any, Callable, Dict, Iterable, List, Optional, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Type, Union +from fastapi._compat import ( + PYDANTIC_V2, + CoreSchema, + GetJsonSchemaHandler, + JsonSchemaValue, + _model_rebuild, + with_info_plain_validator_function, +) from fastapi.logger import logger from pydantic import AnyUrl, BaseModel, Field +from typing_extensions import Annotated, Literal, TypedDict +from typing_extensions import deprecated as typing_deprecated try: - import email_validator # type: ignore + import email_validator assert email_validator # make autoflake ignore the unused import from pydantic import EmailStr @@ -24,43 +34,85 @@ except ImportError: # pragma: no cover ) return str(v) + @classmethod + def _validate(cls, __input_value: Any, _: Any) -> str: + logger.warning( + "email-validator not installed, email fields will be treated as str.\n" + "To install, run: pip install email-validator" + ) + return str(__input_value) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + return {"type": "string", "format": "email"} + + @classmethod + def __get_pydantic_core_schema__( + cls, source: Type[Any], handler: Callable[[Any], CoreSchema] + ) -> CoreSchema: + return with_info_plain_validator_function(cls._validate) + class Contact(BaseModel): name: Optional[str] = None url: Optional[AnyUrl] = None email: Optional[EmailStr] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class License(BaseModel): name: str + identifier: Optional[str] = None url: Optional[AnyUrl] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Info(BaseModel): title: str + summary: Optional[str] = None description: Optional[str] = None termsOfService: Optional[str] = None contact: Optional[Contact] = None license: Optional[License] = None version: str - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class ServerVariable(BaseModel): - enum: Optional[List[str]] = None + enum: Annotated[Optional[List[str]], Field(min_length=1)] = None default: str description: Optional[str] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Server(BaseModel): @@ -68,8 +120,13 @@ class Server(BaseModel): description: Optional[str] = None variables: Optional[Dict[str, ServerVariable]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Reference(BaseModel): @@ -88,68 +145,141 @@ class XML(BaseModel): attribute: Optional[bool] = None wrapped: Optional[bool] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class ExternalDocumentation(BaseModel): description: Optional[str] = None url: AnyUrl - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Schema(BaseModel): + # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-the-json-schema-core-vocabu + # Core Vocabulary + schema_: Optional[str] = Field(default=None, alias="$schema") + vocabulary: Optional[str] = Field(default=None, alias="$vocabulary") + id: Optional[str] = Field(default=None, alias="$id") + anchor: Optional[str] = Field(default=None, alias="$anchor") + dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor") ref: Optional[str] = Field(default=None, alias="$ref") - title: Optional[str] = None - multipleOf: Optional[float] = None + dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef") + defs: Optional[Dict[str, "SchemaOrBool"]] = Field(default=None, alias="$defs") + comment: Optional[str] = Field(default=None, alias="$comment") + # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s + # A Vocabulary for Applying Subschemas + allOf: Optional[List["SchemaOrBool"]] = None + anyOf: Optional[List["SchemaOrBool"]] = None + oneOf: Optional[List["SchemaOrBool"]] = None + not_: Optional["SchemaOrBool"] = Field(default=None, alias="not") + if_: Optional["SchemaOrBool"] = Field(default=None, alias="if") + then: Optional["SchemaOrBool"] = None + else_: Optional["SchemaOrBool"] = Field(default=None, alias="else") + dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None + prefixItems: Optional[List["SchemaOrBool"]] = None + # TODO: uncomment and remove below when deprecating Pydantic v1 + # It generales a list of schemas for tuples, before prefixItems was available + # items: Optional["SchemaOrBool"] = None + items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None + contains: Optional["SchemaOrBool"] = None + properties: Optional[Dict[str, "SchemaOrBool"]] = None + patternProperties: Optional[Dict[str, "SchemaOrBool"]] = None + additionalProperties: Optional["SchemaOrBool"] = None + propertyNames: Optional["SchemaOrBool"] = None + unevaluatedItems: Optional["SchemaOrBool"] = None + unevaluatedProperties: Optional["SchemaOrBool"] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural + # A Vocabulary for Structural Validation + type: Optional[str] = None + enum: Optional[List[Any]] = None + const: Optional[Any] = None + multipleOf: Optional[float] = Field(default=None, gt=0) maximum: Optional[float] = None exclusiveMaximum: Optional[float] = None minimum: Optional[float] = None exclusiveMinimum: Optional[float] = None - maxLength: Optional[int] = Field(default=None, gte=0) - minLength: Optional[int] = Field(default=None, gte=0) + maxLength: Optional[int] = Field(default=None, ge=0) + minLength: Optional[int] = Field(default=None, ge=0) pattern: Optional[str] = None - maxItems: Optional[int] = Field(default=None, gte=0) - minItems: Optional[int] = Field(default=None, gte=0) + maxItems: Optional[int] = Field(default=None, ge=0) + minItems: Optional[int] = Field(default=None, ge=0) uniqueItems: Optional[bool] = None - maxProperties: Optional[int] = Field(default=None, gte=0) - minProperties: Optional[int] = Field(default=None, gte=0) + maxContains: Optional[int] = Field(default=None, ge=0) + minContains: Optional[int] = Field(default=None, ge=0) + maxProperties: Optional[int] = Field(default=None, ge=0) + minProperties: Optional[int] = Field(default=None, ge=0) required: Optional[List[str]] = None - enum: Optional[List[Any]] = None - type: Optional[str] = None - allOf: Optional[List["Schema"]] = None - oneOf: Optional[List["Schema"]] = None - anyOf: Optional[List["Schema"]] = None - not_: Optional["Schema"] = Field(default=None, alias="not") - items: Optional[Union["Schema", List["Schema"]]] = None - properties: Optional[Dict[str, "Schema"]] = None - additionalProperties: Optional[Union["Schema", Reference, bool]] = None - description: Optional[str] = None + dependentRequired: Optional[Dict[str, Set[str]]] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-vocabularies-for-semantic-c + # Vocabularies for Semantic Content With "format" format: Optional[str] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-the-conten + # A Vocabulary for the Contents of String-Encoded Data + contentEncoding: Optional[str] = None + contentMediaType: Optional[str] = None + contentSchema: Optional["SchemaOrBool"] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta + # A Vocabulary for Basic Meta-Data Annotations + title: Optional[str] = None + description: Optional[str] = None default: Optional[Any] = None - nullable: Optional[bool] = None - discriminator: Optional[Discriminator] = None + deprecated: Optional[bool] = None readOnly: Optional[bool] = None writeOnly: Optional[bool] = None + examples: Optional[List[Any]] = None + # Ref: OpenAPI 3.1.0: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#schema-object + # Schema Object + discriminator: Optional[Discriminator] = None xml: Optional[XML] = None externalDocs: Optional[ExternalDocumentation] = None - example: Optional[Any] = None - deprecated: Optional[bool] = None + example: Annotated[ + Optional[Any], + typing_deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = None - class Config: - extra: str = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" -class Example(BaseModel): - summary: Optional[str] = None - description: Optional[str] = None - value: Optional[Any] = None - externalValue: Optional[AnyUrl] = None +# Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents +# A JSON Schema MUST be an object or a boolean. +SchemaOrBool = Union[Schema, bool] - class Config: - extra = "allow" + +class Example(TypedDict, total=False): + summary: Optional[str] + description: Optional[str] + value: Optional[Any] + externalValue: Optional[AnyUrl] + + if PYDANTIC_V2: # type: ignore [misc] + __pydantic_config__ = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class ParameterInType(Enum): @@ -166,8 +296,13 @@ class Encoding(BaseModel): explode: Optional[bool] = None allowReserved: Optional[bool] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class MediaType(BaseModel): @@ -176,8 +311,13 @@ class MediaType(BaseModel): examples: Optional[Dict[str, Union[Example, Reference]]] = None encoding: Optional[Dict[str, Encoding]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class ParameterBase(BaseModel): @@ -194,8 +334,13 @@ class ParameterBase(BaseModel): # Serialization rules for more complex scenarios content: Optional[Dict[str, MediaType]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Parameter(ParameterBase): @@ -212,8 +357,13 @@ class RequestBody(BaseModel): content: Dict[str, MediaType] required: Optional[bool] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Link(BaseModel): @@ -224,8 +374,13 @@ class Link(BaseModel): description: Optional[str] = None server: Optional[Server] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Response(BaseModel): @@ -234,8 +389,13 @@ class Response(BaseModel): content: Optional[Dict[str, MediaType]] = None links: Optional[Dict[str, Union[Link, Reference]]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Operation(BaseModel): @@ -247,14 +407,19 @@ class Operation(BaseModel): parameters: Optional[List[Union[Parameter, Reference]]] = None requestBody: Optional[Union[RequestBody, Reference]] = None # Using Any for Specification Extensions - responses: Dict[str, Union[Response, Any]] + responses: Optional[Dict[str, Union[Response, Any]]] = None callbacks: Optional[Dict[str, Union[Dict[str, "PathItem"], Reference]]] = None deprecated: Optional[bool] = None security: Optional[List[Dict[str, List[str]]]] = None servers: Optional[List[Server]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class PathItem(BaseModel): @@ -272,8 +437,13 @@ class PathItem(BaseModel): servers: Optional[List[Server]] = None parameters: Optional[List[Union[Parameter, Reference]]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class SecuritySchemeType(Enum): @@ -287,8 +457,13 @@ class SecurityBase(BaseModel): type_: SecuritySchemeType = Field(alias="type") description: Optional[str] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class APIKeyIn(Enum): @@ -298,18 +473,18 @@ class APIKeyIn(Enum): class APIKey(SecurityBase): - type_ = Field(SecuritySchemeType.apiKey, alias="type") + type_: SecuritySchemeType = Field(default=SecuritySchemeType.apiKey, alias="type") in_: APIKeyIn = Field(alias="in") name: str class HTTPBase(SecurityBase): - type_ = Field(SecuritySchemeType.http, alias="type") + type_: SecuritySchemeType = Field(default=SecuritySchemeType.http, alias="type") scheme: str class HTTPBearer(HTTPBase): - scheme = "bearer" + scheme: Literal["bearer"] = "bearer" bearerFormat: Optional[str] = None @@ -317,8 +492,13 @@ class OAuthFlow(BaseModel): refreshUrl: Optional[str] = None scopes: Dict[str, str] = {} - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class OAuthFlowImplicit(OAuthFlow): @@ -344,17 +524,24 @@ class OAuthFlows(BaseModel): clientCredentials: Optional[OAuthFlowClientCredentials] = None authorizationCode: Optional[OAuthFlowAuthorizationCode] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class OAuth2(SecurityBase): - type_ = Field(SecuritySchemeType.oauth2, alias="type") + type_: SecuritySchemeType = Field(default=SecuritySchemeType.oauth2, alias="type") flows: OAuthFlows class OpenIdConnect(SecurityBase): - type_ = Field(SecuritySchemeType.openIdConnect, alias="type") + type_: SecuritySchemeType = Field( + default=SecuritySchemeType.openIdConnect, alias="type" + ) openIdConnectUrl: str @@ -372,9 +559,15 @@ class Components(BaseModel): links: Optional[Dict[str, Union[Link, Reference]]] = None # Using Any for Specification Extensions callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None + pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Tag(BaseModel): @@ -382,25 +575,37 @@ class Tag(BaseModel): description: Optional[str] = None externalDocs: Optional[ExternalDocumentation] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class OpenAPI(BaseModel): openapi: str info: Info + jsonSchemaDialect: Optional[str] = None servers: Optional[List[Server]] = None # Using Any for Specification Extensions - paths: Dict[str, Union[PathItem, Any]] + paths: Optional[Dict[str, Union[PathItem, Any]]] = None + webhooks: Optional[Dict[str, Union[PathItem, Reference]]] = None components: Optional[Components] = None security: Optional[List[Dict[str, List[str]]]] = None tags: Optional[List[Tag]] = None externalDocs: Optional[ExternalDocumentation] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" -Schema.update_forward_refs() -Operation.update_forward_refs() -Encoding.update_forward_refs() +_model_rebuild(Schema) +_model_rebuild(Operation) +_model_rebuild(Encoding) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 86e15b46d3..5bfb5acef7 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -1,35 +1,37 @@ import http.client import inspect import warnings -from enum import Enum from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast from fastapi import routing +from fastapi._compat import ( + GenerateJsonSchema, + JsonSchemaValue, + ModelField, + Undefined, + get_compat_model_name_map, + get_definitions, + get_schema_from_model_field, + lenient_issubclass, +) from fastapi.datastructures import DefaultPlaceholder from fastapi.dependencies.models import Dependant from fastapi.dependencies.utils import get_flat_dependant, get_flat_params from fastapi.encoders import jsonable_encoder -from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX +from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE from fastapi.openapi.models import OpenAPI from fastapi.params import Body, Param from fastapi.responses import Response +from fastapi.types import ModelNameMap from fastapi.utils import ( deep_dict_update, generate_operation_id_for_path, - get_model_definitions, is_body_allowed_for_status_code, ) -from pydantic import BaseModel -from pydantic.fields import ModelField, Undefined -from pydantic.schema import ( - field_schema, - get_flat_models_from_fields, - get_model_name_map, -) -from pydantic.utils import lenient_issubclass from starlette.responses import JSONResponse from starlette.routing import BaseRoute from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY +from typing_extensions import Literal validation_error_definition = { "title": "ValidationError", @@ -88,7 +90,12 @@ def get_openapi_security_definitions( def get_openapi_operation_parameters( *, all_route_params: Sequence[ModelField], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + separate_input_output_schemas: bool = True, ) -> List[Dict[str, Any]]: parameters = [] for param in all_route_params: @@ -96,18 +103,23 @@ def get_openapi_operation_parameters( field_info = cast(Param, field_info) if not field_info.include_in_schema: continue + param_schema = get_schema_from_model_field( + field=param, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, + ) parameter = { "name": param.alias, "in": field_info.in_.value, "required": param.required, - "schema": field_schema( - param, model_name_map=model_name_map, ref_prefix=REF_PREFIX - )[0], + "schema": param_schema, } if field_info.description: parameter["description"] = field_info.description - if field_info.examples: - parameter["examples"] = jsonable_encoder(field_info.examples) + if field_info.openapi_examples: + parameter["examples"] = jsonable_encoder(field_info.openapi_examples) elif field_info.example != Undefined: parameter["example"] = jsonable_encoder(field_info.example) if field_info.deprecated: @@ -119,13 +131,22 @@ def get_openapi_operation_parameters( def get_openapi_operation_request_body( *, body_field: Optional[ModelField], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + separate_input_output_schemas: bool = True, ) -> Optional[Dict[str, Any]]: if not body_field: return None assert isinstance(body_field, ModelField) - body_schema, _, _ = field_schema( - body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + body_schema = get_schema_from_model_field( + field=body_field, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) field_info = cast(Body, body_field.field_info) request_media_type = field_info.media_type @@ -134,8 +155,10 @@ def get_openapi_operation_request_body( if required: request_body_oai["required"] = required request_media_content: Dict[str, Any] = {"schema": body_schema} - if field_info.examples: - request_media_content["examples"] = jsonable_encoder(field_info.examples) + if field_info.openapi_examples: + request_media_content["examples"] = jsonable_encoder( + field_info.openapi_examples + ) elif field_info.example != Undefined: request_media_content["example"] = jsonable_encoder(field_info.example) request_body_oai["content"] = {request_media_type: request_media_content} @@ -181,7 +204,7 @@ def get_openapi_operation_metadata( file_name = getattr(route.endpoint, "__globals__", {}).get("__file__") if file_name: message += f" at {file_name}" - warnings.warn(message) + warnings.warn(message, stacklevel=1) operation_ids.add(operation_id) operation["operationId"] = operation_id if route.deprecated: @@ -190,7 +213,15 @@ def get_openapi_operation_metadata( def get_openapi_path( - *, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str] + *, + route: routing.APIRoute, + operation_ids: Set[str], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + separate_input_output_schemas: bool = True, ) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]: path = {} security_schemes: Dict[str, Any] = {} @@ -218,7 +249,11 @@ def get_openapi_path( security_schemes.update(security_definitions) all_route_params = get_flat_params(route.dependant) operation_parameters = get_openapi_operation_parameters( - all_route_params=all_route_params, model_name_map=model_name_map + all_route_params=all_route_params, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) parameters.extend(operation_parameters) if parameters: @@ -236,7 +271,11 @@ def get_openapi_path( operation["parameters"] = list(all_parameters.values()) if method in METHODS_WITH_BODY: request_body_oai = get_openapi_operation_request_body( - body_field=route.body_field, model_name_map=model_name_map + body_field=route.body_field, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) if request_body_oai: operation["requestBody"] = request_body_oai @@ -250,8 +289,11 @@ def get_openapi_path( cb_definitions, ) = get_openapi_path( route=callback, - model_name_map=model_name_map, operation_ids=operation_ids, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) callbacks[callback.name] = {callback.path: cb_path} operation["callbacks"] = callbacks @@ -277,10 +319,12 @@ def get_openapi_path( response_schema = {"type": "string"} if lenient_issubclass(current_response_class, JSONResponse): if route.response_field: - response_schema, _, _ = field_schema( - route.response_field, + response_schema = get_schema_from_model_field( + field=route.response_field, + schema_generator=schema_generator, model_name_map=model_name_map, - ref_prefix=REF_PREFIX, + field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) else: response_schema = {} @@ -309,8 +353,12 @@ def get_openapi_path( field = route.response_fields.get(additional_status_code) additional_field_schema: Optional[Dict[str, Any]] = None if field: - additional_field_schema, _, _ = field_schema( - field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + additional_field_schema = get_schema_from_model_field( + field=field, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) media_type = route_response_media_type or "application/json" additional_schema = ( @@ -332,10 +380,8 @@ def get_openapi_path( openapi_response["description"] = description http422 = str(HTTP_422_UNPROCESSABLE_ENTITY) if (all_route_params or route.body_field) and not any( - [ - status in operation["responses"] - for status in [http422, "4XX", "default"] - ] + status in operation["responses"] + for status in [http422, "4XX", "default"] ): operation["responses"][http422] = { "description": "Validation Error", @@ -358,13 +404,13 @@ def get_openapi_path( return path, security_schemes, definitions -def get_flat_models_from_routes( +def get_fields_from_routes( routes: Sequence[BaseRoute], -) -> Set[Union[Type[BaseModel], Type[Enum]]]: +) -> List[ModelField]: body_fields_from_routes: List[ModelField] = [] responses_from_routes: List[ModelField] = [] request_fields_from_routes: List[ModelField] = [] - callback_flat_models: Set[Union[Type[BaseModel], Type[Enum]]] = set() + callback_flat_models: List[ModelField] = [] for route in routes: if getattr(route, "include_in_schema", None) and isinstance( route, routing.APIRoute @@ -379,13 +425,12 @@ def get_flat_models_from_routes( if route.response_fields: responses_from_routes.extend(route.response_fields.values()) if route.callbacks: - callback_flat_models |= get_flat_models_from_routes(route.callbacks) + callback_flat_models.extend(get_fields_from_routes(route.callbacks)) params = get_flat_params(route.dependant) request_fields_from_routes.extend(params) - flat_models = callback_flat_models | get_flat_models_from_fields( - body_fields_from_routes + responses_from_routes + request_fields_from_routes, - known_models=set(), + flat_models = callback_flat_models + list( + body_fields_from_routes + responses_from_routes + request_fields_from_routes ) return flat_models @@ -394,16 +439,21 @@ def get_openapi( *, title: str, version: str, - openapi_version: str = "3.0.2", + openapi_version: str = "3.1.0", + summary: Optional[str] = None, description: Optional[str] = None, routes: Sequence[BaseRoute], + webhooks: Optional[Sequence[BaseRoute]] = None, tags: Optional[List[Dict[str, Any]]] = None, servers: Optional[List[Dict[str, Union[str, Any]]]] = None, terms_of_service: Optional[str] = None, contact: Optional[Dict[str, Union[str, Any]]] = None, license_info: Optional[Dict[str, Union[str, Any]]] = None, + separate_input_output_schemas: bool = True, ) -> Dict[str, Any]: info: Dict[str, Any] = {"title": title, "version": version} + if summary: + info["summary"] = summary if description: info["description"] = description if terms_of_service: @@ -417,16 +467,26 @@ def get_openapi( output["servers"] = servers components: Dict[str, Dict[str, Any]] = {} paths: Dict[str, Dict[str, Any]] = {} + webhook_paths: Dict[str, Dict[str, Any]] = {} operation_ids: Set[str] = set() - flat_models = get_flat_models_from_routes(routes) - model_name_map = get_model_name_map(flat_models) - definitions = get_model_definitions( - flat_models=flat_models, model_name_map=model_name_map + all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or [])) + model_name_map = get_compat_model_name_map(all_fields) + schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) + field_mapping, definitions = get_definitions( + fields=all_fields, + schema_generator=schema_generator, + model_name_map=model_name_map, + separate_input_output_schemas=separate_input_output_schemas, ) - for route in routes: + for route in routes or []: if isinstance(route, routing.APIRoute): result = get_openapi_path( - route=route, model_name_map=model_name_map, operation_ids=operation_ids + route=route, + operation_ids=operation_ids, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) if result: path, security_schemes, path_definitions = result @@ -438,11 +498,33 @@ def get_openapi( ) if path_definitions: definitions.update(path_definitions) + for webhook in webhooks or []: + if isinstance(webhook, routing.APIRoute): + result = get_openapi_path( + route=webhook, + operation_ids=operation_ids, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, + ) + if result: + path, security_schemes, path_definitions = result + if path: + webhook_paths.setdefault(webhook.path_format, {}).update(path) + if security_schemes: + components.setdefault("securitySchemes", {}).update( + security_schemes + ) + if path_definitions: + definitions.update(path_definitions) if definitions: components["schemas"] = {k: definitions[k] for k in sorted(definitions)} if components: output["components"] = components output["paths"] = paths + if webhook_paths: + output["webhooks"] = webhook_paths if tags: output["tags"] = tags return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 1932ef0657..3f6dbc959d 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1,31 +1,315 @@ -from typing import Any, Callable, Dict, Optional, Sequence +from typing import Any, Callable, Dict, List, Optional, Sequence, Union from fastapi import params -from pydantic.fields import Undefined +from fastapi._compat import Undefined +from fastapi.openapi.models import Example +from typing_extensions import Annotated, Doc, deprecated # type: ignore [attr-defined] + +_Unset: Any = Undefined def Path( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = ..., *, - alias: Optional[str] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - **extra: Any, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, + regex: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: + """ + Declare a path parameter for a *path operation*. + + Read more about it in the + [FastAPI docs for Path Parameters and Numeric Validations](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/). + + ```python + from typing import Annotated + + from fastapi import FastAPI, Path + + app = FastAPI() + + + @app.get("/items/{item_id}") + async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + ): + return {"item_id": item_id} + ``` + """ return params.Path( default=default, + default_factory=default_factory, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -34,37 +318,302 @@ def Path( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, deprecated=deprecated, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) def Query( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + """ + ), + ] = Undefined, *, - alias: Optional[str] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - **extra: Any, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, + regex: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: return params.Query( default=default, + default_factory=default_factory, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -73,38 +622,313 @@ def Query( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, deprecated=deprecated, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) def Header( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + """ + ), + ] = Undefined, *, - alias: Optional[str] = None, - convert_underscores: bool = True, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - **extra: Any, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + convert_underscores: Annotated[ + bool, + Doc( + """ + Automatically convert underscores to hyphens in the parameter field name. + + Read more about it in the + [FastAPI docs for Header Parameters](https://fastapi.tiangolo.com/tutorial/header-params/#automatic-conversion) + """ + ), + ] = True, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, + regex: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: return params.Header( default=default, + default_factory=default_factory, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, convert_underscores=convert_underscores, title=title, description=description, @@ -114,37 +938,302 @@ def Header( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, deprecated=deprecated, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) def Cookie( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + """ + ), + ] = Undefined, *, - alias: Optional[str] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - **extra: Any, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, + regex: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: return params.Cookie( default=default, + default_factory=default_factory, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -153,39 +1242,327 @@ def Cookie( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, deprecated=deprecated, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) def Body( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + """ + ), + ] = Undefined, *, - embed: bool = False, - media_type: str = "application/json", - alias: Optional[str] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, - **extra: Any, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + embed: Annotated[ + bool, + Doc( + """ + When `embed` is `True`, the parameter will be expected in a JSON body as a + key instead of being the JSON body itself. + + This happens automatically when more than one `Body` parameter is declared. + + Read more about it in the + [FastAPI docs for Body - Multiple Parameters](https://fastapi.tiangolo.com/tutorial/body-multiple-params/#embed-a-single-body-parameter). + """ + ), + ] = False, + media_type: Annotated[ + str, + Doc( + """ + The media type of this parameter field. Changing it would affect the + generated OpenAPI, but currently it doesn't affect the parsing of the data. + """ + ), + ] = "application/json", + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, + regex: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: return params.Body( default=default, + default_factory=default_factory, embed=embed, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -194,35 +1571,312 @@ def Body( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, + deprecated=deprecated, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) def Form( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + """ + ), + ] = Undefined, *, - media_type: str = "application/x-www-form-urlencoded", - alias: Optional[str] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, - **extra: Any, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + media_type: Annotated[ + str, + Doc( + """ + The media type of this parameter field. Changing it would affect the + generated OpenAPI, but currently it doesn't affect the parsing of the data. + """ + ), + ] = "application/x-www-form-urlencoded", + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, + regex: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: return params.Form( default=default, + default_factory=default_factory, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -231,35 +1885,312 @@ def Form( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, + deprecated=deprecated, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) def File( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + """ + ), + ] = Undefined, *, - media_type: str = "multipart/form-data", - alias: Optional[str] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, - **extra: Any, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + media_type: Annotated[ + str, + Doc( + """ + The media type of this parameter field. Changing it would affect the + generated OpenAPI, but currently it doesn't affect the parsing of the data. + """ + ), + ] = "multipart/form-data", + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, + regex: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: return params.File( default=default, + default_factory=default_factory, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -268,23 +2199,162 @@ def File( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, + deprecated=deprecated, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) def Depends( # noqa: N802 - dependency: Optional[Callable[..., Any]] = None, *, use_cache: bool = True + dependency: Annotated[ + Optional[Callable[..., Any]], + Doc( + """ + A "dependable" callable (like a function). + + Don't call it directly, FastAPI will call it for you, just pass the object + directly. + """ + ), + ] = None, + *, + use_cache: Annotated[ + bool, + Doc( + """ + By default, after a dependency is called the first time in a request, if + the dependency is declared again for the rest of the request (for example + if the dependency is needed by several dependencies), the value will be + re-used for the rest of the request. + + Set `use_cache` to `False` to disable this behavior and ensure the + dependency is called again (if declared more than once) in the same request. + """ + ), + ] = True, ) -> Any: + """ + Declare a FastAPI dependency. + + It takes a single "dependable" callable (like a function). + + Don't call it directly, FastAPI will call it for you. + + Read more about it in the + [FastAPI docs for Dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/). + + **Example** + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + + app = FastAPI() + + + async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + + @app.get("/items/") + async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + ``` + """ return params.Depends(dependency=dependency, use_cache=use_cache) def Security( # noqa: N802 - dependency: Optional[Callable[..., Any]] = None, + dependency: Annotated[ + Optional[Callable[..., Any]], + Doc( + """ + A "dependable" callable (like a function). + + Don't call it directly, FastAPI will call it for you, just pass the object + directly. + """ + ), + ] = None, *, - scopes: Optional[Sequence[str]] = None, - use_cache: bool = True, + scopes: Annotated[ + Optional[Sequence[str]], + Doc( + """ + OAuth2 scopes required for the *path operation* that uses this Security + dependency. + + The term "scope" comes from the OAuth2 specification, it seems to be + intentionaly vague and interpretable. It normally refers to permissions, + in cases to roles. + + These scopes are integrated with OpenAPI (and the API docs at `/docs`). + So they are visible in the OpenAPI specification. + ) + """ + ), + ] = None, + use_cache: Annotated[ + bool, + Doc( + """ + By default, after a dependency is called the first time in a request, if + the dependency is declared again for the rest of the request (for example + if the dependency is needed by several dependencies), the value will be + re-used for the rest of the request. + + Set `use_cache` to `False` to disable this behavior and ensure the + dependency is called again (if declared more than once) in the same request. + """ + ), + ] = True, ) -> Any: + """ + Declare a FastAPI Security dependency. + + The only difference with a regular dependency is that it can declare OAuth2 + scopes that will be integrated with OpenAPI and the automatic UI docs (by default + at `/docs`). + + It takes a single "dependable" callable (like a function). + + Don't call it directly, FastAPI will call it for you. + + Read more about it in the + [FastAPI docs for Security](https://fastapi.tiangolo.com/tutorial/security/) and + in the + [FastAPI docs for OAuth2 scopes](https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/). + + **Example** + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + + from .db import User + from .security import get_current_active_user + + app = FastAPI() + + @app.get("/users/me/items/") + async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] + ): + return [{"item_id": "Foo", "owner": current_user.username}] + ``` + """ return params.Security(dependency=dependency, scopes=scopes, use_cache=use_cache) diff --git a/fastapi/params.py b/fastapi/params.py index 5395b98a39..b40944dba6 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -1,7 +1,14 @@ +import warnings from enum import Enum -from typing import Any, Callable, Dict, Optional, Sequence +from typing import Any, Callable, Dict, List, Optional, Sequence, Union -from pydantic.fields import FieldInfo, Undefined +from fastapi.openapi.models import Example +from pydantic.fields import FieldInfo +from typing_extensions import Annotated, deprecated + +from ._compat import PYDANTIC_V2, Undefined + +_Unset: Any = Undefined class ParamTypes(Enum): @@ -18,7 +25,14 @@ class Param(FieldInfo): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -27,19 +41,46 @@ class Param(FieldInfo): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): self.deprecated = deprecated + if example is not _Unset: + warnings.warn( + "`example` has been deprecated, please use `examples` instead", + category=DeprecationWarning, + stacklevel=4, + ) self.example = example - self.examples = examples self.include_in_schema = include_in_schema - super().__init__( + self.openapi_examples = openapi_examples + kwargs = dict( default=default, + default_factory=default_factory, alias=alias, title=title, description=description, @@ -49,9 +90,40 @@ class Param(FieldInfo): le=le, min_length=min_length, max_length=max_length, - regex=regex, + discriminator=discriminator, + multiple_of=multiple_of, + allow_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, **extra, ) + if examples is not None: + kwargs["examples"] = examples + if regex is not None: + warnings.warn( + "`regex` has been deprecated, please use `pattern` instead", + category=DeprecationWarning, + stacklevel=4, + ) + current_json_schema_extra = json_schema_extra or extra + if PYDANTIC_V2: + kwargs.update( + { + "annotation": annotation, + "alias_priority": alias_priority, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "strict": strict, + "json_schema_extra": current_json_schema_extra, + } + ) + kwargs["pattern"] = pattern or regex + else: + kwargs["regex"] = pattern or regex + kwargs.update(**current_json_schema_extra) + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} + + super().__init__(**use_kwargs) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.default})" @@ -62,9 +134,16 @@ class Path(Param): def __init__( self, - default: Any = Undefined, + default: Any = ..., *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -73,17 +152,43 @@ class Path(Param): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): + assert default is ..., "Path parameters cannot have a default value" self.in_ = self.in_ super().__init__( - default=..., + default=default, + default_factory=default_factory, + annotation=annotation, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -92,11 +197,20 @@ class Path(Param): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, deprecated=deprecated, example=example, examples=examples, + openapi_examples=openapi_examples, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -108,7 +222,14 @@ class Query(Param): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -117,16 +238,41 @@ class Query(Param): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -135,11 +281,20 @@ class Query(Param): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, deprecated=deprecated, example=example, examples=examples, + openapi_examples=openapi_examples, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -151,7 +306,14 @@ class Header(Param): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, convert_underscores: bool = True, title: Optional[str] = None, description: Optional[str] = None, @@ -161,17 +323,42 @@ class Header(Param): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): self.convert_underscores = convert_underscores super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -180,11 +367,20 @@ class Header(Param): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, deprecated=deprecated, example=example, examples=examples, + openapi_examples=openapi_examples, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -196,7 +392,14 @@ class Cookie(Param): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -205,16 +408,41 @@ class Cookie(Param): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -223,11 +451,20 @@ class Cookie(Param): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, deprecated=deprecated, example=example, examples=examples, + openapi_examples=openapi_examples, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -237,9 +474,16 @@ class Body(FieldInfo): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, embed: bool = False, media_type: str = "application/json", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -248,17 +492,48 @@ class Body(FieldInfo): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): self.embed = embed self.media_type = media_type + self.deprecated = deprecated + if example is not _Unset: + warnings.warn( + "`example` has been deprecated, please use `examples` instead", + category=DeprecationWarning, + stacklevel=4, + ) self.example = example - self.examples = examples - super().__init__( + self.include_in_schema = include_in_schema + self.openapi_examples = openapi_examples + kwargs = dict( default=default, + default_factory=default_factory, alias=alias, title=title, description=description, @@ -268,9 +543,41 @@ class Body(FieldInfo): le=le, min_length=min_length, max_length=max_length, - regex=regex, + discriminator=discriminator, + multiple_of=multiple_of, + allow_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, **extra, ) + if examples is not None: + kwargs["examples"] = examples + if regex is not None: + warnings.warn( + "`regex` has been depreacated, please use `pattern` instead", + category=DeprecationWarning, + stacklevel=4, + ) + current_json_schema_extra = json_schema_extra or extra + if PYDANTIC_V2: + kwargs.update( + { + "annotation": annotation, + "alias_priority": alias_priority, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "strict": strict, + "json_schema_extra": current_json_schema_extra, + } + ) + kwargs["pattern"] = pattern or regex + else: + kwargs["regex"] = pattern or regex + kwargs.update(**current_json_schema_extra) + + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} + + super().__init__(**use_kwargs) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.default})" @@ -279,10 +586,17 @@ class Body(FieldInfo): class Form(Body): def __init__( self, - default: Any, + default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, media_type: str = "application/x-www-form-urlencoded", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -291,16 +605,43 @@ class Form(Body): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, embed=True, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -309,9 +650,20 @@ class Form(Body): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, example=example, examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -319,10 +671,17 @@ class Form(Body): class File(Form): def __init__( self, - default: Any, + default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, media_type: str = "multipart/form-data", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -331,15 +690,42 @@ class File(Form): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -348,9 +734,20 @@ class File(Form): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, example=example, examples=examples, + openapi_examples=openapi_examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) diff --git a/fastapi/responses.py b/fastapi/responses.py index 88dba96e8f..6c8db6f335 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -21,13 +21,25 @@ except ImportError: # pragma: nocover class UJSONResponse(JSONResponse): + """ + JSON response using the high-performance ujson library to serialize data to JSON. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + """ + def render(self, content: Any) -> bytes: assert ujson is not None, "ujson must be installed to use UJSONResponse" return ujson.dumps(content, ensure_ascii=False).encode("utf-8") class ORJSONResponse(JSONResponse): - media_type = "application/json" + """ + JSON response using the high-performance orjson library to serialize data to JSON. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + """ def render(self, content: Any) -> bytes: assert orjson is not None, "orjson must be installed to use ORJSONResponse" diff --git a/fastapi/routing.py b/fastapi/routing.py index 8c73b954f1..acebabfca0 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -20,6 +20,14 @@ from typing import ( ) from fastapi import params +from fastapi._compat import ( + ModelField, + Undefined, + _get_model_config, + _model_dump, + _normalize_errors, + lenient_issubclass, +) from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.dependencies.models import Dependant from fastapi.dependencies.utils import ( @@ -29,9 +37,14 @@ from fastapi.dependencies.utils import ( get_typed_return_annotation, solve_dependencies, ) -from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder -from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError -from fastapi.types import DecoratedCallable +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import ( + FastAPIError, + RequestValidationError, + ResponseValidationError, + WebSocketRequestValidationError, +) +from fastapi.types import DecoratedCallable, IncEx from fastapi.utils import ( create_cloned_field, create_response_field, @@ -40,24 +53,23 @@ from fastapi.utils import ( is_body_allowed_for_status_code, ) from pydantic import BaseModel -from pydantic.error_wrappers import ErrorWrapper, ValidationError -from pydantic.fields import ModelField, Undefined from starlette import routing from starlette.concurrency import run_in_threadpool from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.responses import JSONResponse, Response -from starlette.routing import BaseRoute, Match -from starlette.routing import Mount as Mount # noqa from starlette.routing import ( + BaseRoute, + Match, compile_path, get_name, request_response, websocket_session, ) -from starlette.status import WS_1008_POLICY_VIOLATION -from starlette.types import ASGIApp, Scope +from starlette.routing import Mount as Mount # noqa +from starlette.types import ASGIApp, Lifespan, Scope from starlette.websockets import WebSocket +from typing_extensions import Annotated, Doc, deprecated # type: ignore [attr-defined] def _prepare_response_content( @@ -68,14 +80,15 @@ def _prepare_response_content( exclude_none: bool = False, ) -> Any: if isinstance(res, BaseModel): - read_with_orm_mode = getattr(res.__config__, "read_with_orm_mode", None) + read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) if read_with_orm_mode: # Let from_orm extract the data from this model instead of converting # it now to a dict. - # Otherwise there's no way to extract lazy data that requires attribute + # Otherwise, there's no way to extract lazy data that requires attribute # access instead of dict iteration, e.g. lazy relationships. return res - return res.dict( + return _model_dump( + res, by_alias=True, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, @@ -110,8 +123,8 @@ async def serialize_response( *, field: Optional[ModelField] = None, response_content: Any, - include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + include: Optional[IncEx] = None, + exclude: Optional[IncEx] = None, by_alias: bool = True, exclude_unset: bool = False, exclude_defaults: bool = False, @@ -120,24 +133,40 @@ async def serialize_response( ) -> Any: if field: errors = [] - response_content = _prepare_response_content( - response_content, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) + if not hasattr(field, "serialize"): + # pydantic v1 + response_content = _prepare_response_content( + response_content, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) if is_coroutine: value, errors_ = field.validate(response_content, {}, loc=("response",)) else: value, errors_ = await run_in_threadpool( field.validate, response_content, {}, loc=("response",) ) - if isinstance(errors_, ErrorWrapper): - errors.append(errors_) - elif isinstance(errors_, list): + if isinstance(errors_, list): errors.extend(errors_) + elif errors_: + errors.append(errors_) if errors: - raise ValidationError(errors, field.type_) + raise ResponseValidationError( + errors=_normalize_errors(errors), body=response_content + ) + + if hasattr(field, "serialize"): + return field.serialize( + value, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + return jsonable_encoder( value, include=include, @@ -170,8 +199,8 @@ def get_request_handler( status_code: Optional[int] = None, response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse), response_field: Optional[ModelField] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -187,86 +216,124 @@ def get_request_handler( actual_response_class = response_class async def app(request: Request) -> Response: - try: - body: Any = None - if body_field: - if is_body_form: - body = await request.form() - stack = request.scope.get("fastapi_astack") - assert isinstance(stack, AsyncExitStack) - stack.push_async_callback(body.close) + exception_to_reraise: Optional[Exception] = None + response: Union[Response, None] = None + async with AsyncExitStack() as async_exit_stack: + # TODO: remove this scope later, after a few releases + # This scope fastapi_astack is no longer used by FastAPI, kept for + # compatibility, just in case + request.scope["fastapi_astack"] = async_exit_stack + try: + body: Any = None + if body_field: + if is_body_form: + body = await request.form() + async_exit_stack.push_async_callback(body.close) + else: + body_bytes = await request.body() + if body_bytes: + json_body: Any = Undefined + content_type_value = request.headers.get("content-type") + if not content_type_value: + json_body = await request.json() + else: + message = email.message.Message() + message["content-type"] = content_type_value + if message.get_content_maintype() == "application": + subtype = message.get_content_subtype() + if subtype == "json" or subtype.endswith("+json"): + json_body = await request.json() + if json_body != Undefined: + body = json_body + else: + body = body_bytes + except json.JSONDecodeError as e: + validation_error = RequestValidationError( + [ + { + "type": "json_invalid", + "loc": ("body", e.pos), + "msg": "JSON decode error", + "input": {}, + "ctx": {"error": e.msg}, + } + ], + body=e.doc, + ) + exception_to_reraise = validation_error + raise validation_error from e + except HTTPException as e: + exception_to_reraise = e + raise + except Exception as e: + http_error = HTTPException( + status_code=400, detail="There was an error parsing the body" + ) + exception_to_reraise = http_error + raise http_error from e + try: + solved_result = await solve_dependencies( + request=request, + dependant=dependant, + body=body, + dependency_overrides_provider=dependency_overrides_provider, + async_exit_stack=async_exit_stack, + ) + values, errors, background_tasks, sub_response, _ = solved_result + except Exception as e: + exception_to_reraise = e + raise e + if errors: + validation_error = RequestValidationError( + _normalize_errors(errors), body=body + ) + exception_to_reraise = validation_error + raise validation_error + else: + try: + raw_response = await run_endpoint_function( + dependant=dependant, values=values, is_coroutine=is_coroutine + ) + except Exception as e: + exception_to_reraise = e + raise e + if isinstance(raw_response, Response): + if raw_response.background is None: + raw_response.background = background_tasks + response = raw_response else: - body_bytes = await request.body() - if body_bytes: - json_body: Any = Undefined - content_type_value = request.headers.get("content-type") - if not content_type_value: - json_body = await request.json() - else: - message = email.message.Message() - message["content-type"] = content_type_value - if message.get_content_maintype() == "application": - subtype = message.get_content_subtype() - if subtype == "json" or subtype.endswith("+json"): - json_body = await request.json() - if json_body != Undefined: - body = json_body - else: - body = body_bytes - except json.JSONDecodeError as e: - raise RequestValidationError( - [ErrorWrapper(e, ("body", e.pos))], body=e.doc - ) from e - except HTTPException: - raise - except Exception as e: - raise HTTPException( - status_code=400, detail="There was an error parsing the body" - ) from e - solved_result = await solve_dependencies( - request=request, - dependant=dependant, - body=body, - dependency_overrides_provider=dependency_overrides_provider, - ) - values, errors, background_tasks, sub_response, _ = solved_result - if errors: - raise RequestValidationError(errors, body=body) - else: - raw_response = await run_endpoint_function( - dependant=dependant, values=values, is_coroutine=is_coroutine - ) - - if isinstance(raw_response, Response): - if raw_response.background is None: - raw_response.background = background_tasks - return raw_response - response_args: Dict[str, Any] = {"background": background_tasks} - # If status_code was set, use it, otherwise use the default from the - # response class, in the case of redirect it's 307 - current_status_code = ( - status_code if status_code else sub_response.status_code - ) - if current_status_code is not None: - response_args["status_code"] = current_status_code - if sub_response.status_code: - response_args["status_code"] = sub_response.status_code - content = await serialize_response( - field=response_field, - response_content=raw_response, - include=response_model_include, - exclude=response_model_exclude, - by_alias=response_model_by_alias, - exclude_unset=response_model_exclude_unset, - exclude_defaults=response_model_exclude_defaults, - exclude_none=response_model_exclude_none, - is_coroutine=is_coroutine, - ) - response = actual_response_class(content, **response_args) - if not is_body_allowed_for_status_code(response.status_code): - response.body = b"" - response.headers.raw.extend(sub_response.headers.raw) - return response + response_args: Dict[str, Any] = {"background": background_tasks} + # If status_code was set, use it, otherwise use the default from the + # response class, in the case of redirect it's 307 + current_status_code = ( + status_code if status_code else sub_response.status_code + ) + if current_status_code is not None: + response_args["status_code"] = current_status_code + if sub_response.status_code: + response_args["status_code"] = sub_response.status_code + content = await serialize_response( + field=response_field, + response_content=raw_response, + include=response_model_include, + exclude=response_model_exclude, + by_alias=response_model_by_alias, + exclude_unset=response_model_exclude_unset, + exclude_defaults=response_model_exclude_defaults, + exclude_none=response_model_exclude_none, + is_coroutine=is_coroutine, + ) + response = actual_response_class(content, **response_args) + if not is_body_allowed_for_status_code(response.status_code): + response.body = b"" + response.headers.raw.extend(sub_response.headers.raw) + # This exception was possibly handled by the dependency but it should + # still bubble up so that the ServerErrorMiddleware can return a 500 + # or the ExceptionMiddleware can catch and handle any other exceptions + if exception_to_reraise: + raise exception_to_reraise + assert response is not None, "An error occurred while generating the request" + return response return app @@ -275,17 +342,22 @@ def get_websocket_app( dependant: Dependant, dependency_overrides_provider: Optional[Any] = None ) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]: async def app(websocket: WebSocket) -> None: - solved_result = await solve_dependencies( - request=websocket, - dependant=dependant, - dependency_overrides_provider=dependency_overrides_provider, - ) - values, errors, _, _2, _3 = solved_result - if errors: - await websocket.close(code=WS_1008_POLICY_VIOLATION) - raise WebSocketRequestValidationError(errors) - assert dependant.call is not None, "dependant.call must be a function" - await dependant.call(**values) + async with AsyncExitStack() as async_exit_stack: + # TODO: remove this scope later, after a few releases + # This scope fastapi_astack is no longer used by FastAPI, kept for + # compatibility, just in case + websocket.scope["fastapi_astack"] = async_exit_stack + solved_result = await solve_dependencies( + request=websocket, + dependant=dependant, + dependency_overrides_provider=dependency_overrides_provider, + async_exit_stack=async_exit_stack, + ) + values, errors, _, _2, _3 = solved_result + if errors: + raise WebSocketRequestValidationError(_normalize_errors(errors)) + assert dependant.call is not None, "dependant.call must be a function" + await dependant.call(**values) return app @@ -297,13 +369,21 @@ class APIWebSocketRoute(routing.WebSocketRoute): endpoint: Callable[..., Any], *, name: Optional[str] = None, + dependencies: Optional[Sequence[params.Depends]] = None, dependency_overrides_provider: Optional[Any] = None, ) -> None: self.path = path self.endpoint = endpoint self.name = get_name(endpoint) if name is None else name + self.dependencies = list(dependencies or []) self.path_regex, self.path_format, self.param_convertors = compile_path(path) self.dependant = get_dependant(path=self.path_format, call=self.endpoint) + for depends in self.dependencies[::-1]: + self.dependant.dependencies.insert( + 0, + get_parameterless_sub_dependant(depends=depends, path=self.path_format), + ) + self.app = websocket_session( get_websocket_app( dependant=self.dependant, @@ -336,8 +416,8 @@ class APIRoute(routing.Route): name: Optional[str] = None, methods: Optional[Union[Set[str], List[str]]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -356,7 +436,11 @@ class APIRoute(routing.Route): self.path = path self.endpoint = endpoint if isinstance(response_model, DefaultPlaceholder): - response_model = get_typed_return_annotation(endpoint) + return_annotation = get_typed_return_annotation(endpoint) + if lenient_issubclass(return_annotation, Response): + response_model = None + else: + response_model = return_annotation self.response_model = response_model self.summary = summary self.response_description = response_description @@ -398,7 +482,9 @@ class APIRoute(routing.Route): ), f"Status code {status_code} must not have a response body" response_name = "Response_" + self.unique_id self.response_field = create_response_field( - name=response_name, type_=self.response_model + name=response_name, + type_=self.response_model, + mode="serialization", ) # Create a clone of the field, so that a Pydantic submodel is not returned # as is just because it's an instance of a subclass of a more limited class @@ -406,17 +492,15 @@ class APIRoute(routing.Route): # that doesn't have the hashed_password. But because it's a subclass, it # would pass the validation and be returned as is. # By being a new field, no inheritance will be passed as is. A new model - # will be always created. + # will always be created. + # TODO: remove when deprecating Pydantic v1 self.secure_cloned_response_field: Optional[ ModelField ] = create_cloned_field(self.response_field) else: self.response_field = None # type: ignore self.secure_cloned_response_field = None - if dependencies: - self.dependencies = list(dependencies) - else: - self.dependencies = [] + self.dependencies = list(dependencies or []) self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "") # if a "form feed" character (page break) is found in the description text, # truncate description text to the content preceding the first "form feed" @@ -471,27 +555,246 @@ class APIRoute(routing.Route): class APIRouter(routing.Router): + """ + `APIRouter` class, used to group *path operations*, for example to structure + an app in multiple files. It would then be included in the `FastAPI` app, or + in another `APIRouter` (ultimately included in the app). + + Read more about it in the + [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/). + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + + app = FastAPI() + router = APIRouter() + + + @router.get("/users/", tags=["users"]) + async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] + + + app.include_router(router) + ``` + """ + def __init__( self, *, - prefix: str = "", - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - default_response_class: Type[Response] = Default(JSONResponse), - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - callbacks: Optional[List[BaseRoute]] = None, - routes: Optional[List[routing.BaseRoute]] = None, - redirect_slashes: bool = True, - default: Optional[ASGIApp] = None, - dependency_overrides_provider: Optional[Any] = None, - route_class: Type[APIRoute] = APIRoute, - on_startup: Optional[Sequence[Callable[[], Any]]] = None, - on_shutdown: Optional[Sequence[Callable[[], Any]]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "", + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to all the *path operations* in this + router. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to all the + *path operations* in this router. + + Read more about it in the + [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """ + ), + ] = None, + default_response_class: Annotated[ + Type[Response], + Doc( + """ + The default response class to be used. + + Read more in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). + """ + ), + ] = Default(JSONResponse), + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses to be shown in OpenAPI. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/). + + And in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + OpenAPI callbacks that should apply to all *path operations* in this + router. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + routes: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + **Note**: you probably shouldn't use this parameter, it is inherited + from Starlette and supported for compatibility. + + --- + + A list of routes to serve incoming HTTP and WebSocket requests. + """ + ), + deprecated( + """ + You normally wouldn't use this parameter with FastAPI, it is inherited + from Starlette and supported for compatibility. + + In FastAPI, you normally would use the *path operation methods*, + like `router.get()`, `router.post()`, etc. + """ + ), + ] = None, + redirect_slashes: Annotated[ + bool, + Doc( + """ + Whether to detect and redirect slashes in URLs when the client doesn't + use the same format. + """ + ), + ] = True, + default: Annotated[ + Optional[ASGIApp], + Doc( + """ + Default function handler for this router. Used to handle + 404 Not Found errors. + """ + ), + ] = None, + dependency_overrides_provider: Annotated[ + Optional[Any], + Doc( + """ + Only used internally by FastAPI to handle dependency overrides. + + You shouldn't need to use it. It normally points to the `FastAPI` app + object. + """ + ), + ] = None, + route_class: Annotated[ + Type[APIRoute], + Doc( + """ + Custom route (*path operation*) class to be used by this router. + + Read more about it in the + [FastAPI docs for Custom Request and APIRoute class](https://fastapi.tiangolo.com/how-to/custom-request-and-route/#custom-apiroute-class-in-a-router). + """ + ), + ] = APIRoute, + on_startup: Annotated[ + Optional[Sequence[Callable[[], Any]]], + Doc( + """ + A list of startup event handler functions. + + You should instead use the `lifespan` handlers. + + Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """ + ), + ] = None, + on_shutdown: Annotated[ + Optional[Sequence[Callable[[], Any]]], + Doc( + """ + A list of shutdown event handler functions. + + You should instead use the `lifespan` handlers. + + Read more in the + [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """ + ), + ] = None, + # the generic to Lifespan[AppType] is the type of the top level application + # which the router cannot know statically, so we use typing.Any + lifespan: Annotated[ + Optional[Lifespan[Any]], + Doc( + """ + A `Lifespan` context manager handler. This replaces `startup` and + `shutdown` functions with a single context manager. + + Read more in the + [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark all *path operations* in this router as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) all the *path operations* in this router in the + generated OpenAPI. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> None: super().__init__( routes=routes, @@ -499,6 +802,7 @@ class APIRouter(routing.Router): default=default, on_startup=on_startup, on_shutdown=on_shutdown, + lifespan=lifespan, ) if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" @@ -507,7 +811,7 @@ class APIRouter(routing.Router): ), "A path prefix must not end with '/', as the routes will start with '/'" self.prefix = prefix self.tags: List[Union[str, Enum]] = tags or [] - self.dependencies = list(dependencies or []) or [] + self.dependencies = list(dependencies or []) self.deprecated = deprecated self.include_in_schema = include_in_schema self.responses = responses or {} @@ -517,6 +821,25 @@ class APIRouter(routing.Router): self.default_response_class = default_response_class self.generate_unique_id_function = generate_unique_id_function + def route( + self, + path: str, + methods: Optional[List[str]] = None, + name: Optional[str] = None, + include_in_schema: bool = True, + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_route( + path, + func, + methods=methods, + name=name, + include_in_schema=include_in_schema, + ) + return func + + return decorator + def add_api_route( self, path: str, @@ -533,8 +856,8 @@ class APIRouter(routing.Router): deprecated: Optional[bool] = None, methods: Optional[Union[Set[str], List[str]]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -614,8 +937,8 @@ class APIRouter(routing.Router): deprecated: Optional[bool] = None, methods: Optional[List[str]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -662,41 +985,237 @@ class APIRouter(routing.Router): return decorator def add_api_websocket_route( - self, path: str, endpoint: Callable[..., Any], name: Optional[str] = None + self, + path: str, + endpoint: Callable[..., Any], + name: Optional[str] = None, + *, + dependencies: Optional[Sequence[params.Depends]] = None, ) -> None: + current_dependencies = self.dependencies.copy() + if dependencies: + current_dependencies.extend(dependencies) + route = APIWebSocketRoute( self.prefix + path, endpoint=endpoint, name=name, + dependencies=current_dependencies, dependency_overrides_provider=self.dependency_overrides_provider, ) self.routes.append(route) def websocket( - self, path: str, name: Optional[str] = None + self, + path: Annotated[ + str, + Doc( + """ + WebSocket path. + """ + ), + ], + name: Annotated[ + Optional[str], + Doc( + """ + A name for the WebSocket. Only used internally. + """ + ), + ] = None, + *, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be used for this + WebSocket. + + Read more about it in the + [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/). + """ + ), + ] = None, + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Decorate a WebSocket function. + + Read more about it in the + [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/). + + **Example** + + ## Example + + ```python + from fastapi import APIRouter, FastAPI, WebSocket + + app = FastAPI() + router = APIRouter() + + @router.websocket("/ws") + async def websocket_endpoint(websocket: WebSocket): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text(f"Message text was: {data}") + + app.include_router(router) + ``` + """ + + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_api_websocket_route( + path, func, name=name, dependencies=dependencies + ) + return func + + return decorator + + def websocket_route( + self, path: str, name: Union[str, None] = None ) -> Callable[[DecoratedCallable], DecoratedCallable]: def decorator(func: DecoratedCallable) -> DecoratedCallable: - self.add_api_websocket_route(path, func, name=name) + self.add_websocket_route(path, func, name=name) return func return decorator def include_router( self, - router: "APIRouter", + router: Annotated["APIRouter", Doc("The `APIRouter` to include.")], *, - prefix: str = "", - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - default_response_class: Type[Response] = Default(JSONResponse), - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - callbacks: Optional[List[BaseRoute]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "", + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to all the *path operations* in this + router. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to all the + *path operations* in this router. + + Read more about it in the + [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """ + ), + ] = None, + default_response_class: Annotated[ + Type[Response], + Doc( + """ + The default response class to be used. + + Read more in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). + """ + ), + ] = Default(JSONResponse), + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses to be shown in OpenAPI. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/). + + And in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + OpenAPI callbacks that should apply to all *path operations* in this + router. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark all *path operations* in this router as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include (or not) all the *path operations* in this router in the + generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> None: + """ + Include another `APIRouter` in the same current `APIRouter`. + + Read more about it in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/). + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + + app = FastAPI() + internal_router = APIRouter() + users_router = APIRouter() + + @users_router.get("/users/") + def read_users(): + return [{"name": "Rick"}, {"name": "Morty"}] + + internal_router.include_router(users_router) + app.include_router(internal_router) + ``` + """ if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" assert not prefix.endswith( @@ -707,7 +1226,7 @@ class APIRouter(routing.Router): path = getattr(r, "path") # noqa: B009 name = getattr(r, "name", "unknown") if path is not None and not path: - raise Exception( + raise FastAPIError( f"Prefix and path cannot be both empty (path operation: {name})" ) if responses is None: @@ -782,8 +1301,16 @@ class APIRouter(routing.Router): name=route.name, ) elif isinstance(route, APIWebSocketRoute): + current_dependencies = [] + if dependencies: + current_dependencies.extend(dependencies) + if route.dependencies: + current_dependencies.extend(route.dependencies) self.add_api_websocket_route( - prefix + route.path, route.endpoint, name=route.name + prefix + route.path, + route.endpoint, + dependencies=current_dependencies, + name=route.name, ) elif isinstance(route, routing.WebSocketRoute): self.add_websocket_route( @@ -796,33 +1323,354 @@ class APIRouter(routing.Router): def get( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP GET operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + + app = FastAPI() + router = APIRouter() + + @router.get("/items/") + def read_items(): + return [{"name": "Empanada"}, {"name": "Arepa"}] + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -852,33 +1700,359 @@ class APIRouter(routing.Router): def put( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP PUT operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + router = APIRouter() + + @router.put("/items/{item_id}") + def replace_item(item_id: str, item: Item): + return {"message": "Item replaced", "id": item_id} + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -908,33 +2082,359 @@ class APIRouter(routing.Router): def post( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP POST operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + router = APIRouter() + + @router.post("/items/") + def create_item(item: Item): + return {"message": "Item created"} + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -964,33 +2464,354 @@ class APIRouter(routing.Router): def delete( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP DELETE operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + + app = FastAPI() + router = APIRouter() + + @router.delete("/items/{item_id}") + def delete_item(item_id: str): + return {"message": "Item deleted"} + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -1020,33 +2841,354 @@ class APIRouter(routing.Router): def options( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP OPTIONS operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + + app = FastAPI() + router = APIRouter() + + @router.options("/items/") + def get_item_options(): + return {"additions": ["Aji", "Guacamole"]} + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -1076,33 +3218,359 @@ class APIRouter(routing.Router): def head( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP HEAD operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + router = APIRouter() + + @router.head("/items/", status_code=204) + def get_items_headers(response: Response): + response.headers["X-Cat-Dog"] = "Alone in the world" + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -1132,33 +3600,359 @@ class APIRouter(routing.Router): def patch( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP PATCH operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + router = APIRouter() + + @router.patch("/items/") + def update_item(item: Item): + return {"message": "Item updated in place"} + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -1188,34 +3982,359 @@ class APIRouter(routing.Router): def trace( self, - path: str, - *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), - ) -> Callable[[DecoratedCallable], DecoratedCallable]: + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], + *, + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP TRACE operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + router = APIRouter() + + @router.trace("/items/{item_id}") + def trace_item(item_id: str): + return None + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -1242,3 +4361,37 @@ class APIRouter(routing.Router): openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, ) + + @deprecated( + """ + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + """ + ) + def on_event( + self, + event_type: Annotated[ + str, + Doc( + """ + The type of event. `startup` or `shutdown`. + """ + ), + ], + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add an event handler for the router. + + `on_event` is deprecated, use `lifespan` event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/#alternative-events-deprecated). + """ + + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_event_handler(event_type, func) + return func + + return decorator diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 24ddbf4825..b1a6b4f94b 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -5,6 +5,7 @@ from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.status import HTTP_403_FORBIDDEN +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] class APIKeyBase(SecurityBase): @@ -12,16 +13,88 @@ class APIKeyBase(SecurityBase): class APIKeyQuery(APIKeyBase): + """ + API key authentication using a query parameter. + + This defines the name of the query parameter that should be provided in the request + with the API key and integrates that into the OpenAPI documentation. It extracts + the key value sent in the query parameter automatically and provides it as the + dependency result. But it doesn't define how to send that API key to the client. + + ## Usage + + Create an instance object and use that object as the dependency in `Depends()`. + + The dependency result will be a string containing the key value. + + ## Example + + ```python + from fastapi import Depends, FastAPI + from fastapi.security import APIKeyQuery + + app = FastAPI() + + query_scheme = APIKeyQuery(name="api_key") + + + @app.get("/items/") + async def read_items(api_key: str = Depends(query_scheme)): + return {"api_key": api_key} + ``` + """ + def __init__( self, *, - name: str, - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True + name: Annotated[ + str, + Doc("Query parameter name."), + ], + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if the query parameter is not provided, `APIKeyQuery` will + automatically cancel the request and sebd the client an error. + + If `auto_error` is set to `False`, when the query parameter is not + available, instead of erroring out, the dependency result will be + `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, in a query + parameter or in an HTTP Bearer token). + """ + ), + ] = True, ): self.model: APIKey = APIKey( - **{"in": APIKeyIn.query}, name=name, description=description + **{"in": APIKeyIn.query}, # type: ignore[arg-type] + name=name, + description=description, ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error @@ -39,16 +112,84 @@ class APIKeyQuery(APIKeyBase): class APIKeyHeader(APIKeyBase): + """ + API key authentication using a header. + + This defines the name of the header that should be provided in the request with + the API key and integrates that into the OpenAPI documentation. It extracts + the key value sent in the header automatically and provides it as the dependency + result. But it doesn't define how to send that key to the client. + + ## Usage + + Create an instance object and use that object as the dependency in `Depends()`. + + The dependency result will be a string containing the key value. + + ## Example + + ```python + from fastapi import Depends, FastAPI + from fastapi.security import APIKeyHeader + + app = FastAPI() + + header_scheme = APIKeyHeader(name="x-key") + + + @app.get("/items/") + async def read_items(key: str = Depends(header_scheme)): + return {"key": key} + ``` + """ + def __init__( self, *, - name: str, - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True + name: Annotated[str, Doc("Header name.")], + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if the header is not provided, `APIKeyHeader` will + automatically cancel the request and send the client an error. + + If `auto_error` is set to `False`, when the header is not available, + instead of erroring out, the dependency result will be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, in a header or + in an HTTP Bearer token). + """ + ), + ] = True, ): self.model: APIKey = APIKey( - **{"in": APIKeyIn.header}, name=name, description=description + **{"in": APIKeyIn.header}, # type: ignore[arg-type] + name=name, + description=description, ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error @@ -66,16 +207,84 @@ class APIKeyHeader(APIKeyBase): class APIKeyCookie(APIKeyBase): + """ + API key authentication using a cookie. + + This defines the name of the cookie that should be provided in the request with + the API key and integrates that into the OpenAPI documentation. It extracts + the key value sent in the cookie automatically and provides it as the dependency + result. But it doesn't define how to set that cookie. + + ## Usage + + Create an instance object and use that object as the dependency in `Depends()`. + + The dependency result will be a string containing the key value. + + ## Example + + ```python + from fastapi import Depends, FastAPI + from fastapi.security import APIKeyCookie + + app = FastAPI() + + cookie_scheme = APIKeyCookie(name="session") + + + @app.get("/items/") + async def read_items(session: str = Depends(cookie_scheme)): + return {"session": session} + ``` + """ + def __init__( self, *, - name: str, - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True + name: Annotated[str, Doc("Cookie name.")], + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if the cookie is not provided, `APIKeyCookie` will + automatically cancel the request and send the client an error. + + If `auto_error` is set to `False`, when the cookie is not available, + instead of erroring out, the dependency result will be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, in a cookie or + in an HTTP Bearer token). + """ + ), + ] = True, ): self.model: APIKey = APIKey( - **{"in": APIKeyIn.cookie}, name=name, description=description + **{"in": APIKeyIn.cookie}, # type: ignore[arg-type] + name=name, + description=description, ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 8b677299dd..738455de38 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -10,16 +10,60 @@ from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] class HTTPBasicCredentials(BaseModel): - username: str - password: str + """ + The HTTP Basic credendials given as the result of using `HTTPBasic` in a + dependency. + + Read more about it in the + [FastAPI docs for HTTP Basic Auth](https://fastapi.tiangolo.com/advanced/security/http-basic-auth/). + """ + + username: Annotated[str, Doc("The HTTP Basic username.")] + password: Annotated[str, Doc("The HTTP Basic password.")] class HTTPAuthorizationCredentials(BaseModel): - scheme: str - credentials: str + """ + The HTTP authorization credentials in the result of using `HTTPBearer` or + `HTTPDigest` in a dependency. + + The HTTP authorization header value is split by the first space. + + The first part is the `scheme`, the second part is the `credentials`. + + For example, in an HTTP Bearer token scheme, the client will send a header + like: + + ``` + Authorization: Bearer deadbeef12346 + ``` + + In this case: + + * `scheme` will have the value `"Bearer"` + * `credentials` will have the value `"deadbeef12346"` + """ + + scheme: Annotated[ + str, + Doc( + """ + The HTTP authorization scheme extracted from the header value. + """ + ), + ] + credentials: Annotated[ + str, + Doc( + """ + The HTTP authorization credentials extracted from the header value. + """ + ), + ] class HTTPBase(SecurityBase): @@ -51,13 +95,89 @@ class HTTPBase(SecurityBase): class HTTPBasic(HTTPBase): + """ + HTTP Basic authentication. + + ## Usage + + Create an instance object and use that object as the dependency in `Depends()`. + + The dependency result will be an `HTTPBasicCredentials` object containing the + `username` and the `password`. + + Read more about it in the + [FastAPI docs for HTTP Basic Auth](https://fastapi.tiangolo.com/advanced/security/http-basic-auth/). + + ## Example + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + from fastapi.security import HTTPBasic, HTTPBasicCredentials + + app = FastAPI() + + security = HTTPBasic() + + + @app.get("/users/me") + def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): + return {"username": credentials.username, "password": credentials.password} + ``` + """ + def __init__( self, *, - scheme_name: Optional[str] = None, - realm: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True, + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + realm: Annotated[ + Optional[str], + Doc( + """ + HTTP Basic authentication realm. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if the HTTP Basic authentication is not provided (a + header), `HTTPBasic` will automatically cancel the request and send the + client an error. + + If `auto_error` is set to `False`, when the HTTP Basic authentication + is not available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, in HTTP Basic + authentication or in an HTTP Bearer token). + """ + ), + ] = True, ): self.model = HTTPBaseModel(scheme="basic", description=description) self.scheme_name = scheme_name or self.__class__.__name__ @@ -73,11 +193,6 @@ class HTTPBasic(HTTPBase): unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'} else: unauthorized_headers = {"WWW-Authenticate": "Basic"} - invalid_user_credentials_exc = HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", - headers=unauthorized_headers, - ) if not authorization or scheme.lower() != "basic": if self.auto_error: raise HTTPException( @@ -87,10 +202,15 @@ class HTTPBasic(HTTPBase): ) else: return None + invalid_user_credentials_exc = HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers=unauthorized_headers, + ) try: data = b64decode(param).decode("ascii") except (ValueError, UnicodeDecodeError, binascii.Error): - raise invalid_user_credentials_exc + raise invalid_user_credentials_exc # noqa: B904 username, separator, password = data.partition(":") if not separator: raise invalid_user_credentials_exc @@ -98,13 +218,81 @@ class HTTPBasic(HTTPBase): class HTTPBearer(HTTPBase): + """ + HTTP Bearer token authentication. + + ## Usage + + Create an instance object and use that object as the dependency in `Depends()`. + + The dependency result will be an `HTTPAuthorizationCredentials` object containing + the `scheme` and the `credentials`. + + ## Example + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer + + app = FastAPI() + + security = HTTPBearer() + + + @app.get("/users/me") + def read_current_user( + credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)] + ): + return {"scheme": credentials.scheme, "credentials": credentials.credentials} + ``` + """ + def __init__( self, *, - bearerFormat: Optional[str] = None, - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True, + bearerFormat: Annotated[Optional[str], Doc("Bearer token format.")] = None, + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if the HTTP Bearer token not provided (in an + `Authorization` header), `HTTPBearer` will automatically cancel the + request and send the client an error. + + If `auto_error` is set to `False`, when the HTTP Bearer token + is not available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, in an HTTP + Bearer token or in a cookie). + """ + ), + ] = True, ): self.model = HTTPBearerModel(bearerFormat=bearerFormat, description=description) self.scheme_name = scheme_name or self.__class__.__name__ @@ -134,12 +322,79 @@ class HTTPBearer(HTTPBase): class HTTPDigest(HTTPBase): + """ + HTTP Digest authentication. + + ## Usage + + Create an instance object and use that object as the dependency in `Depends()`. + + The dependency result will be an `HTTPAuthorizationCredentials` object containing + the `scheme` and the `credentials`. + + ## Example + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + from fastapi.security import HTTPAuthorizationCredentials, HTTPDigest + + app = FastAPI() + + security = HTTPDigest() + + + @app.get("/users/me") + def read_current_user( + credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)] + ): + return {"scheme": credentials.scheme, "credentials": credentials.credentials} + ``` + """ + def __init__( self, *, - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True, + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if the HTTP Digest not provided, `HTTPDigest` will + automatically cancel the request and send the client an error. + + If `auto_error` is set to `False`, when the HTTP Digest is not + available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, in HTTP + Digest or in a cookie). + """ + ), + ] = True, ): self.model = HTTPBaseModel(scheme="digest", description=description) self.scheme_name = scheme_name or self.__class__.__name__ diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index eb6b4277cf..be3e18cd80 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union, cast from fastapi.exceptions import HTTPException from fastapi.openapi.models import OAuth2 as OAuth2Model @@ -9,48 +9,137 @@ from fastapi.security.utils import get_authorization_scheme_param from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +# TODO: import from typing when deprecating Python 3.9 +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] + class OAuth2PasswordRequestForm: """ - This is a dependency class, use it like: + This is a dependency class to collect the `username` and `password` as form data + for an OAuth2 password flow. - @app.post("/login") - def login(form_data: OAuth2PasswordRequestForm = Depends()): - data = form_data.parse() - print(data.username) - print(data.password) - for scope in data.scopes: - print(scope) - if data.client_id: - print(data.client_id) - if data.client_secret: - print(data.client_secret) - return data + The OAuth2 specification dictates that for a password flow the data should be + collected using form data (instead of JSON) and that it should have the specific + fields `username` and `password`. + + All the initialization parameters are extracted from the request. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). + + ## Example + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + from fastapi.security import OAuth2PasswordRequestForm + + app = FastAPI() - It creates the following Form request parameters in your endpoint: + @app.post("/login") + def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + data = {} + data["scopes"] = [] + for scope in form_data.scopes: + data["scopes"].append(scope) + if form_data.client_id: + data["client_id"] = form_data.client_id + if form_data.client_secret: + data["client_secret"] = form_data.client_secret + return data + ``` - grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password". - Nevertheless, this dependency class is permissive and allows not passing it. If you want to enforce it, - use instead the OAuth2PasswordRequestFormStrict dependency. - username: username string. The OAuth2 spec requires the exact field name "username". - password: password string. The OAuth2 spec requires the exact field name "password". - scope: Optional string. Several scopes (each one a string) separated by spaces. E.g. - "items:read items:write users:read profile openid" - client_id: optional string. OAuth2 recommends sending the client_id and client_secret (if any) - using HTTP Basic auth, as: client_id:client_secret - client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any) - using HTTP Basic auth, as: client_id:client_secret + Note that for OAuth2 the scope `items:read` is a single scope in an opaque string. + You could have custom internal logic to separate it by colon caracters (`:`) or + similar, and get the two parts `items` and `read`. Many applications do that to + group and organize permisions, you could do it as well in your application, just + know that that it is application specific, it's not part of the specification. """ def __init__( self, - grant_type: str = Form(default=None, regex="password"), - username: str = Form(), - password: str = Form(), - scope: str = Form(default=""), - client_id: Optional[str] = Form(default=None), - client_secret: Optional[str] = Form(default=None), + *, + grant_type: Annotated[ + Union[str, None], + Form(pattern="password"), + Doc( + """ + The OAuth2 spec says it is required and MUST be the fixed string + "password". Nevertheless, this dependency class is permissive and + allows not passing it. If you want to enforce it, use instead the + `OAuth2PasswordRequestFormStrict` dependency. + """ + ), + ] = None, + username: Annotated[ + str, + Form(), + Doc( + """ + `username` string. The OAuth2 spec requires the exact field name + `username`. + """ + ), + ], + password: Annotated[ + str, + Form(), + Doc( + """ + `password` string. The OAuth2 spec requires the exact field name + `password". + """ + ), + ], + scope: Annotated[ + str, + Form(), + Doc( + """ + A single string with actually several scopes separated by spaces. Each + scope is also a string. + + For example, a single string with: + + ```python + "items:read items:write users:read profile openid" + ```` + + would represent the scopes: + + * `items:read` + * `items:write` + * `users:read` + * `profile` + * `openid` + """ + ), + ] = "", + client_id: Annotated[ + Union[str, None], + Form(), + Doc( + """ + If there's a `client_id`, it can be sent as part of the form fields. + But the OAuth2 specification recommends sending the `client_id` and + `client_secret` (if any) using HTTP Basic auth. + """ + ), + ] = None, + client_secret: Annotated[ + Union[str, None], + Form(), + Doc( + """ + If there's a `client_password` (and a `client_id`), they can be sent + as part of the form fields. But the OAuth2 specification recommends + sending the `client_id` and `client_secret` (if any) using HTTP Basic + auth. + """ + ), + ] = None, ): self.grant_type = grant_type self.username = username @@ -62,23 +151,54 @@ class OAuth2PasswordRequestForm: class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): """ - This is a dependency class, use it like: + This is a dependency class to collect the `username` and `password` as form data + for an OAuth2 password flow. - @app.post("/login") - def login(form_data: OAuth2PasswordRequestFormStrict = Depends()): - data = form_data.parse() - print(data.username) - print(data.password) - for scope in data.scopes: - print(scope) - if data.client_id: - print(data.client_id) - if data.client_secret: - print(data.client_secret) - return data + The OAuth2 specification dictates that for a password flow the data should be + collected using form data (instead of JSON) and that it should have the specific + fields `username` and `password`. + + All the initialization parameters are extracted from the request. + + The only difference between `OAuth2PasswordRequestFormStrict` and + `OAuth2PasswordRequestForm` is that `OAuth2PasswordRequestFormStrict` requires the + client to send the form field `grant_type` with the value `"password"`, which + is required in the OAuth2 specification (it seems that for no particular reason), + while for `OAuth2PasswordRequestForm` `grant_type` is optional. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). + + ## Example + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + from fastapi.security import OAuth2PasswordRequestForm + + app = FastAPI() - It creates the following Form request parameters in your endpoint: + @app.post("/login") + def login(form_data: Annotated[OAuth2PasswordRequestFormStrict, Depends()]): + data = {} + data["scopes"] = [] + for scope in form_data.scopes: + data["scopes"].append(scope) + if form_data.client_id: + data["client_id"] = form_data.client_id + if form_data.client_secret: + data["client_secret"] = form_data.client_secret + return data + ``` + + Note that for OAuth2 the scope `items:read` is a single scope in an opaque string. + You could have custom internal logic to separate it by colon caracters (`:`) or + similar, and get the two parts `items` and `read`. Many applications do that to + group and organize permisions, you could do it as well in your application, just + know that that it is application specific, it's not part of the specification. + grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password". This dependency is strict about it. If you want to be permissive, use instead the @@ -95,12 +215,85 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): def __init__( self, - grant_type: str = Form(regex="password"), - username: str = Form(), - password: str = Form(), - scope: str = Form(default=""), - client_id: Optional[str] = Form(default=None), - client_secret: Optional[str] = Form(default=None), + grant_type: Annotated[ + str, + Form(pattern="password"), + Doc( + """ + The OAuth2 spec says it is required and MUST be the fixed string + "password". This dependency is strict about it. If you want to be + permissive, use instead the `OAuth2PasswordRequestForm` dependency + class. + """ + ), + ], + username: Annotated[ + str, + Form(), + Doc( + """ + `username` string. The OAuth2 spec requires the exact field name + `username`. + """ + ), + ], + password: Annotated[ + str, + Form(), + Doc( + """ + `password` string. The OAuth2 spec requires the exact field name + `password". + """ + ), + ], + scope: Annotated[ + str, + Form(), + Doc( + """ + A single string with actually several scopes separated by spaces. Each + scope is also a string. + + For example, a single string with: + + ```python + "items:read items:write users:read profile openid" + ```` + + would represent the scopes: + + * `items:read` + * `items:write` + * `users:read` + * `profile` + * `openid` + """ + ), + ] = "", + client_id: Annotated[ + Union[str, None], + Form(), + Doc( + """ + If there's a `client_id`, it can be sent as part of the form fields. + But the OAuth2 specification recommends sending the `client_id` and + `client_secret` (if any) using HTTP Basic auth. + """ + ), + ] = None, + client_secret: Annotated[ + Union[str, None], + Form(), + Doc( + """ + If there's a `client_password` (and a `client_id`), they can be sent + as part of the form fields. But the OAuth2 specification recommends + sending the `client_id` and `client_secret` (if any) using HTTP Basic + auth. + """ + ), + ] = None, ): super().__init__( grant_type=grant_type, @@ -113,15 +306,73 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): class OAuth2(SecurityBase): + """ + This is the base class for OAuth2 authentication, an instance of it would be used + as a dependency. All other OAuth2 classes inherit from it and customize it for + each OAuth2 flow. + + You normally would not create a new class inheriting from it but use one of the + existing subclasses, and maybe compose them if you want to support multiple flows. + + Read more about it in the + [FastAPI docs for Security](https://fastapi.tiangolo.com/tutorial/security/). + """ + def __init__( self, *, - flows: Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]] = OAuthFlowsModel(), - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True + flows: Annotated[ + Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]], + Doc( + """ + The dictionary of OAuth2 flows. + """ + ), + ] = OAuthFlowsModel(), + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if no HTTP Authorization header is provided, required for + OAuth2 authentication, it will automatically cancel the request and + send the client an error. + + If `auto_error` is set to `False`, when the HTTP Authorization header + is not available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, with OAuth2 + or in a cookie). + """ + ), + ] = True, ): - self.model = OAuth2Model(flows=flows, description=description) + self.model = OAuth2Model( + flows=cast(OAuthFlowsModel, flows), description=description + ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error @@ -138,17 +389,80 @@ class OAuth2(SecurityBase): class OAuth2PasswordBearer(OAuth2): + """ + OAuth2 flow for authentication using a bearer token obtained with a password. + An instance of it would be used as a dependency. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). + """ + def __init__( self, - tokenUrl: str, - scheme_name: Optional[str] = None, - scopes: Optional[Dict[str, str]] = None, - description: Optional[str] = None, - auto_error: bool = True, + tokenUrl: Annotated[ + str, + Doc( + """ + The URL to obtain the OAuth2 token. This would be the *path operation* + that has `OAuth2PasswordRequestForm` as a dependency. + """ + ), + ], + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + scopes: Annotated[ + Optional[Dict[str, str]], + Doc( + """ + The OAuth2 scopes that would be required by the *path operations* that + use this dependency. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if no HTTP Auhtorization header is provided, required for + OAuth2 authentication, it will automatically cancel the request and + send the client an error. + + If `auto_error` is set to `False`, when the HTTP Authorization header + is not available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, with OAuth2 + or in a cookie). + """ + ), + ] = True, ): if not scopes: scopes = {} - flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes}) + flows = OAuthFlowsModel( + password=cast(Any, {"tokenUrl": tokenUrl, "scopes": scopes}) + ) super().__init__( flows=flows, scheme_name=scheme_name, @@ -172,25 +486,92 @@ class OAuth2PasswordBearer(OAuth2): class OAuth2AuthorizationCodeBearer(OAuth2): + """ + OAuth2 flow for authentication using a bearer token obtained with an OAuth2 code + flow. An instance of it would be used as a dependency. + """ + def __init__( self, authorizationUrl: str, - tokenUrl: str, - refreshUrl: Optional[str] = None, - scheme_name: Optional[str] = None, - scopes: Optional[Dict[str, str]] = None, - description: Optional[str] = None, - auto_error: bool = True, + tokenUrl: Annotated[ + str, + Doc( + """ + The URL to obtain the OAuth2 token. + """ + ), + ], + refreshUrl: Annotated[ + Optional[str], + Doc( + """ + The URL to refresh the token and obtain a new one. + """ + ), + ] = None, + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + scopes: Annotated[ + Optional[Dict[str, str]], + Doc( + """ + The OAuth2 scopes that would be required by the *path operations* that + use this dependency. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if no HTTP Auhtorization header is provided, required for + OAuth2 authentication, it will automatically cancel the request and + send the client an error. + + If `auto_error` is set to `False`, when the HTTP Authorization header + is not available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, with OAuth2 + or in a cookie). + """ + ), + ] = True, ): if not scopes: scopes = {} flows = OAuthFlowsModel( - authorizationCode={ - "authorizationUrl": authorizationUrl, - "tokenUrl": tokenUrl, - "refreshUrl": refreshUrl, - "scopes": scopes, - } + authorizationCode=cast( + Any, + { + "authorizationUrl": authorizationUrl, + "tokenUrl": tokenUrl, + "refreshUrl": refreshUrl, + "scopes": scopes, + }, + ) ) super().__init__( flows=flows, @@ -215,6 +596,43 @@ class OAuth2AuthorizationCodeBearer(OAuth2): class SecurityScopes: - def __init__(self, scopes: Optional[List[str]] = None): - self.scopes = scopes or [] - self.scope_str = " ".join(self.scopes) + """ + This is a special class that you can define in a parameter in a dependency to + obtain the OAuth2 scopes required by all the dependencies in the same chain. + + This way, multiple dependencies can have different scopes, even when used in the + same *path operation*. And with this, you can access all the scopes required in + all those dependencies in a single place. + + Read more about it in the + [FastAPI docs for OAuth2 scopes](https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/). + """ + + def __init__( + self, + scopes: Annotated[ + Optional[List[str]], + Doc( + """ + This will be filled by FastAPI. + """ + ), + ] = None, + ): + self.scopes: Annotated[ + List[str], + Doc( + """ + The list of all the scopes required by dependencies. + """ + ), + ] = scopes or [] + self.scope_str: Annotated[ + str, + Doc( + """ + All the scopes required by all the dependencies in a single string + separated by spaces, as defined in the OAuth2 specification. + """ + ), + ] = " ".join(self.scopes) diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index 393614f7cb..c612b475de 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -5,16 +5,66 @@ from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.status import HTTP_403_FORBIDDEN +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] class OpenIdConnect(SecurityBase): + """ + OpenID Connect authentication class. An instance of it would be used as a + dependency. + """ + def __init__( self, *, - openIdConnectUrl: str, - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True + openIdConnectUrl: Annotated[ + str, + Doc( + """ + The OpenID Connect URL. + """ + ), + ], + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if no HTTP Auhtorization header is provided, required for + OpenID Connect authentication, it will automatically cancel the request + and send the client an error. + + If `auto_error` is set to `False`, when the HTTP Authorization header + is not available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, with OpenID + Connect or in a cookie). + """ + ), + ] = True, ): self.model = OpenIdConnectModel( openIdConnectUrl=openIdConnectUrl, description=description diff --git a/fastapi/types.py b/fastapi/types.py index e0bca46320..3205654c73 100644 --- a/fastapi/types.py +++ b/fastapi/types.py @@ -1,3 +1,10 @@ -from typing import Any, Callable, TypeVar +import types +from enum import Enum +from typing import Any, Callable, Dict, Set, Type, TypeVar, Union + +from pydantic import BaseModel DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]) +UnionType = getattr(types, "UnionType", Union) +ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str] +IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]] diff --git a/fastapi/utils.py b/fastapi/utils.py index b15f6a2cfb..53b2fa0c36 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -1,22 +1,43 @@ -import functools import re import warnings from dataclasses import is_dataclass -from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Type, Union, cast +from typing import ( + TYPE_CHECKING, + Any, + Dict, + MutableMapping, + Optional, + Set, + Type, + Union, + cast, +) +from weakref import WeakKeyDictionary import fastapi +from fastapi._compat import ( + PYDANTIC_V2, + BaseConfig, + ModelField, + PydanticSchemaGenerationError, + Undefined, + UndefinedType, + Validator, + lenient_issubclass, +) from fastapi.datastructures import DefaultPlaceholder, DefaultType -from fastapi.openapi.constants import REF_PREFIX -from pydantic import BaseConfig, BaseModel, create_model -from pydantic.class_validators import Validator -from pydantic.fields import FieldInfo, ModelField, UndefinedType -from pydantic.schema import model_process_schema -from pydantic.utils import lenient_issubclass +from pydantic import BaseModel, create_model +from pydantic.fields import FieldInfo +from typing_extensions import Literal if TYPE_CHECKING: # pragma: nocover from .routing import APIRoute +# Cache for `create_cloned_field` +_CLONED_TYPES_CACHE: MutableMapping[ + Type[BaseModel], Type[BaseModel] +] = WeakKeyDictionary() + def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool: if status_code is None: @@ -32,25 +53,7 @@ def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool: }: return True current_status_code = int(status_code) - return not (current_status_code < 200 or current_status_code in {204, 304}) - - -def get_model_definitions( - *, - flat_models: Set[Union[Type[BaseModel], Type[Enum]]], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], -) -> Dict[str, Any]: - definitions: Dict[str, Dict[str, Any]] = {} - for model in flat_models: - m_schema, m_definitions, m_nested_models = model_process_schema( - model, model_name_map=model_name_map, ref_prefix=REF_PREFIX - ) - definitions.update(m_definitions) - model_name = model_name_map[model] - if "description" in m_schema: - m_schema["description"] = m_schema["description"].split("\f")[0] - definitions[model_name] = m_schema - return definitions + return not (current_status_code < 200 or current_status_code in {204, 205, 304}) def get_path_param_names(path: str) -> Set[str]: @@ -61,45 +64,63 @@ def create_response_field( name: str, type_: Type[Any], class_validators: Optional[Dict[str, Validator]] = None, - default: Optional[Any] = None, - required: Union[bool, UndefinedType] = True, + default: Optional[Any] = Undefined, + required: Union[bool, UndefinedType] = Undefined, model_config: Type[BaseConfig] = BaseConfig, field_info: Optional[FieldInfo] = None, alias: Optional[str] = None, + mode: Literal["validation", "serialization"] = "validation", ) -> ModelField: """ Create a new response field. Raises if type_ is invalid. """ class_validators = class_validators or {} - field_info = field_info or FieldInfo() - - response_field = functools.partial( - ModelField, - name=name, - type_=type_, - class_validators=class_validators, - default=default, - required=required, - model_config=model_config, - alias=alias, - ) - + if PYDANTIC_V2: + field_info = field_info or FieldInfo( + annotation=type_, default=default, alias=alias + ) + else: + field_info = field_info or FieldInfo() + kwargs = {"name": name, "field_info": field_info} + if PYDANTIC_V2: + kwargs.update({"mode": mode}) + else: + kwargs.update( + { + "type_": type_, + "class_validators": class_validators, + "default": default, + "required": required, + "model_config": model_config, + "alias": alias, + } + ) try: - return response_field(field_info=field_info) - except RuntimeError: + return ModelField(**kwargs) # type: ignore[arg-type] + except (RuntimeError, PydanticSchemaGenerationError): raise fastapi.exceptions.FastAPIError( - f"Invalid args for response field! Hint: check that {type_} is a valid pydantic field type" + "Invalid args for response field! Hint: " + f"check that {type_} is a valid Pydantic field type. " + "If you are using a return type annotation that is not a valid Pydantic " + "field (e.g. Union[Response, dict, None]) you can disable generating the " + "response model from the type annotation with the path operation decorator " + "parameter response_model=None. Read more: " + "https://fastapi.tiangolo.com/tutorial/response-model/" ) from None def create_cloned_field( field: ModelField, *, - cloned_types: Optional[Dict[Type[BaseModel], Type[BaseModel]]] = None, + cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None, ) -> ModelField: - # _cloned_types has already cloned types, to support recursive models + if PYDANTIC_V2: + return field + # cloned_types caches already cloned types to support recursive models and improve + # performance by avoiding unnecessary cloning if cloned_types is None: - cloned_types = {} + cloned_types = _CLONED_TYPES_CACHE + original_type = field.type_ if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"): original_type = original_type.__pydantic_model__ @@ -115,30 +136,31 @@ def create_cloned_field( f, cloned_types=cloned_types ) new_field = create_response_field(name=field.name, type_=use_type) - new_field.has_alias = field.has_alias - new_field.alias = field.alias - new_field.class_validators = field.class_validators - new_field.default = field.default - new_field.required = field.required - new_field.model_config = field.model_config + new_field.has_alias = field.has_alias # type: ignore[attr-defined] + new_field.alias = field.alias # type: ignore[misc] + new_field.class_validators = field.class_validators # type: ignore[attr-defined] + new_field.default = field.default # type: ignore[misc] + new_field.required = field.required # type: ignore[misc] + new_field.model_config = field.model_config # type: ignore[attr-defined] new_field.field_info = field.field_info - new_field.allow_none = field.allow_none - new_field.validate_always = field.validate_always - if field.sub_fields: - new_field.sub_fields = [ + new_field.allow_none = field.allow_none # type: ignore[attr-defined] + new_field.validate_always = field.validate_always # type: ignore[attr-defined] + if field.sub_fields: # type: ignore[attr-defined] + new_field.sub_fields = [ # type: ignore[attr-defined] create_cloned_field(sub_field, cloned_types=cloned_types) - for sub_field in field.sub_fields + for sub_field in field.sub_fields # type: ignore[attr-defined] ] - if field.key_field: - new_field.key_field = create_cloned_field( - field.key_field, cloned_types=cloned_types + if field.key_field: # type: ignore[attr-defined] + new_field.key_field = create_cloned_field( # type: ignore[attr-defined] + field.key_field, # type: ignore[attr-defined] + cloned_types=cloned_types, ) - new_field.validators = field.validators - new_field.pre_validators = field.pre_validators - new_field.post_validators = field.post_validators - new_field.parse_json = field.parse_json - new_field.shape = field.shape - new_field.populate_validators() + new_field.validators = field.validators # type: ignore[attr-defined] + new_field.pre_validators = field.pre_validators # type: ignore[attr-defined] + new_field.post_validators = field.post_validators # type: ignore[attr-defined] + new_field.parse_json = field.parse_json # type: ignore[attr-defined] + new_field.shape = field.shape # type: ignore[attr-defined] + new_field.populate_validators() # type: ignore[attr-defined] return new_field @@ -151,17 +173,17 @@ def generate_operation_id_for_path( DeprecationWarning, stacklevel=2, ) - operation_id = name + path + operation_id = f"{name}{path}" operation_id = re.sub(r"\W", "_", operation_id) - operation_id = operation_id + "_" + method.lower() + operation_id = f"{operation_id}_{method.lower()}" return operation_id def generate_unique_id(route: "APIRoute") -> str: - operation_id = route.name + route.path_format + operation_id = f"{route.name}{route.path_format}" operation_id = re.sub(r"\W", "_", operation_id) assert route.methods - operation_id = operation_id + "_" + list(route.methods)[0].lower() + operation_id = f"{operation_id}_{list(route.methods)[0].lower()}" return operation_id @@ -199,3 +221,9 @@ def get_value_or_default( if not isinstance(item, DefaultPlaceholder): return item return first_item + + +def match_pydantic_error_url(error_type: str) -> Any: + from dirty_equals import IsStr + + return IsStr(regex=rf"^https://errors\.pydantic\.dev/.*/v/{error_type}") diff --git a/pyproject.toml b/pyproject.toml index b29549f5c8..3e43f35e17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [build-system] -requires = ["hatchling"] +requires = ["hatchling >= 1.13.0"] build-backend = "hatchling.build" [project] name = "fastapi" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" license = "MIT" authors = [ { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, @@ -27,70 +27,32 @@ classifiers = [ "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", + "Framework :: Pydantic", + "Framework :: Pydantic :: 1", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette==0.22.0", - "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", + "starlette>=0.35.0,<0.36.0", + "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", + "typing-extensions>=4.8.0", ] dynamic = ["version"] [project.urls] Homepage = "https://github.com/tiangolo/fastapi" Documentation = "https://fastapi.tiangolo.com/" +Repository = "https://github.com/tiangolo/fastapi" [project.optional-dependencies] -test = [ - "pytest >=7.1.3,<8.0.0", - "coverage[toml] >= 6.5.0,< 8.0", - "mypy ==0.982", - "ruff ==0.0.138", - "black == 22.10.0", - "isort >=5.0.6,<6.0.0", - "httpx >=0.23.0,<0.24.0", - "email_validator >=1.1.1,<2.0.0", - # TODO: once removing databases from tutorial, upgrade SQLAlchemy - # probably when including SQLModel - "sqlalchemy >=1.3.18,<1.4.43", - "peewee >=3.13.3,<4.0.0", - "databases[sqlite] >=0.3.2,<0.7.0", - "orjson >=3.2.1,<4.0.0", - "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0", - "python-multipart >=0.0.5,<0.0.6", - "flask >=1.1.2,<3.0.0", - "anyio[trio] >=3.2.1,<4.0.0", - "python-jose[cryptography] >=3.3.0,<4.0.0", - "pyyaml >=5.3.1,<7.0.0", - "passlib[bcrypt] >=1.7.2,<2.0.0", - - # types - "types-ujson ==5.6.0.0", - "types-orjson ==3.6.2", -] -doc = [ - "mkdocs >=1.1.2,<2.0.0", - "mkdocs-material >=8.1.4,<9.0.0", - "mdx-include >=1.4.1,<2.0.0", - "mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0", - # TODO: upgrade and enable typer-cli once it supports Click 8.x.x - # "typer-cli >=0.0.12,<0.0.13", - "typer[all] >=0.6.1,<0.8.0", - "pyyaml >=5.3.1,<7.0.0", -] -dev = [ - "ruff ==0.0.138", - "uvicorn[standard] >=0.12.0,<0.21.0", - "pre-commit >=2.17.0,<3.0.0", -] all = [ "httpx >=0.23.0", "jinja2 >=2.11.2", @@ -99,17 +61,15 @@ all = [ "pyyaml >=5.3.1", "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0", "orjson >=3.2.1", - "email_validator >=1.1.1", + "email_validator >=2.0.0", "uvicorn[standard] >=0.12.0", + "pydantic-settings >=2.0.0", + "pydantic-extra-types >=2.0.0", ] [tool.hatch.version] path = "fastapi/__init__.py" -[tool.isort] -profile = "black" -known_third_party = ["fastapi", "pydantic", "starlette"] - [tool.mypy] strict = true @@ -123,10 +83,17 @@ module = "fastapi.tests.*" ignore_missing_imports = true check_untyped_defs = true +[[tool.mypy.overrides]] +module = "docs_src.*" +disallow_incomplete_defs = false +disallow_untyped_defs = false +disallow_untyped_calls = false + [tool.pytest.ini_options] addopts = [ "--strict-config", "--strict-markers", + "--ignore=docs_src", ] xfail_strict = true junit_family = "xunit2" @@ -145,6 +112,18 @@ filterwarnings = [ "ignore::trio.TrioDeprecationWarning", # TODO remove pytest-cov 'ignore::pytest.PytestDeprecationWarning:pytest_cov', + # TODO: remove after upgrading SQLAlchemy to a version that includes the following changes + # https://github.com/sqlalchemy/sqlalchemy/commit/59521abcc0676e936b31a523bd968fc157fef0c2 + 'ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning:sqlalchemy', + # TODO: remove after upgrading python-jose to a version that explicitly supports Python 3.12 + # also, if it won't receive an update, consider replacing python-jose with some alternative + # related issues: + # - https://github.com/mpdavis/python-jose/issues/332 + # - https://github.com/mpdavis/python-jose/issues/334 + 'ignore:datetime\.datetime\.utcnow\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning:jose', + # TODO: remove after upgrading Starlette to a version including https://github.com/encode/starlette/pull/2406 + # Probably Starlette 0.36.0 + "ignore: The 'method' parameter is not used, and it will be removed.:DeprecationWarning:starlette", ] [tool.coverage.run] @@ -155,20 +134,25 @@ source = [ "fastapi" ] context = '${CONTEXT}' +omit = [ + "docs_src/response_model/tutorial003_04.py", + "docs_src/response_model/tutorial003_04_py310.py", +] [tool.ruff] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes - # "I", # isort - "C", # flake8-comprehensions + "I", # isort "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade ] ignore = [ "E501", # line too long, handled by black "B008", # do not perform function calls in argument defaults - "C901", # too complex + "W191", # indentation contains tabs ] [tool.ruff.per-file-ignores] @@ -180,7 +164,33 @@ ignore = [ "docs_src/custom_response/tutorial007.py" = ["B007"] "docs_src/dataclasses/tutorial003.py" = ["I001"] "docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"] +"docs_src/path_operation_advanced_configuration/tutorial007_pv1.py" = ["B904"] "docs_src/custom_request_and_route/tutorial002.py" = ["B904"] +"docs_src/dependencies/tutorial008_an.py" = ["F821"] +"docs_src/dependencies/tutorial008_an_py39.py" = ["F821"] +"docs_src/query_params_str_validations/tutorial012_an.py" = ["B006"] +"docs_src/query_params_str_validations/tutorial012_an_py39.py" = ["B006"] +"docs_src/query_params_str_validations/tutorial013_an.py" = ["B006"] +"docs_src/query_params_str_validations/tutorial013_an_py39.py" = ["B006"] +"docs_src/security/tutorial004.py" = ["B904"] +"docs_src/security/tutorial004_an.py" = ["B904"] +"docs_src/security/tutorial004_an_py310.py" = ["B904"] +"docs_src/security/tutorial004_an_py39.py" = ["B904"] +"docs_src/security/tutorial004_py310.py" = ["B904"] +"docs_src/security/tutorial005.py" = ["B904"] +"docs_src/security/tutorial005_an.py" = ["B904"] +"docs_src/security/tutorial005_an_py310.py" = ["B904"] +"docs_src/security/tutorial005_an_py39.py" = ["B904"] +"docs_src/security/tutorial005_py310.py" = ["B904"] +"docs_src/security/tutorial005_py39.py" = ["B904"] +"docs_src/dependencies/tutorial008b.py" = ["B904"] +"docs_src/dependencies/tutorial008b_an.py" = ["B904"] +"docs_src/dependencies/tutorial008b_an_py39.py" = ["B904"] + [tool.ruff.isort] known-third-party = ["fastapi", "pydantic", "starlette"] + +[tool.ruff.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true diff --git a/requirements-docs-tests.txt b/requirements-docs-tests.txt new file mode 100644 index 0000000000..b82df49338 --- /dev/null +++ b/requirements-docs-tests.txt @@ -0,0 +1,2 @@ +# For mkdocstrings and tests +httpx >=0.23.0,<0.25.0 diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000000..28408a9f1b --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,19 @@ +-e . +-r requirements-docs-tests.txt +mkdocs-material==9.4.7 +mdx-include >=1.4.1,<2.0.0 +mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 +mkdocs-redirects>=1.2.1,<1.3.0 +typer-cli >=0.0.13,<0.0.14 +typer[all] >=0.6.1,<0.8.0 +pyyaml >=5.3.1,<7.0.0 +# For Material for MkDocs, Chinese search +jieba==0.42.1 +# For image processing by Material for MkDocs +pillow==10.1.0 +# For image processing by Material for MkDocs +cairosvg==2.7.0 +mkdocstrings[python]==0.23.0 +griffe-typingdoc==0.2.2 +# For griffe, it formats with black +black==23.3.0 diff --git a/requirements-tests.txt b/requirements-tests.txt new file mode 100644 index 0000000000..e1a976c138 --- /dev/null +++ b/requirements-tests.txt @@ -0,0 +1,25 @@ +-e . +-r requirements-docs-tests.txt +pydantic-settings >=2.0.0 +pytest >=7.1.3,<8.0.0 +coverage[toml] >= 6.5.0,< 8.0 +mypy ==1.4.1 +ruff ==0.1.2 +email_validator >=1.1.1,<3.0.0 +dirty-equals ==0.6.0 +# TODO: once removing databases from tutorial, upgrade SQLAlchemy +# probably when including SQLModel +sqlalchemy >=1.3.18,<1.4.43 +databases[sqlite] >=0.3.2,<0.7.0 +orjson >=3.2.1,<4.0.0 +ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0 +python-multipart >=0.0.5,<0.0.7 +flask >=1.1.2,<3.0.0 +anyio[trio] >=3.2.1,<4.0.0 +python-jose[cryptography] >=3.3.0,<4.0.0 +pyyaml >=5.3.1,<7.0.0 +passlib[bcrypt] >=1.7.2,<2.0.0 + +# types +types-ujson ==5.7.0.1 +types-orjson ==3.6.2 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..ef25ec483f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +-e .[all] +-r requirements-tests.txt +-r requirements-docs.txt +uvicorn[standard] >=0.12.0,<0.23.0 +pre-commit >=2.17.0,<4.0.0 +# For generating screenshots +playwright diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh index 383ad3f446..ebf864afa3 100755 --- a/scripts/build-docs.sh +++ b/scripts/build-docs.sh @@ -3,4 +3,6 @@ set -e set -x +# Check README.md is up to date +python ./scripts/docs.py verify-readme python ./scripts/docs.py build-all diff --git a/scripts/docs.py b/scripts/docs.py index e0953b8edb..37a7a34779 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -1,11 +1,15 @@ +import json +import logging import os import re import shutil import subprocess +from functools import lru_cache from http.server import HTTPServer, SimpleHTTPRequestHandler +from importlib import metadata from multiprocessing import Pool from pathlib import Path -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Union import mkdocs.commands.build import mkdocs.commands.serve @@ -15,6 +19,8 @@ import typer import yaml from jinja2 import Template +logging.basicConfig(level=logging.INFO) + app = typer.Typer() mkdocs_name = "mkdocs.yml" @@ -26,22 +32,27 @@ missing_translation_snippet = """ docs_path = Path("docs") en_docs_path = Path("docs/en") en_config_path: Path = en_docs_path / mkdocs_name +site_path = Path("site").absolute() +build_site_path = Path("site_build").absolute() -def get_en_config() -> dict: +@lru_cache +def is_mkdocs_insiders() -> bool: + version = metadata.version("mkdocs-material") + return "insiders" in version + + +def get_en_config() -> Dict[str, Any]: return mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) -def get_lang_paths(): +def get_lang_paths() -> List[Path]: return sorted(docs_path.iterdir()) -def lang_callback(lang: Optional[str]): +def lang_callback(lang: Optional[str]) -> Union[str, None]: if lang is None: - return - if not lang.isalpha() or len(lang) != 2: - typer.echo("Use a 2 letter language code, like: es") - raise typer.Abort() + return None lang = lang.lower() return lang @@ -53,33 +64,12 @@ def complete_existing_lang(incomplete: str): yield lang_path.name -def get_base_lang_config(lang: str): - en_config = get_en_config() - fastapi_url_base = "https://fastapi.tiangolo.com/" - new_config = en_config.copy() - new_config["site_url"] = en_config["site_url"] + f"{lang}/" - new_config["theme"]["logo"] = fastapi_url_base + en_config["theme"]["logo"] - new_config["theme"]["favicon"] = fastapi_url_base + en_config["theme"]["favicon"] - new_config["theme"]["language"] = lang - new_config["nav"] = en_config["nav"][:2] - extra_css = [] - css: str - for css in en_config["extra_css"]: - if css.startswith("http"): - extra_css.append(css) - else: - extra_css.append(fastapi_url_base + css) - new_config["extra_css"] = extra_css - - extra_js = [] - js: str - for js in en_config["extra_javascript"]: - if js.startswith("http"): - extra_js.append(js) - else: - extra_js.append(fastapi_url_base + js) - new_config["extra_javascript"] = extra_js - return new_config +@app.callback() +def callback() -> None: + if is_mkdocs_insiders(): + os.environ["INSIDERS_FILE"] = "../en/mkdocs.insiders.yml" + # For MacOS with insiders and Cairo + os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib" @app.command() @@ -94,12 +84,8 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): typer.echo(f"The language was already created: {lang}") raise typer.Abort() new_path.mkdir() - new_config = get_base_lang_config(lang) new_config_path: Path = Path(new_path) / mkdocs_name - new_config_path.write_text( - yaml.dump(new_config, sort_keys=False, width=200, allow_unicode=True), - encoding="utf-8", - ) + new_config_path.write_text("INHERIT: ../en/mkdocs.yml\n", encoding="utf-8") new_config_docs_path: Path = new_path / "docs" new_config_docs_path.mkdir() en_index_path: Path = en_docs_path / "docs" / "index.md" @@ -107,101 +93,44 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): en_index_content = en_index_path.read_text(encoding="utf-8") new_index_content = f"{missing_translation_snippet}\n\n{en_index_content}" new_index_path.write_text(new_index_content, encoding="utf-8") - new_overrides_gitignore_path = new_path / "overrides" / ".gitignore" - new_overrides_gitignore_path.parent.mkdir(parents=True, exist_ok=True) - new_overrides_gitignore_path.write_text("") typer.secho(f"Successfully initialized: {new_path}", color=typer.colors.GREEN) - update_languages(lang=None) + update_languages() @app.command() def build_lang( lang: str = typer.Argument( ..., callback=lang_callback, autocompletion=complete_existing_lang - ) -): + ), +) -> None: """ - Build the docs for a language, filling missing pages with translation notifications. + Build the docs for a language. """ + insiders_env_file = os.environ.get("INSIDERS_FILE") + print(f"Insiders file {insiders_env_file}") + if is_mkdocs_insiders(): + print("Using insiders") lang_path: Path = Path("docs") / lang if not lang_path.is_dir(): typer.echo(f"The language translation doesn't seem to exist yet: {lang}") raise typer.Abort() typer.echo(f"Building docs for: {lang}") - build_dir_path = Path("docs_build") - build_dir_path.mkdir(exist_ok=True) - build_lang_path = build_dir_path / lang - en_lang_path = Path("docs/en") - site_path = Path("site").absolute() + build_site_dist_path = build_site_path / lang if lang == "en": dist_path = site_path + # Don't remove en dist_path as it might already contain other languages. + # When running build_all(), that function already removes site_path. + # All this is only relevant locally, on GitHub Actions all this is done through + # artifacts and multiple workflows, so it doesn't matter if directories are + # removed or not. else: - dist_path: Path = site_path / lang - shutil.rmtree(build_lang_path, ignore_errors=True) - shutil.copytree(lang_path, build_lang_path) - shutil.copytree(en_docs_path / "data", build_lang_path / "data") - overrides_src = en_docs_path / "overrides" - overrides_dest = build_lang_path / "overrides" - for path in overrides_src.iterdir(): - dest_path = overrides_dest / path.name - if not dest_path.exists(): - shutil.copy(path, dest_path) - en_config_path: Path = en_lang_path / mkdocs_name - en_config: dict = mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) - nav = en_config["nav"] - lang_config_path: Path = lang_path / mkdocs_name - lang_config: dict = mkdocs.utils.yaml_load( - lang_config_path.read_text(encoding="utf-8") - ) - lang_nav = lang_config["nav"] - # Exclude first 2 entries FastAPI and Languages, for custom handling - use_nav = nav[2:] - lang_use_nav = lang_nav[2:] - file_to_nav = get_file_to_nav_map(use_nav) - sections = get_sections(use_nav) - lang_file_to_nav = get_file_to_nav_map(lang_use_nav) - use_lang_file_to_nav = get_file_to_nav_map(lang_use_nav) - for file in file_to_nav: - file_path = Path(file) - lang_file_path: Path = build_lang_path / "docs" / file_path - en_file_path: Path = en_lang_path / "docs" / file_path - lang_file_path.parent.mkdir(parents=True, exist_ok=True) - if not lang_file_path.is_file(): - en_text = en_file_path.read_text(encoding="utf-8") - lang_text = get_text_with_translate_missing(en_text) - lang_file_path.write_text(lang_text, encoding="utf-8") - file_key = file_to_nav[file] - use_lang_file_to_nav[file] = file_key - if file_key: - composite_key = () - new_key = () - for key_part in file_key: - composite_key += (key_part,) - key_first_file = sections[composite_key] - if key_first_file in lang_file_to_nav: - new_key = lang_file_to_nav[key_first_file] - else: - new_key += (key_part,) - use_lang_file_to_nav[file] = new_key - key_to_section = {(): []} - for file, orig_file_key in file_to_nav.items(): - if file in use_lang_file_to_nav: - file_key = use_lang_file_to_nav[file] - else: - file_key = orig_file_key - section = get_key_section(key_to_section=key_to_section, key=file_key) - section.append(file) - new_nav = key_to_section[()] - export_lang_nav = [lang_nav[0], nav[1]] + new_nav - lang_config["nav"] = export_lang_nav - build_lang_config_path: Path = build_lang_path / mkdocs_name - build_lang_config_path.write_text( - yaml.dump(lang_config, sort_keys=False, width=200, allow_unicode=True), - encoding="utf-8", - ) + dist_path = site_path / lang + shutil.rmtree(dist_path, ignore_errors=True) current_dir = os.getcwd() - os.chdir(build_lang_path) - subprocess.run(["mkdocs", "build", "--site-dir", dist_path], check=True) + os.chdir(lang_path) + shutil.rmtree(build_site_dist_path, ignore_errors=True) + subprocess.run(["mkdocs", "build", "--site-dir", build_site_dist_path], check=True) + shutil.copytree(build_site_dist_path, dist_path, dirs_exist_ok=True) os.chdir(current_dir) typer.secho(f"Successfully built docs for: {lang}", color=typer.colors.GREEN) @@ -218,27 +147,31 @@ index_sponsors_template = """ """ -def generate_readme_content(): +def generate_readme_content() -> str: en_index = en_docs_path / "docs" / "index.md" content = en_index.read_text("utf-8") + match_pre = re.search(r"</style>\n\n", content) match_start = re.search(r"<!-- sponsors -->", content) match_end = re.search(r"<!-- /sponsors -->", content) sponsors_data_path = en_docs_path / "data" / "sponsors.yml" sponsors = mkdocs.utils.yaml_load(sponsors_data_path.read_text(encoding="utf-8")) if not (match_start and match_end): raise RuntimeError("Couldn't auto-generate sponsors section") + if not match_pre: + raise RuntimeError("Couldn't find pre section (<style>) in index.md") + frontmatter_end = match_pre.end() pre_end = match_start.end() post_start = match_end.start() template = Template(index_sponsors_template) message = template.render(sponsors=sponsors) - pre_content = content[:pre_end] + pre_content = content[frontmatter_end:pre_end] post_content = content[post_start:] new_content = pre_content + message + post_content return new_content @app.command() -def generate_readme(): +def generate_readme() -> None: """ Generate README.md content from main index.md """ @@ -249,7 +182,7 @@ def generate_readme(): @app.command() -def verify_readme(): +def verify_readme() -> None: """ Verify README.md content from main index.md """ @@ -266,23 +199,14 @@ def verify_readme(): @app.command() -def build_all(): +def build_all() -> None: """ Build mkdocs site for en, and then build each language inside, end result is located at directory ./site/ with each language inside. """ - site_path = Path("site").absolute() - update_languages(lang=None) - current_dir = os.getcwd() - os.chdir(en_docs_path) - typer.echo("Building docs for: en") - subprocess.run(["mkdocs", "build", "--site-dir", site_path], check=True) - os.chdir(current_dir) - langs = [] - for lang in get_lang_paths(): - if lang == en_docs_path or not lang.is_dir(): - continue - langs.append(lang.name) + update_languages() + shutil.rmtree(site_path, ignore_errors=True) + langs = [lang.name for lang in get_lang_paths() if lang.is_dir()] cpu_count = os.cpu_count() or 1 process_pool_size = cpu_count * 4 typer.echo(f"Using process pool size: {process_pool_size}") @@ -290,34 +214,16 @@ def build_all(): p.map(build_lang, langs) -def update_single_lang(lang: str): - lang_path = docs_path / lang - typer.echo(f"Updating {lang_path.name}") - update_config(lang_path.name) - - @app.command() -def update_languages( - lang: str = typer.Argument( - None, callback=lang_callback, autocompletion=complete_existing_lang - ) -): +def update_languages() -> None: """ Update the mkdocs.yml file Languages section including all the available languages. - - The LANG argument is a 2-letter language code. If it's not provided, update all the - mkdocs.yml files (for all the languages). """ - if lang is None: - for lang_path in get_lang_paths(): - if lang_path.is_dir(): - update_single_lang(lang_path.name) - else: - update_single_lang(lang) + update_config() @app.command() -def serve(): +def serve() -> None: """ A quick server to preview a built site with translations. @@ -342,8 +248,8 @@ def serve(): def live( lang: str = typer.Argument( None, callback=lang_callback, autocompletion=complete_existing_lang - ) -): + ), +) -> None: """ Serve with livereload a docs site for a specific language. @@ -353,6 +259,8 @@ def live( Takes an optional LANG argument with the name of the language to serve, by default en. """ + # Enable line numbers during local development to make it easier to highlight + os.environ["LINENUMS"] = "true" if lang is None: lang = "en" lang_path: Path = docs_path / lang @@ -360,92 +268,47 @@ def live( mkdocs.commands.serve.serve(dev_addr="127.0.0.1:8008") -def update_config(lang: str): - lang_path: Path = docs_path / lang - config_path = lang_path / mkdocs_name - current_config: dict = mkdocs.utils.yaml_load( - config_path.read_text(encoding="utf-8") - ) - if lang == "en": - config = get_en_config() - else: - config = get_base_lang_config(lang) - config["nav"] = current_config["nav"] - config["theme"]["language"] = current_config["theme"]["language"] +def update_config() -> None: + config = get_en_config() languages = [{"en": "/"}] - alternate: List[Dict[str, str]] = config["extra"].get("alternate", []) - alternate_dict = {alt["link"]: alt["name"] for alt in alternate} new_alternate: List[Dict[str, str]] = [] + # Language names sourced from https://quickref.me/iso-639-1 + # Contributors may wish to update or change these, e.g. to fix capitalization. + language_names_path = Path(__file__).parent / "../docs/language_names.yml" + local_language_names: Dict[str, str] = mkdocs.utils.yaml_load( + language_names_path.read_text(encoding="utf-8") + ) for lang_path in get_lang_paths(): - if lang_path.name == "en" or not lang_path.is_dir(): + if lang_path.name in {"en", "em"} or not lang_path.is_dir(): continue - name = lang_path.name - languages.append({name: f"/{name}/"}) + code = lang_path.name + languages.append({code: f"/{code}/"}) for lang_dict in languages: - name = list(lang_dict.keys())[0] - url = lang_dict[name] - if url not in alternate_dict: - new_alternate.append({"link": url, "name": name}) - else: - use_name = alternate_dict[url] - new_alternate.append({"link": url, "name": use_name}) - config["nav"][1] = {"Languages": languages} + code = list(lang_dict.keys())[0] + url = lang_dict[code] + if code not in local_language_names: + print( + f"Missing language name for: {code}, " + "update it in docs/language_names.yml" + ) + raise typer.Abort() + use_name = f"{code} - {local_language_names[code]}" + new_alternate.append({"link": url, "name": use_name}) + new_alternate.append({"link": "/em/", "name": "😉"}) config["extra"]["alternate"] = new_alternate - config_path.write_text( + en_config_path.write_text( yaml.dump(config, sort_keys=False, width=200, allow_unicode=True), encoding="utf-8", ) -def get_key_section( - *, key_to_section: Dict[Tuple[str, ...], list], key: Tuple[str, ...] -) -> list: - if key in key_to_section: - return key_to_section[key] - super_key = key[:-1] - title = key[-1] - super_section = get_key_section(key_to_section=key_to_section, key=super_key) - new_section = [] - super_section.append({title: new_section}) - key_to_section[key] = new_section - return new_section - - -def get_text_with_translate_missing(text: str) -> str: - lines = text.splitlines() - lines.insert(1, missing_translation_snippet) - new_text = "\n".join(lines) - return new_text - - -def get_file_to_nav_map(nav: list) -> Dict[str, Tuple[str, ...]]: - file_to_nav = {} - for item in nav: - if type(item) is str: - file_to_nav[item] = () - elif type(item) is dict: - item_key = list(item.keys())[0] - sub_nav = item[item_key] - sub_file_to_nav = get_file_to_nav_map(sub_nav) - for k, v in sub_file_to_nav.items(): - file_to_nav[k] = (item_key,) + v - return file_to_nav - - -def get_sections(nav: list) -> Dict[Tuple[str, ...], str]: - sections = {} - for item in nav: - if type(item) is str: - continue - elif type(item) is dict: - item_key = list(item.keys())[0] - sub_nav = item[item_key] - sections[(item_key,)] = sub_nav[0] - sub_sections = get_sections(sub_nav) - for k, v in sub_sections.items(): - new_key = (item_key,) + k - sections[new_key] = v - return sections +@app.command() +def langs_json(): + langs = [] + for lang_path in get_lang_paths(): + if lang_path.is_dir(): + langs.append(lang_path.name) + print(json.dumps(langs)) if __name__ == "__main__": diff --git a/scripts/format.sh b/scripts/format.sh index 3ac1fead86..11f25f1ce8 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -2,5 +2,4 @@ set -x ruff fastapi tests docs_src scripts --fix -black fastapi tests docs_src scripts -isort fastapi tests docs_src scripts +ruff format fastapi tests docs_src scripts diff --git a/scripts/gitter_releases_bot.py b/scripts/gitter_releases_bot.py deleted file mode 100644 index a033d0d69e..0000000000 --- a/scripts/gitter_releases_bot.py +++ /dev/null @@ -1,67 +0,0 @@ -import inspect -import os - -import requests - -room_id = "5c9c9540d73408ce4fbc1403" # FastAPI -# room_id = "5cc46398d73408ce4fbed233" # Gitter development - -gitter_token = os.getenv("GITTER_TOKEN") -assert gitter_token -github_token = os.getenv("GITHUB_TOKEN") -assert github_token -tag_name = os.getenv("TAG") -assert tag_name - - -def get_github_graphql(tag_name: str): - github_graphql = """ - { - repository(owner: "tiangolo", name: "fastapi") { - release (tagName: "{{tag_name}}" ) { - description - } - } - } - """ - github_graphql = github_graphql.replace("{{tag_name}}", tag_name) - return github_graphql - - -def get_github_release_text(tag_name: str): - url = "https://api.github.com/graphql" - headers = {"Authorization": f"Bearer {github_token}"} - github_graphql = get_github_graphql(tag_name=tag_name) - response = requests.post(url, json={"query": github_graphql}, headers=headers) - assert response.status_code == 200 - data = response.json() - return data["data"]["repository"]["release"]["description"] - - -def get_gitter_message(release_text: str): - text = f""" - New release! :tada: :rocket: - (by FastAPI bot) - - ## {tag_name} - """ - text = inspect.cleandoc(text) + "\n\n" + release_text - return text - - -def send_gitter_message(text: str): - headers = {"Authorization": f"Bearer {gitter_token}"} - url = f"https://api.gitter.im/v1/rooms/{room_id}/chatMessages" - data = {"text": text} - response = requests.post(url, headers=headers, json=data) - assert response.status_code == 200 - - -def main(): - release_text = get_github_release_text(tag_name=tag_name) - text = get_gitter_message(release_text=release_text) - send_gitter_message(text=text) - - -if __name__ == "__main__": - main() diff --git a/scripts/lint.sh b/scripts/lint.sh index 0feb973a87..c0e24db9f6 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,5 +5,4 @@ set -x mypy fastapi ruff fastapi tests docs_src scripts -black fastapi tests --check -isort fastapi tests docs_src scripts --check-only +ruff format fastapi tests --check diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py new file mode 100644 index 0000000000..8335a13f62 --- /dev/null +++ b/scripts/mkdocs_hooks.py @@ -0,0 +1,140 @@ +from functools import lru_cache +from pathlib import Path +from typing import Any, List, Union + +import material +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.structure.files import File, Files +from mkdocs.structure.nav import Link, Navigation, Section +from mkdocs.structure.pages import Page + +non_traslated_sections = [ + "reference/", + "release-notes.md", +] + + +@lru_cache +def get_missing_translation_content(docs_dir: str) -> str: + docs_dir_path = Path(docs_dir) + missing_translation_path = docs_dir_path.parent.parent / "missing-translation.md" + return missing_translation_path.read_text(encoding="utf-8") + + +@lru_cache +def get_mkdocs_material_langs() -> List[str]: + material_path = Path(material.__file__).parent + material_langs_path = material_path / "templates" / "partials" / "languages" + langs = [file.stem for file in material_langs_path.glob("*.html")] + return langs + + +class EnFile(File): + pass + + +def on_config(config: MkDocsConfig, **kwargs: Any) -> MkDocsConfig: + available_langs = get_mkdocs_material_langs() + dir_path = Path(config.docs_dir) + lang = dir_path.parent.name + if lang in available_langs: + config.theme["language"] = lang + if not (config.site_url or "").endswith(f"{lang}/") and not lang == "en": + config.site_url = f"{config.site_url}{lang}/" + return config + + +def resolve_file(*, item: str, files: Files, config: MkDocsConfig) -> None: + item_path = Path(config.docs_dir) / item + if not item_path.is_file(): + en_src_dir = (Path(config.docs_dir) / "../../en/docs").resolve() + potential_path = en_src_dir / item + if potential_path.is_file(): + files.append( + EnFile( + path=item, + src_dir=str(en_src_dir), + dest_dir=config.site_dir, + use_directory_urls=config.use_directory_urls, + ) + ) + + +def resolve_files(*, items: List[Any], files: Files, config: MkDocsConfig) -> None: + for item in items: + if isinstance(item, str): + resolve_file(item=item, files=files, config=config) + elif isinstance(item, dict): + assert len(item) == 1 + values = list(item.values()) + if not values: + continue + if isinstance(values[0], str): + resolve_file(item=values[0], files=files, config=config) + elif isinstance(values[0], list): + resolve_files(items=values[0], files=files, config=config) + else: + raise ValueError(f"Unexpected value: {values}") + + +def on_files(files: Files, *, config: MkDocsConfig) -> Files: + resolve_files(items=config.nav or [], files=files, config=config) + if "logo" in config.theme: + resolve_file(item=config.theme["logo"], files=files, config=config) + if "favicon" in config.theme: + resolve_file(item=config.theme["favicon"], files=files, config=config) + resolve_files(items=config.extra_css, files=files, config=config) + resolve_files(items=config.extra_javascript, files=files, config=config) + return files + + +def generate_renamed_section_items( + items: List[Union[Page, Section, Link]], *, config: MkDocsConfig +) -> List[Union[Page, Section, Link]]: + new_items: List[Union[Page, Section, Link]] = [] + for item in items: + if isinstance(item, Section): + new_title = item.title + new_children = generate_renamed_section_items(item.children, config=config) + first_child = new_children[0] + if isinstance(first_child, Page): + if first_child.file.src_path.endswith("index.md"): + # Read the source so that the title is parsed and available + first_child.read_source(config=config) + new_title = first_child.title or new_title + # Creating a new section makes it render it collapsed by default + # no idea why, so, let's just modify the existing one + # new_section = Section(title=new_title, children=new_children) + item.title = new_title + item.children = new_children + new_items.append(item) + else: + new_items.append(item) + return new_items + + +def on_nav( + nav: Navigation, *, config: MkDocsConfig, files: Files, **kwargs: Any +) -> Navigation: + new_items = generate_renamed_section_items(nav.items, config=config) + return Navigation(items=new_items, pages=nav.pages) + + +def on_pre_page(page: Page, *, config: MkDocsConfig, files: Files) -> Page: + return page + + +def on_page_markdown( + markdown: str, *, page: Page, config: MkDocsConfig, files: Files +) -> str: + if isinstance(page.file, EnFile): + for excluded_section in non_traslated_sections: + if page.file.src_path.startswith(excluded_section): + return markdown + missing_translation_content = get_missing_translation_content(config.docs_dir) + header = "" + body = markdown + if markdown.startswith("#"): + header, _, body = markdown.partition("\n\n") + return f"{header}\n\n{missing_translation_content}\n\n{body}" + return markdown diff --git a/scripts/notify.sh b/scripts/notify.sh deleted file mode 100755 index 8ce550026a..0000000000 --- a/scripts/notify.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -e - -python scripts/gitter_releases_bot.py diff --git a/scripts/playwright/separate_openapi_schemas/image01.py b/scripts/playwright/separate_openapi_schemas/image01.py new file mode 100644 index 0000000000..0b40f3bbcf --- /dev/null +++ b/scripts/playwright/separate_openapi_schemas/image01.py @@ -0,0 +1,29 @@ +import subprocess + +from playwright.sync_api import Playwright, sync_playwright + + +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_text("POST/items/Create Item").click() + page.get_by_role("tab", name="Schema").first.click() + page.screenshot( + path="docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png" + ) + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["uvicorn", "docs_src.separate_openapi_schemas.tutorial001:app"] +) +try: + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/separate_openapi_schemas/image02.py b/scripts/playwright/separate_openapi_schemas/image02.py new file mode 100644 index 0000000000..f76af7ee22 --- /dev/null +++ b/scripts/playwright/separate_openapi_schemas/image02.py @@ -0,0 +1,30 @@ +import subprocess + +from playwright.sync_api import Playwright, sync_playwright + + +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_text("GET/items/Read Items").click() + page.get_by_role("button", name="Try it out").click() + page.get_by_role("button", name="Execute").click() + page.screenshot( + path="docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png" + ) + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["uvicorn", "docs_src.separate_openapi_schemas.tutorial001:app"] +) +try: + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/separate_openapi_schemas/image03.py b/scripts/playwright/separate_openapi_schemas/image03.py new file mode 100644 index 0000000000..127f5c428e --- /dev/null +++ b/scripts/playwright/separate_openapi_schemas/image03.py @@ -0,0 +1,30 @@ +import subprocess + +from playwright.sync_api import Playwright, sync_playwright + + +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_text("GET/items/Read Items").click() + page.get_by_role("tab", name="Schema").click() + page.get_by_label("Schema").get_by_role("button", name="Expand all").click() + page.screenshot( + path="docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png" + ) + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["uvicorn", "docs_src.separate_openapi_schemas.tutorial001:app"] +) +try: + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/separate_openapi_schemas/image04.py b/scripts/playwright/separate_openapi_schemas/image04.py new file mode 100644 index 0000000000..208eaf8a0c --- /dev/null +++ b/scripts/playwright/separate_openapi_schemas/image04.py @@ -0,0 +1,29 @@ +import subprocess + +from playwright.sync_api import Playwright, sync_playwright + + +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="Item-Input").click() + page.get_by_role("button", name="Item-Output").click() + page.set_viewport_size({"width": 960, "height": 820}) + page.screenshot( + path="docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png" + ) + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["uvicorn", "docs_src.separate_openapi_schemas.tutorial001:app"] +) +try: + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/separate_openapi_schemas/image05.py b/scripts/playwright/separate_openapi_schemas/image05.py new file mode 100644 index 0000000000..83966b4498 --- /dev/null +++ b/scripts/playwright/separate_openapi_schemas/image05.py @@ -0,0 +1,29 @@ +import subprocess + +from playwright.sync_api import Playwright, sync_playwright + + +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="Item", exact=True).click() + page.set_viewport_size({"width": 960, "height": 700}) + page.screenshot( + path="docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png" + ) + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["uvicorn", "docs_src.separate_openapi_schemas.tutorial002:app"] +) +try: + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/test.sh b/scripts/test.sh index 62449ea415..7d17add8fa 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,7 +3,5 @@ set -e set -x -# Check README.md is up to date -python ./scripts/docs.py verify-readme export PYTHONPATH=./docs_src coverage run -m pytest tests ${@} diff --git a/scripts/zip-docs.sh b/scripts/zip-docs.sh deleted file mode 100644 index 69315f5ddd..0000000000 --- a/scripts/zip-docs.sh +++ /dev/null @@ -1,11 +0,0 @@ -#! /usr/bin/env bash - -set -x -set -e - -cd ./site - -if [ -f docs.zip ]; then - rm -rf docs.zip -fi -zip -r docs.zip ./ diff --git a/tests/main.py b/tests/main.py index fce6657040..15760c0396 100644 --- a/tests/main.py +++ b/tests/main.py @@ -49,12 +49,7 @@ def get_bool_id(item_id: bool): @app.get("/path/param/{item_id}") -def get_path_param_id(item_id: str = Path()): - return item_id - - -@app.get("/path/param-required/{item_id}") -def get_path_param_required_id(item_id: str = Path()): +def get_path_param_id(item_id: Optional[str] = Path()): return item_id diff --git a/tests/test_additional_properties.py b/tests/test_additional_properties.py index 016c1f734e..be14d10edc 100644 --- a/tests/test_additional_properties.py +++ b/tests/test_additional_properties.py @@ -19,92 +19,91 @@ def foo(items: Items): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/foo": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Foo", - "operationId": "foo_foo_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Items"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Items": { - "title": "Items", - "required": ["items"], - "type": "object", - "properties": { - "items": { - "title": "Items", - "type": "object", - "additionalProperties": {"type": "integer"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_additional_properties_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_additional_properties_post(): response = client.post("/foo", json={"items": {"foo": 1, "bar": 2}}) assert response.status_code == 200, response.text assert response.json() == {"foo": 1, "bar": 2} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/foo": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Foo", + "operationId": "foo_foo_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Items"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Items": { + "title": "Items", + "required": ["items"], + "type": "object", + "properties": { + "items": { + "title": "Items", + "type": "object", + "additionalProperties": {"type": "integer"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py new file mode 100644 index 0000000000..de59e48ce7 --- /dev/null +++ b/tests/test_additional_properties_bool.py @@ -0,0 +1,133 @@ +from typing import Union + +from dirty_equals import IsDict +from fastapi import FastAPI +from fastapi._compat import PYDANTIC_V2 +from fastapi.testclient import TestClient +from pydantic import BaseModel, ConfigDict + + +class FooBaseModel(BaseModel): + if PYDANTIC_V2: + model_config = ConfigDict(extra="forbid") + else: + + class Config: + extra = "forbid" + + +class Foo(FooBaseModel): + pass + + +app = FastAPI() + + +@app.post("/") +async def post( + foo: Union[Foo, None] = None, +): + return foo + + +client = TestClient(app) + + +def test_call_invalid(): + response = client.post("/", json={"foo": {"bar": "baz"}}) + assert response.status_code == 422 + + +def test_call_valid(): + response = client.post("/", json={}) + assert response.status_code == 200 + assert response.json() == {} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Post", + "operationId": "post__post", + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Foo"}, + {"type": "null"}, + ], + "title": "Foo", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Foo"} + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "Foo": { + "properties": {}, + "additionalProperties": False, + "type": "object", + "title": "Foo", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_additional_response_extra.py b/tests/test_additional_response_extra.py index 1df1891e05..55be19bad2 100644 --- a/tests/test_additional_response_extra.py +++ b/tests/test_additional_response_extra.py @@ -17,36 +17,33 @@ router.include_router(sub_router, prefix="/items") app.include_router(router) - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Item", - "operationId": "read_item_items__get", - } - } - }, -} - client = TestClient(app) -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_path_operation(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == {"id": "foo"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Item", + "operationId": "read_item_items__get", + } + } + }, + } diff --git a/tests/test_additional_responses_bad.py b/tests/test_additional_responses_bad.py index d2eb4d7a19..36df07f468 100644 --- a/tests/test_additional_responses_bad.py +++ b/tests/test_additional_responses_bad.py @@ -11,7 +11,7 @@ async def a(): openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a": { diff --git a/tests/test_additional_responses_custom_model_in_callback.py b/tests/test_additional_responses_custom_model_in_callback.py index a1072cc569..2ad5754551 100644 --- a/tests/test_additional_responses_custom_model_in_callback.py +++ b/tests/test_additional_responses_custom_model_in_callback.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, HttpUrl @@ -25,114 +26,127 @@ 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": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "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 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Main Route", + "operationId": "main_route__post", + "parameters": [ + { + "required": True, + "schema": IsDict( + { + "title": "Callback Url", + "minLength": 1, + "type": "string", + "format": "uri", + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict( + { + "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": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_additional_responses_custom_validationerror.py b/tests/test_additional_responses_custom_validationerror.py index 811fe69221..9fec5c96d4 100644 --- a/tests/test_additional_responses_custom_validationerror.py +++ b/tests/test_additional_responses_custom_validationerror.py @@ -30,71 +30,70 @@ async def a(id): pass # pragma: no cover -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a/{id}": { - "get": { - "responses": { - "422": { - "description": "Error", - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/JsonApiError"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/vnd.api+json": {"schema": {}}}, - }, - }, - "summary": "A", - "operationId": "a_a__id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Id"}, - "name": "id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Error": { - "title": "Error", - "required": ["status", "title"], - "type": "object", - "properties": { - "status": {"title": "Status", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - }, - }, - "JsonApiError": { - "title": "JsonApiError", - "required": ["errors"], - "type": "object", - "properties": { - "errors": { - "title": "Errors", - "type": "array", - "items": {"$ref": "#/components/schemas/Error"}, - } - }, - }, - } - }, -} - - client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a/{id}": { + "get": { + "responses": { + "422": { + "description": "Error", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/JsonApiError" + } + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/vnd.api+json": {"schema": {}}}, + }, + }, + "summary": "A", + "operationId": "a_a__id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Id"}, + "name": "id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "Error": { + "title": "Error", + "required": ["status", "title"], + "type": "object", + "properties": { + "status": {"title": "Status", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + }, + }, + "JsonApiError": { + "title": "JsonApiError", + "required": ["errors"], + "type": "object", + "properties": { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "#/components/schemas/Error"}, + } + }, + }, + } + }, + } diff --git a/tests/test_additional_responses_default_validationerror.py b/tests/test_additional_responses_default_validationerror.py index cabb536d71..153f04f579 100644 --- a/tests/test_additional_responses_default_validationerror.py +++ b/tests/test_additional_responses_default_validationerror.py @@ -9,77 +9,76 @@ async def a(id): pass # pragma: no cover -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a/{id}": { - "get": { - "responses": { - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "A", - "operationId": "a_a__id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Id"}, - "name": "id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a/{id}": { + "get": { + "responses": { + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "A", + "operationId": "a_a__id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Id"}, + "name": "id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_additional_responses_response_class.py b/tests/test_additional_responses_response_class.py index aa549b1636..68753561c9 100644 --- a/tests/test_additional_responses_response_class.py +++ b/tests/test_additional_responses_response_class.py @@ -35,83 +35,82 @@ async def b(): pass # pragma: no cover -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/JsonApiError"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/vnd.api+json": {"schema": {}}}, - }, - }, - "summary": "A", - "operationId": "a_a_get", - } - }, - "/b": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Error"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "B", - "operationId": "b_b_get", - } - }, - }, - "components": { - "schemas": { - "Error": { - "title": "Error", - "required": ["status", "title"], - "type": "object", - "properties": { - "status": {"title": "Status", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - }, - }, - "JsonApiError": { - "title": "JsonApiError", - "required": ["errors"], - "type": "object", - "properties": { - "errors": { - "title": "Errors", - "type": "array", - "items": {"$ref": "#/components/schemas/Error"}, - } - }, - }, - } - }, -} - - client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/JsonApiError" + } + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/vnd.api+json": {"schema": {}}}, + }, + }, + "summary": "A", + "operationId": "a_a_get", + } + }, + "/b": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Error"} + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "B", + "operationId": "b_b_get", + } + }, + }, + "components": { + "schemas": { + "Error": { + "title": "Error", + "required": ["status", "title"], + "type": "object", + "properties": { + "status": {"title": "Status", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + }, + }, + "JsonApiError": { + "title": "JsonApiError", + "required": ["errors"], + "type": "object", + "properties": { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "#/components/schemas/Error"}, + } + }, + }, + } + }, + } diff --git a/tests/test_additional_responses_router.py b/tests/test_additional_responses_router.py index fe4956f8fc..71cabc7c3d 100644 --- a/tests/test_additional_responses_router.py +++ b/tests/test_additional_responses_router.py @@ -53,103 +53,10 @@ async def d(): app.include_router(router) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a": { - "get": { - "responses": { - "501": {"description": "Error 1"}, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "A", - "operationId": "a_a_get", - } - }, - "/b": { - "get": { - "responses": { - "502": {"description": "Error 2"}, - "4XX": {"description": "Error with range, upper"}, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "B", - "operationId": "b_b_get", - } - }, - "/c": { - "get": { - "responses": { - "400": {"description": "Error with str"}, - "5XX": {"description": "Error with range, lower"}, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "default": {"description": "A default response"}, - }, - "summary": "C", - "operationId": "c_c_get", - } - }, - "/d": { - "get": { - "responses": { - "400": {"description": "Error with str"}, - "5XX": { - "description": "Server Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ResponseModel"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "default": { - "description": "Default Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ResponseModel"} - } - }, - }, - }, - "summary": "D", - "operationId": "d_d_get", - } - }, - }, - "components": { - "schemas": { - "ResponseModel": { - "title": "ResponseModel", - "required": ["message"], - "type": "object", - "properties": {"message": {"title": "Message", "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 - - def test_a(): response = client.get("/a") assert response.status_code == 200, response.text @@ -172,3 +79,99 @@ def test_d(): response = client.get("/d") assert response.status_code == 200, response.text assert response.json() == "d" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a": { + "get": { + "responses": { + "501": {"description": "Error 1"}, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "A", + "operationId": "a_a_get", + } + }, + "/b": { + "get": { + "responses": { + "502": {"description": "Error 2"}, + "4XX": {"description": "Error with range, upper"}, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "B", + "operationId": "b_b_get", + } + }, + "/c": { + "get": { + "responses": { + "400": {"description": "Error with str"}, + "5XX": {"description": "Error with range, lower"}, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "default": {"description": "A default response"}, + }, + "summary": "C", + "operationId": "c_c_get", + } + }, + "/d": { + "get": { + "responses": { + "400": {"description": "Error with str"}, + "5XX": { + "description": "Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseModel" + } + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "default": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseModel" + } + } + }, + }, + }, + "summary": "D", + "operationId": "d_d_get", + } + }, + }, + "components": { + "schemas": { + "ResponseModel": { + "title": "ResponseModel", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + } + } + }, + } diff --git a/tests/test_ambiguous_params.py b/tests/test_ambiguous_params.py new file mode 100644 index 0000000000..8a31442eb0 --- /dev/null +++ b/tests/test_ambiguous_params.py @@ -0,0 +1,75 @@ +import pytest +from fastapi import Depends, FastAPI, Path +from fastapi.param_functions import Query +from fastapi.testclient import TestClient +from fastapi.utils import PYDANTIC_V2 +from typing_extensions import Annotated + +app = FastAPI() + + +def test_no_annotated_defaults(): + with pytest.raises( + AssertionError, match="Path parameters cannot have a default value" + ): + + @app.get("/items/{item_id}/") + async def get_item(item_id: Annotated[int, Path(default=1)]): + pass # pragma: nocover + + with pytest.raises( + AssertionError, + match=( + "`Query` default value cannot be set in `Annotated` for 'item_id'. Set the" + " default value with `=` instead." + ), + ): + + @app.get("/") + async def get(item_id: Annotated[int, Query(default=1)]): + pass # pragma: nocover + + +def test_multiple_annotations(): + async def dep(): + pass # pragma: nocover + + @app.get("/multi-query") + async def get(foo: Annotated[int, Query(gt=2), Query(lt=10)]): + return foo + + with pytest.raises( + AssertionError, + match=( + "Cannot specify `Depends` in `Annotated` and default value" + " together for 'foo'" + ), + ): + + @app.get("/") + async def get2(foo: Annotated[int, Depends(dep)] = Depends(dep)): + pass # pragma: nocover + + with pytest.raises( + AssertionError, + match=( + "Cannot specify a FastAPI annotation in `Annotated` and `Depends` as a" + " default value together for 'foo'" + ), + ): + + @app.get("/") + async def get3(foo: Annotated[int, Query(min_length=1)] = Depends(dep)): + pass # pragma: nocover + + client = TestClient(app) + response = client.get("/multi-query", params={"foo": "5"}) + assert response.status_code == 200 + assert response.json() == 5 + + response = client.get("/multi-query", params={"foo": "123"}) + assert response.status_code == 422 + + if PYDANTIC_V2: + response = client.get("/multi-query", params={"foo": "1"}) + assert response.status_code == 422 diff --git a/tests/test_annotated.py b/tests/test_annotated.py new file mode 100644 index 0000000000..2222be9783 --- /dev/null +++ b/tests/test_annotated.py @@ -0,0 +1,315 @@ +import pytest +from dirty_equals import IsDict +from fastapi import APIRouter, FastAPI, Query +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/default") +async def default(foo: Annotated[str, Query()] = "foo"): + return {"foo": foo} + + +@app.get("/required") +async def required(foo: Annotated[str, Query(min_length=1)]): + return {"foo": foo} + + +@app.get("/multiple") +async def multiple(foo: Annotated[str, object(), Query(min_length=1)]): + return {"foo": foo} + + +@app.get("/unrelated") +async def unrelated(foo: Annotated[str, object()]): + return {"foo": foo} + + +client = TestClient(app) + +foo_is_missing = { + "detail": [ + IsDict( + { + "loc": ["query", "foo"], + "msg": "Field required", + "type": "missing", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict( + { + "loc": ["query", "foo"], + "msg": "field required", + "type": "value_error.missing", + } + ) + ] +} +foo_is_short = { + "detail": [ + IsDict( + { + "ctx": {"min_length": 1}, + "loc": ["query", "foo"], + "msg": "String should have at least 1 character", + "type": "string_too_short", + "input": "", + "url": match_pydantic_error_url("string_too_short"), + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict( + { + "ctx": {"limit_value": 1}, + "loc": ["query", "foo"], + "msg": "ensure this value has at least 1 characters", + "type": "value_error.any_str.min_length", + } + ) + ] +} + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/default", 200, {"foo": "foo"}), + ("/default?foo=bar", 200, {"foo": "bar"}), + ("/required?foo=bar", 200, {"foo": "bar"}), + ("/required", 422, foo_is_missing), + ("/required?foo=", 422, foo_is_short), + ("/multiple?foo=bar", 200, {"foo": "bar"}), + ("/multiple", 422, foo_is_missing), + ("/multiple?foo=", 422, foo_is_short), + ("/unrelated?foo=bar", 200, {"foo": "bar"}), + ("/unrelated", 422, foo_is_missing), + ], +) +def test_get(path, expected_status, expected_response): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_multiple_path(): + app = FastAPI() + + @app.get("/test1") + @app.get("/test2") + async def test(var: Annotated[str, Query()] = "bar"): + return {"foo": var} + + client = TestClient(app) + response = client.get("/test1") + assert response.status_code == 200 + assert response.json() == {"foo": "bar"} + + response = client.get("/test1", params={"var": "baz"}) + assert response.status_code == 200 + assert response.json() == {"foo": "baz"} + + response = client.get("/test2") + assert response.status_code == 200 + assert response.json() == {"foo": "bar"} + + response = client.get("/test2", params={"var": "baz"}) + assert response.status_code == 200 + assert response.json() == {"foo": "baz"} + + +def test_nested_router(): + app = FastAPI() + + router = APIRouter(prefix="/nested") + + @router.get("/test") + async def test(var: Annotated[str, Query()] = "bar"): + return {"foo": var} + + app.include_router(router) + + client = TestClient(app) + + response = client.get("/nested/test") + assert response.status_code == 200 + assert response.json() == {"foo": "bar"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/default": { + "get": { + "summary": "Default", + "operationId": "default_default_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Foo", + "type": "string", + "default": "foo", + }, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/required": { + "get": { + "summary": "Required", + "operationId": "required_required_get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Foo", + "minLength": 1, + "type": "string", + }, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/multiple": { + "get": { + "summary": "Multiple", + "operationId": "multiple_multiple_get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Foo", + "minLength": 1, + "type": "string", + }, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/unrelated": { + "get": { + "summary": "Unrelated", + "operationId": "unrelated_unrelated_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Foo", "type": "string"}, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_application.py b/tests/test_application.py index b7d72f9ad1..ea7a80128f 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,1162 +1,11 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from .main import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/api_route": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Non Operation", - "operationId": "non_operation_api_route_get", - } - }, - "/non_decorated_route": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Non Decorated Route", - "operationId": "non_decorated_route_non_decorated_route_get", - } - }, - "/text": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Get Text", - "operationId": "get_text_text_get", - } - }, - "/path/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Id", - "operationId": "get_id_path__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/str/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Str Id", - "operationId": "get_str_id_path_str__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Int Id", - "operationId": "get_int_id_path_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/float/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Float Id", - "operationId": "get_float_id_path_float__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "number"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/bool/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Bool Id", - "operationId": "get_bool_id_path_bool__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "boolean"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Id", - "operationId": "get_path_param_id_path_param__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-required/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Required Id", - "operationId": "get_path_param_required_id_path_param_required__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-minlength/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Min Length", - "operationId": "get_path_param_min_length_path_param_minlength__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "minLength": 3, - "type": "string", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-maxlength/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Max Length", - "operationId": "get_path_param_max_length_path_param_maxlength__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maxLength": 3, - "type": "string", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-min_maxlength/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Min Max Length", - "operationId": "get_path_param_min_max_length_path_param_min_maxlength__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maxLength": 3, - "minLength": 2, - "type": "string", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-gt/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Gt", - "operationId": "get_path_param_gt_path_param_gt__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 3.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-gt0/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Gt0", - "operationId": "get_path_param_gt0_path_param_gt0__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 0.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-ge/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Ge", - "operationId": "get_path_param_ge_path_param_ge__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "minimum": 3.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt", - "operationId": "get_path_param_lt_path_param_lt__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt0/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt0", - "operationId": "get_path_param_lt0_path_param_lt0__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 0.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-le/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Le", - "operationId": "get_path_param_le_path_param_le__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt-gt/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt Gt", - "operationId": "get_path_param_lt_gt_path_param_lt_gt__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "exclusiveMinimum": 1.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-le-ge/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Le Ge", - "operationId": "get_path_param_le_ge_path_param_le_ge__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "minimum": 1.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt Int", - "operationId": "get_path_param_lt_int_path_param_lt_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-gt-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Gt Int", - "operationId": "get_path_param_gt_int_path_param_gt_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 3.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-le-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Le Int", - "operationId": "get_path_param_le_int_path_param_le_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-ge-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Ge Int", - "operationId": "get_path_param_ge_int_path_param_ge_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "minimum": 3.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt-gt-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt Gt Int", - "operationId": "get_path_param_lt_gt_int_path_param_lt_gt_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "exclusiveMinimum": 1.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-le-ge-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Le Ge Int", - "operationId": "get_path_param_le_ge_int_path_param_le_ge_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "minimum": 1.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/query": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query", - "operationId": "get_query_query_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Query"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/optional": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Optional", - "operationId": "get_query_optional_query_optional_get", - "parameters": [ - { - "required": False, - "schema": {"title": "Query"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/int": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Type", - "operationId": "get_query_type_query_int_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Query", "type": "integer"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/int/optional": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Type Optional", - "operationId": "get_query_type_optional_query_int_optional_get", - "parameters": [ - { - "required": False, - "schema": {"title": "Query", "type": "integer"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/int/default": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Type Int Default", - "operationId": "get_query_type_int_default_query_int_default_get", - "parameters": [ - { - "required": False, - "schema": {"title": "Query", "type": "integer", "default": 10}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/param": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Param", - "operationId": "get_query_param_query_param_get", - "parameters": [ - { - "required": False, - "schema": {"title": "Query"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/param-required": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Param Required", - "operationId": "get_query_param_required_query_param_required_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Query"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/param-required/int": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Param Required Type", - "operationId": "get_query_param_required_type_query_param_required_int_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Query", "type": "integer"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/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", - } - }, - "/query/frozenset": { - "get": { - "summary": "Get Query Type Frozenset", - "operationId": "get_query_type_frozenset_query_frozenset_get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Query", - "uniqueItems": True, - "type": "array", - "items": {"type": "integer"}, - }, - "name": "query", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -1164,7 +13,6 @@ openapi_schema = { ("/api_route", 200, {"message": "Hello World"}), ("/non_decorated_route", 200, {"message": "Hello World"}), ("/nonexistent", 404, {"detail": "Not Found"}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get_path(path, expected_status, expected_response): @@ -1202,3 +50,1149 @@ def test_enum_status_code_response(): response = client.get("/enum-status-code") assert response.status_code == 201, response.text assert response.json() == "foo bar" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/api_route": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Non Operation", + "operationId": "non_operation_api_route_get", + } + }, + "/non_decorated_route": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Non Decorated Route", + "operationId": "non_decorated_route_non_decorated_route_get", + } + }, + "/text": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Get Text", + "operationId": "get_text_text_get", + } + }, + "/path/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Id", + "operationId": "get_id_path__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/str/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Str Id", + "operationId": "get_str_id_path_str__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Int Id", + "operationId": "get_int_id_path_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/float/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Float Id", + "operationId": "get_float_id_path_float__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "number"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/bool/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Bool Id", + "operationId": "get_bool_id_path_bool__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "boolean"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Id", + "operationId": "get_path_param_id_path_param__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Item Id", + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict({"title": "Item Id", "type": "string"}), + } + ], + } + }, + "/path/param-minlength/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Min Length", + "operationId": "get_path_param_min_length_path_param_minlength__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "minLength": 3, + "type": "string", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-maxlength/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Max Length", + "operationId": "get_path_param_max_length_path_param_maxlength__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maxLength": 3, + "type": "string", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-min_maxlength/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Min Max Length", + "operationId": "get_path_param_min_max_length_path_param_min_maxlength__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maxLength": 3, + "minLength": 2, + "type": "string", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-gt/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Gt", + "operationId": "get_path_param_gt_path_param_gt__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMinimum": 3.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-gt0/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Gt0", + "operationId": "get_path_param_gt0_path_param_gt0__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMinimum": 0.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-ge/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Ge", + "operationId": "get_path_param_ge_path_param_ge__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "minimum": 3.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt", + "operationId": "get_path_param_lt_path_param_lt__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 3.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt0/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt0", + "operationId": "get_path_param_lt0_path_param_lt0__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 0.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-le/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Le", + "operationId": "get_path_param_le_path_param_le__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maximum": 3.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt-gt/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt Gt", + "operationId": "get_path_param_lt_gt_path_param_lt_gt__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 3.0, + "exclusiveMinimum": 1.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-le-ge/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Le Ge", + "operationId": "get_path_param_le_ge_path_param_le_ge__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maximum": 3.0, + "minimum": 1.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt Int", + "operationId": "get_path_param_lt_int_path_param_lt_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 3.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-gt-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Gt Int", + "operationId": "get_path_param_gt_int_path_param_gt_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMinimum": 3.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-le-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Le Int", + "operationId": "get_path_param_le_int_path_param_le_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maximum": 3.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-ge-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Ge Int", + "operationId": "get_path_param_ge_int_path_param_ge_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "minimum": 3.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt-gt-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt Gt Int", + "operationId": "get_path_param_lt_gt_int_path_param_lt_gt_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 3.0, + "exclusiveMinimum": 1.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-le-ge-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Le Ge Int", + "operationId": "get_path_param_le_ge_int_path_param_le_ge_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maximum": 3.0, + "minimum": 1.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/query": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query", + "operationId": "get_query_query_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Query"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/optional": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Optional", + "operationId": "get_query_optional_query_optional_get", + "parameters": [ + { + "required": False, + "schema": {"title": "Query"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/int": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Type", + "operationId": "get_query_type_query_int_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Query", "type": "integer"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/int/optional": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Type Optional", + "operationId": "get_query_type_optional_query_int_optional_get", + "parameters": [ + { + "name": "query", + "in": "query", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Query", + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict({"title": "Query", "type": "integer"}), + } + ], + } + }, + "/query/int/default": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Type Int Default", + "operationId": "get_query_type_int_default_query_int_default_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Query", + "type": "integer", + "default": 10, + }, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/param": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Param", + "operationId": "get_query_param_query_param_get", + "parameters": [ + { + "required": False, + "schema": {"title": "Query"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/param-required": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Param Required", + "operationId": "get_query_param_required_query_param_required_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Query"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/param-required/int": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Param Required Type", + "operationId": "get_query_param_required_type_query_param_required_int_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Query", "type": "integer"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/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", + } + }, + "/query/frozenset": { + "get": { + "summary": "Get Query Type Frozenset", + "operationId": "get_query_type_frozenset_query_frozenset_get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Query", + "uniqueItems": True, + "type": "array", + "items": {"type": "integer"}, + }, + "name": "query", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 0000000000..bf268b860b --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,93 @@ +from typing import List, Union + +from fastapi import FastAPI, UploadFile +from fastapi._compat import ( + ModelField, + Undefined, + _get_model_config, + is_bytes_sequence_annotation, + is_uploadfile_sequence_annotation, +) +from fastapi.testclient import TestClient +from pydantic import BaseConfig, BaseModel, ConfigDict +from pydantic.fields import FieldInfo + +from .utils import needs_pydanticv1, needs_pydanticv2 + + +@needs_pydanticv2 +def test_model_field_default_required(): + # For coverage + field_info = FieldInfo(annotation=str) + field = ModelField(name="foo", field_info=field_info) + assert field.default is Undefined + + +@needs_pydanticv1 +def test_upload_file_dummy_with_info_plain_validator_function(): + # For coverage + assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {} + + +@needs_pydanticv1 +def test_union_scalar_list(): + # For coverage + # TODO: there might not be a current valid code path that uses this, it would + # potentially enable query parameters defined as both a scalar and a list + # but that would require more refactors, also not sure it's really useful + from fastapi._compat import is_pv1_scalar_field + + field_info = FieldInfo() + field = ModelField( + name="foo", + field_info=field_info, + type_=Union[str, List[int]], + class_validators={}, + model_config=BaseConfig, + ) + assert not is_pv1_scalar_field(field) + + +@needs_pydanticv2 +def test_get_model_config(): + # For coverage in Pydantic v2 + class Foo(BaseModel): + model_config = ConfigDict(from_attributes=True) + + foo = Foo() + config = _get_model_config(foo) + assert config == {"from_attributes": True} + + +def test_complex(): + app = FastAPI() + + @app.post("/") + def foo(foo: Union[str, List[int]]): + return foo + + client = TestClient(app) + + response = client.post("/", json="bar") + assert response.status_code == 200, response.text + assert response.json() == "bar" + + response2 = client.post("/", json=[1, 2]) + assert response2.status_code == 200, response2.text + assert response2.json() == [1, 2] + + +def test_is_bytes_sequence_annotation_union(): + # For coverage + # TODO: in theory this would allow declaring types that could be lists of bytes + # to be read from files and other types, but I'm not even sure it's a good idea + # to support it as a first class "feature" + assert is_bytes_sequence_annotation(Union[List[str], List[bytes]]) + + +def test_is_uploadfile_sequence_annotation(): + # For coverage + # TODO: in theory this would allow declaring types that could be lists of UploadFile + # and other types, but I'm not even sure it's a good idea to support it as a first + # class "feature" + assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) diff --git a/tests/test_computed_fields.py b/tests/test_computed_fields.py new file mode 100644 index 0000000000..5286507b2d --- /dev/null +++ b/tests/test_computed_fields.py @@ -0,0 +1,77 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + app = FastAPI() + + from pydantic import BaseModel, computed_field + + class Rectangle(BaseModel): + width: int + length: int + + @computed_field + @property + def area(self) -> int: + return self.width * self.length + + @app.get("/") + def read_root() -> Rectangle: + return Rectangle(width=3, length=4) + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_get(client: TestClient): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"width": 3, "length": 4, "area": 12} + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Read Root", + "operationId": "read_root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Rectangle"} + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "Rectangle": { + "properties": { + "width": {"type": "integer", "title": "Width"}, + "length": {"type": "integer", "title": "Length"}, + "area": {"type": "integer", "title": "Area", "readOnly": True}, + }, + "type": "object", + "required": ["width", "length", "area"], + "title": "Rectangle", + } + } + }, + } diff --git a/tests/test_custom_route_class.py b/tests/test_custom_route_class.py index 2e8d9c6de5..55374584b0 100644 --- a/tests/test_custom_route_class.py +++ b/tests/test_custom_route_class.py @@ -46,49 +46,6 @@ app.include_router(router=router_a, prefix="/a") client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Get A", - "operationId": "get_a_a__get", - } - }, - "/a/b/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Get B", - "operationId": "get_b_a_b__get", - } - }, - "/a/b/c/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Get C", - "operationId": "get_c_a_b_c__get", - } - }, - }, -} - @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -96,7 +53,6 @@ openapi_schema = { ("/a", 200, {"msg": "A"}), ("/a/b", 200, {"msg": "B"}), ("/a/b/c", 200, {"msg": "C"}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get_path(path, expected_status, expected_response): @@ -113,3 +69,50 @@ def test_route_classes(): assert getattr(routes["/a/"], "x_type") == "A" # noqa: B009 assert getattr(routes["/a/b/"], "x_type") == "B" # noqa: B009 assert getattr(routes["/a/b/c/"], "x_type") == "C" # noqa: B009 + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Get A", + "operationId": "get_a_a__get", + } + }, + "/a/b/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Get B", + "operationId": "get_b_a_b__get", + } + }, + "/a/b/c/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Get C", + "operationId": "get_c_a_b_c__get", + } + }, + }, + } diff --git a/tests/test_custom_schema_fields.py b/tests/test_custom_schema_fields.py index 10b02608c3..ee51fc7ff5 100644 --- a/tests/test_custom_schema_fields.py +++ b/tests/test_custom_schema_fields.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel @@ -8,10 +9,18 @@ app = FastAPI() class Item(BaseModel): name: str - class Config: - schema_extra = { - "x-something-internal": {"level": 4}, + if PYDANTIC_V2: + model_config = { + "json_schema_extra": { + "x-something-internal": {"level": 4}, + } } + else: + + class Config: + schema_extra = { + "x-something-internal": {"level": 4}, + } @app.get("/foo", response_model=Item) diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 2e6217d34e..7e57d525ce 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -1,3 +1,4 @@ +import io from pathlib import Path from typing import List @@ -7,11 +8,17 @@ from fastapi.datastructures import Default from fastapi.testclient import TestClient +# TODO: remove when deprecating Pydantic v1 def test_upload_file_invalid(): with pytest.raises(ValueError): UploadFile.validate("not a Starlette UploadFile") +def test_upload_file_invalid_pydantic_v2(): + with pytest.raises(ValueError): + UploadFile._validate("not a Starlette UploadFile", {}) + + def test_default_placeholder_equals(): placeholder_1 = Default("a") placeholder_2 = Default("a") @@ -46,3 +53,20 @@ def test_upload_file_is_closed(tmp_path: Path): assert testing_file_store assert testing_file_store[0].file.closed + + +# For UploadFile coverage, segments copied from Starlette tests + + +@pytest.mark.anyio +async def test_upload_file(): + stream = io.BytesIO(b"data") + file = UploadFile(filename="file", file=stream, size=4) + assert await file.read() == b"data" + assert file.size == 4 + await file.write(b" and more data!") + assert await file.read() == b"" + assert file.size == 19 + await file.seek(0) + assert await file.read() == b"data and more data!" + await file.close() diff --git a/tests/test_datetime_custom_encoder.py b/tests/test_datetime_custom_encoder.py index 5c1833eb4c..3aa77c0b1d 100644 --- a/tests/test_datetime_custom_encoder.py +++ b/tests/test_datetime_custom_encoder.py @@ -4,31 +4,54 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel - -class ModelWithDatetimeField(BaseModel): - dt_field: datetime - - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } +from .utils import needs_pydanticv1, needs_pydanticv2 -app = FastAPI() -model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) +@needs_pydanticv2 +def test_pydanticv2(): + from pydantic import field_serializer + class ModelWithDatetimeField(BaseModel): + dt_field: datetime -@app.get("/model", response_model=ModelWithDatetimeField) -def get_model(): - return model + @field_serializer("dt_field") + def serialize_datetime(self, dt_field: datetime): + return dt_field.replace(microsecond=0, tzinfo=timezone.utc).isoformat() + app = FastAPI() + model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) -client = TestClient(app) + @app.get("/model", response_model=ModelWithDatetimeField) + def get_model(): + return model - -def test_dt(): + client = TestClient(app) + with client: + response = client.get("/model") + assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_pydanticv1(): + class ModelWithDatetimeField(BaseModel): + dt_field: datetime + + class Config: + json_encoders = { + datetime: lambda dt: dt.replace( + microsecond=0, tzinfo=timezone.utc + ).isoformat() + } + + app = FastAPI() + model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) + + @app.get("/model", response_model=ModelWithDatetimeField) + def get_model(): + return model + + client = TestClient(app) with client: response = client.get("/model") assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} diff --git a/tests/test_dependency_contextmanager.py b/tests/test_dependency_contextmanager.py index 03ef56c4d7..b07f9aa5b6 100644 --- a/tests/test_dependency_contextmanager.py +++ b/tests/test_dependency_contextmanager.py @@ -1,7 +1,9 @@ +import json from typing import Dict import pytest from fastapi import BackgroundTasks, Depends, FastAPI +from fastapi.responses import StreamingResponse from fastapi.testclient import TestClient app = FastAPI() @@ -200,6 +202,13 @@ async def get_sync_context_b_bg( return state +@app.middleware("http") +async def middleware(request, call_next): + response: StreamingResponse = await call_next(request) + response.headers["x-state"] = json.dumps(state.copy()) + return response + + client = TestClient(app) @@ -274,9 +283,13 @@ def test_background_tasks(): assert data["context_b"] == "started b" assert data["context_a"] == "started a" assert data["bg"] == "not set" + middleware_state = json.loads(response.headers["x-state"]) + assert middleware_state["context_b"] == "finished b with a: started a" + assert middleware_state["context_a"] == "finished a" + assert middleware_state["bg"] == "not set" assert state["context_b"] == "finished b with a: started a" assert state["context_a"] == "finished a" - assert state["bg"] == "bg set - b: started b - a: started a" + assert state["bg"] == "bg set - b: finished b with a: started a - a: finished a" def test_sync_raise_raises(): @@ -382,4 +395,7 @@ def test_sync_background_tasks(): assert data["sync_bg"] == "not set" assert state["context_b"] == "finished b with a: started a" assert state["context_a"] == "finished a" - assert state["sync_bg"] == "sync_bg set - b: started b - a: started a" + assert ( + state["sync_bg"] + == "sync_bg set - b: finished b with a: started a - a: finished a" + ) diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py index 33899134e9..0882cc41d7 100644 --- a/tests/test_dependency_duplicates.py +++ b/tests/test_dependency_duplicates.py @@ -1,7 +1,9 @@ from typing import List +from dirty_equals import IsDict from fastapi import Depends, FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel app = FastAPI() @@ -44,168 +46,33 @@ async def no_duplicates_sub( return [item, sub_items] -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/with-duplicates": { - "post": { - "summary": "With Duplicates", - "operationId": "with_duplicates_with_duplicates_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/no-duplicates": { - "post": { - "summary": "No Duplicates", - "operationId": "no_duplicates_no_duplicates_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_no_duplicates_no_duplicates_post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/with-duplicates-sub": { - "post": { - "summary": "No Duplicates Sub", - "operationId": "no_duplicates_sub_with_duplicates_sub_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_no_duplicates_no_duplicates_post": { - "title": "Body_no_duplicates_no_duplicates_post", - "required": ["item", "item2"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "item2": {"$ref": "#/components/schemas/Item"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["data"], - "type": "object", - "properties": {"data": {"title": "Data", "type": "string"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_no_duplicates_invalid(): response = client.post("/no-duplicates", json={"item": {"data": "myitem"}}) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", "item2"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item2"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item2"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_no_duplicates(): @@ -230,3 +97,152 @@ def test_sub_duplicates(): {"data": "myitem"}, [{"data": "myitem"}, {"data": "myitem"}], ] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/with-duplicates": { + "post": { + "summary": "With Duplicates", + "operationId": "with_duplicates_with_duplicates_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/no-duplicates": { + "post": { + "summary": "No Duplicates", + "operationId": "no_duplicates_no_duplicates_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_no_duplicates_no_duplicates_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/with-duplicates-sub": { + "post": { + "summary": "No Duplicates Sub", + "operationId": "no_duplicates_sub_with_duplicates_sub_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_no_duplicates_no_duplicates_post": { + "title": "Body_no_duplicates_no_duplicates_post", + "required": ["item", "item2"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["data"], + "type": "object", + "properties": {"data": {"title": "Data", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_dependency_overrides.py b/tests/test_dependency_overrides.py index 8bb307971b..21cff998d1 100644 --- a/tests/test_dependency_overrides.py +++ b/tests/test_dependency_overrides.py @@ -1,8 +1,10 @@ from typing import Optional import pytest +from dirty_equals import IsDict from fastapi import APIRouter, Depends, FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url app = FastAPI() @@ -50,99 +52,180 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend return msg -@pytest.mark.parametrize( - "url,status_code,expected", - [ - ( - "/main-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/main-depends/?q=foo", - 200, - {"in": "main-depends", "params": {"q": "foo", "skip": 0, "limit": 100}}, - ), - ( - "/main-depends/?q=foo&skip=100&limit=200", - 200, - {"in": "main-depends", "params": {"q": "foo", "skip": 100, "limit": 200}}, - ), - ( - "/decorator-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/decorator-depends/?q=foo", 200, {"in": "decorator-depends"}), - ( - "/decorator-depends/?q=foo&skip=100&limit=200", - 200, - {"in": "decorator-depends"}, - ), - ( - "/router-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/router-depends/?q=foo", - 200, - {"in": "router-depends", "params": {"q": "foo", "skip": 0, "limit": 100}}, - ), - ( - "/router-depends/?q=foo&skip=100&limit=200", - 200, - {"in": "router-depends", "params": {"q": "foo", "skip": 100, "limit": 200}}, - ), - ( - "/router-decorator-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/router-decorator-depends/?q=foo", 200, {"in": "router-decorator-depends"}), - ( - "/router-decorator-depends/?q=foo&skip=100&limit=200", - 200, - {"in": "router-decorator-depends"}, - ), - ], -) -def test_normal_app(url, status_code, expected): - response = client.get(url) - assert response.status_code == status_code - assert response.json() == expected +def test_main_depends(): + response = client.get("/main-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_main_depends_q_foo(): + response = client.get("/main-depends/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "in": "main-depends", + "params": {"q": "foo", "skip": 0, "limit": 100}, + } + + +def test_main_depends_q_foo_skip_100_limit_200(): + response = client.get("/main-depends/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "in": "main-depends", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } + + +def test_decorator_depends(): + response = client.get("/decorator-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_decorator_depends_q_foo(): + response = client.get("/decorator-depends/?q=foo") + assert response.status_code == 200 + assert response.json() == {"in": "decorator-depends"} + + +def test_decorator_depends_q_foo_skip_100_limit_200(): + response = client.get("/decorator-depends/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == {"in": "decorator-depends"} + + +def test_router_depends(): + response = client.get("/router-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_router_depends_q_foo(): + response = client.get("/router-depends/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "in": "router-depends", + "params": {"q": "foo", "skip": 0, "limit": 100}, + } + + +def test_router_depends_q_foo_skip_100_limit_200(): + response = client.get("/router-depends/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "in": "router-depends", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } + + +def test_router_decorator_depends(): + response = client.get("/router-decorator-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_router_decorator_depends_q_foo(): + response = client.get("/router-decorator-depends/?q=foo") + assert response.status_code == 200 + assert response.json() == {"in": "router-decorator-depends"} + + +def test_router_decorator_depends_q_foo_skip_100_limit_200(): + response = client.get("/router-decorator-depends/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == {"in": "router-decorator-depends"} @pytest.mark.parametrize( @@ -190,126 +273,281 @@ def test_override_simple(url, status_code, expected): app.dependency_overrides = {} -@pytest.mark.parametrize( - "url,status_code,expected", - [ - ( - "/main-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/main-depends/?q=foo", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/main-depends/?k=bar", 200, {"in": "main-depends", "params": {"k": "bar"}}), - ( - "/decorator-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/decorator-depends/?q=foo", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/decorator-depends/?k=bar", 200, {"in": "decorator-depends"}), - ( - "/router-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/router-depends/?q=foo", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/router-depends/?k=bar", - 200, - {"in": "router-depends", "params": {"k": "bar"}}, - ), - ( - "/router-decorator-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/router-decorator-depends/?q=foo", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/router-decorator-depends/?k=bar", 200, {"in": "router-decorator-depends"}), - ], -) -def test_override_with_sub(url, status_code, expected): +def test_override_with_sub_main_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub - response = client.get(url) - assert response.status_code == status_code - assert response.json() == expected + response = client.get("/main-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub__main_depends_q_foo(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/main-depends/?q=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_main_depends_k_bar(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/main-depends/?k=bar") + assert response.status_code == 200 + assert response.json() == {"in": "main-depends", "params": {"k": "bar"}} + app.dependency_overrides = {} + + +def test_override_with_sub_decorator_depends(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/decorator-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_decorator_depends_q_foo(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/decorator-depends/?q=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_decorator_depends_k_bar(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/decorator-depends/?k=bar") + assert response.status_code == 200 + assert response.json() == {"in": "decorator-depends"} + app.dependency_overrides = {} + + +def test_override_with_sub_router_depends(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_router_depends_q_foo(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-depends/?q=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_router_depends_k_bar(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-depends/?k=bar") + assert response.status_code == 200 + assert response.json() == {"in": "router-depends", "params": {"k": "bar"}} + app.dependency_overrides = {} + + +def test_override_with_sub_router_decorator_depends(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-decorator-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_router_decorator_depends_q_foo(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-decorator-depends/?q=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_router_decorator_depends_k_bar(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-decorator-depends/?k=bar") + assert response.status_code == 200 + assert response.json() == {"in": "router-decorator-depends"} app.dependency_overrides = {} diff --git a/tests/test_deprecated_openapi_prefix.py b/tests/test_deprecated_openapi_prefix.py index a3355256f9..ec7366d2af 100644 --- a/tests/test_deprecated_openapi_prefix.py +++ b/tests/test_deprecated_openapi_prefix.py @@ -11,34 +11,32 @@ def read_main(request: Request): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, - "servers": [{"url": "/api/v1"}], -} - - -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"} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + "servers": [{"url": "/api/v1"}], + } diff --git a/tests/test_duplicate_models_openapi.py b/tests/test_duplicate_models_openapi.py index f077dfea08..83e86d231a 100644 --- a/tests/test_duplicate_models_openapi.py +++ b/tests/test_duplicate_models_openapi.py @@ -23,60 +23,57 @@ def f(): return {"c": {}, "d": {"a": {}}} -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "summary": "F", - "operationId": "f__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model3"} - } - }, - } - }, - } - } - }, - "components": { - "schemas": { - "Model": {"title": "Model", "type": "object", "properties": {}}, - "Model2": { - "title": "Model2", - "required": ["a"], - "type": "object", - "properties": {"a": {"$ref": "#/components/schemas/Model"}}, - }, - "Model3": { - "title": "Model3", - "required": ["c", "d"], - "type": "object", - "properties": { - "c": {"$ref": "#/components/schemas/Model"}, - "d": {"$ref": "#/components/schemas/Model2"}, - }, - }, - } - }, -} - - client = TestClient(app) -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_get_api_route(): response = client.get("/") assert response.status_code == 200, response.text assert response.json() == {"c": {}, "d": {"a": {}}} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "F", + "operationId": "f__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model3"} + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "Model": {"title": "Model", "type": "object", "properties": {}}, + "Model2": { + "title": "Model2", + "required": ["a"], + "type": "object", + "properties": {"a": {"$ref": "#/components/schemas/Model"}}, + }, + "Model3": { + "title": "Model3", + "required": ["c", "d"], + "type": "object", + "properties": { + "c": {"$ref": "#/components/schemas/Model"}, + "d": {"$ref": "#/components/schemas/Model2"}, + }, + }, + } + }, + } diff --git a/tests/test_empty_router.py b/tests/test_empty_router.py index 186ceb347e..1a40cbe304 100644 --- a/tests/test_empty_router.py +++ b/tests/test_empty_router.py @@ -1,5 +1,6 @@ import pytest from fastapi import APIRouter, FastAPI +from fastapi.exceptions import FastAPIError from fastapi.testclient import TestClient app = FastAPI() @@ -31,5 +32,5 @@ def test_use_empty(): def test_include_empty(): # if both include and router.path are empty - it should raise exception - with pytest.raises(Exception): + with pytest.raises(FastAPIError): app.include_router(router) diff --git a/tests/test_enforce_once_required_parameter.py b/tests/test_enforce_once_required_parameter.py index bf05aa5852..b64f8341b8 100644 --- a/tests/test_enforce_once_required_parameter.py +++ b/tests/test_enforce_once_required_parameter.py @@ -57,7 +57,7 @@ expected_schema = { } }, "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.0.2", + "openapi": "3.1.0", "paths": { "/foo": { "get": { diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index e979628a5c..bd16fe9254 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -1,5 +1,6 @@ from typing import Optional +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient @@ -52,273 +53,6 @@ def trace_item(item_id: str): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Items", - "operationId": "get_items_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "delete": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Delete Item", - "operationId": "delete_item_items__item_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - "options": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Options Item", - "operationId": "options_item_items__item_id__options", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "head": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Head Item", - "operationId": "head_item_items__item_id__head", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "patch": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Patch Item", - "operationId": "patch_item_items__item_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - "trace": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Trace Item", - "operationId": "trace_item_items__item_id__trace", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - }, - "/items-not-decorated/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Not Decorated", - "operationId": "get_not_decorated_items_not_decorated__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_api_route(): response = client.get("/items/foo") @@ -360,3 +94,277 @@ def test_trace(): response = client.request("trace", "/items/foo") assert response.status_code == 200, response.text assert response.headers["content-type"] == "message/http" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Items", + "operationId": "get_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "delete": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Delete Item", + "operationId": "delete_item_items__item_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + "options": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Options Item", + "operationId": "options_item_items__item_id__options", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "head": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Head Item", + "operationId": "head_item_items__item_id__head", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "patch": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Patch Item", + "operationId": "patch_item_items__item_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + "trace": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Trace Item", + "operationId": "trace_item_items__item_id__trace", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + }, + "/items-not-decorated/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Not Decorated", + "operationId": "get_not_decorated_items_not_decorated__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": IsDict( + { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict({"title": "Price", "type": "number"}), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_filter_pydantic_sub_model.py b/tests/test_filter_pydantic_sub_model.py deleted file mode 100644 index 8814356a10..0000000000 --- a/tests/test_filter_pydantic_sub_model.py +++ /dev/null @@ -1,155 +0,0 @@ -from typing import Optional - -import pytest -from fastapi import Depends, FastAPI -from fastapi.testclient import TestClient -from pydantic import BaseModel, ValidationError, validator - -app = FastAPI() - - -class ModelB(BaseModel): - username: str - - -class ModelC(ModelB): - password: str - - -class ModelA(BaseModel): - name: str - description: Optional[str] = None - model_b: ModelB - - @validator("name") - def lower_username(cls, name: str, values): - if not name.endswith("A"): - raise ValueError("name must end in A") - return name - - -async def get_model_c() -> ModelC: - return ModelC(username="test-user", password="test-password") - - -@app.get("/model/{name}", response_model=ModelA) -async def get_model_a(name: str, model_c=Depends(get_model_c)): - return {"name": name, "description": "model-a-desc", "model_b": model_c} - - -client = TestClient(app) - - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/model/{name}": { - "get": { - "summary": "Get Model A", - "operationId": "get_model_a_model__name__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Name", "type": "string"}, - "name": "name", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ModelA"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ModelA": { - "title": "ModelA", - "required": ["name", "model_b"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "model_b": {"$ref": "#/components/schemas/ModelB"}, - }, - }, - "ModelB": { - "title": "ModelB", - "required": ["username"], - "type": "object", - "properties": {"username": {"title": "Username", "type": "string"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_filter_sub_model(): - response = client.get("/model/modelA") - assert response.status_code == 200, response.text - assert response.json() == { - "name": "modelA", - "description": "model-a-desc", - "model_b": {"username": "test-user"}, - } - - -def test_validator_is_cloned(): - with pytest.raises(ValidationError) as err: - client.get("/model/modelX") - assert err.value.errors() == [ - { - "loc": ("response", "name"), - "msg": "name must end in A", - "type": "value_error", - } - ] diff --git a/docs/pt/overrides/.gitignore b/tests/test_filter_pydantic_sub_model/__init__.py similarity index 100% rename from docs/pt/overrides/.gitignore rename to tests/test_filter_pydantic_sub_model/__init__.py diff --git a/tests/test_filter_pydantic_sub_model/app_pv1.py b/tests/test_filter_pydantic_sub_model/app_pv1.py new file mode 100644 index 0000000000..657e8c5d12 --- /dev/null +++ b/tests/test_filter_pydantic_sub_model/app_pv1.py @@ -0,0 +1,35 @@ +from typing import Optional + +from fastapi import Depends, FastAPI +from pydantic import BaseModel, validator + +app = FastAPI() + + +class ModelB(BaseModel): + username: str + + +class ModelC(ModelB): + password: str + + +class ModelA(BaseModel): + name: str + description: Optional[str] = None + model_b: ModelB + + @validator("name") + def lower_username(cls, name: str, values): + if not name.endswith("A"): + raise ValueError("name must end in A") + return name + + +async def get_model_c() -> ModelC: + return ModelC(username="test-user", password="test-password") + + +@app.get("/model/{name}", response_model=ModelA) +async def get_model_a(name: str, model_c=Depends(get_model_c)): + return {"name": name, "description": "model-a-desc", "model_b": model_c} diff --git a/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py b/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py new file mode 100644 index 0000000000..48732dbf06 --- /dev/null +++ b/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py @@ -0,0 +1,130 @@ +import pytest +from fastapi.exceptions import ResponseValidationError +from fastapi.testclient import TestClient + +from ..utils import needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from .app_pv1 import app + + client = TestClient(app) + return client + + +@needs_pydanticv1 +def test_filter_sub_model(client: TestClient): + response = client.get("/model/modelA") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "modelA", + "description": "model-a-desc", + "model_b": {"username": "test-user"}, + } + + +@needs_pydanticv1 +def test_validator_is_cloned(client: TestClient): + with pytest.raises(ResponseValidationError) as err: + client.get("/model/modelX") + assert err.value.errors() == [ + { + "loc": ("response", "name"), + "msg": "name must end in A", + "type": "value_error", + } + ] + + +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model/{name}": { + "get": { + "summary": "Get Model A", + "operationId": "get_model_a_model__name__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Name", "type": "string"}, + "name": "name", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ModelA"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ModelA": { + "title": "ModelA", + "required": ["name", "model_b"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "model_b": {"$ref": "#/components/schemas/ModelB"}, + }, + }, + "ModelB": { + "title": "ModelB", + "required": ["username"], + "type": "object", + "properties": {"username": {"title": "Username", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py new file mode 100644 index 0000000000..9097d2ce52 --- /dev/null +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -0,0 +1,186 @@ +from typing import Optional + +import pytest +from dirty_equals import HasRepr, IsDict, IsOneOf +from fastapi import Depends, FastAPI +from fastapi.exceptions import ResponseValidationError +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from .utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from pydantic import BaseModel, ValidationInfo, field_validator + + app = FastAPI() + + class ModelB(BaseModel): + username: str + + class ModelC(ModelB): + password: str + + class ModelA(BaseModel): + name: str + description: Optional[str] = None + foo: ModelB + + @field_validator("name") + def lower_username(cls, name: str, info: ValidationInfo): + if not name.endswith("A"): + raise ValueError("name must end in A") + return name + + async def get_model_c() -> ModelC: + return ModelC(username="test-user", password="test-password") + + @app.get("/model/{name}", response_model=ModelA) + async def get_model_a(name: str, model_c=Depends(get_model_c)): + return {"name": name, "description": "model-a-desc", "foo": model_c} + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_filter_sub_model(client: TestClient): + response = client.get("/model/modelA") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "modelA", + "description": "model-a-desc", + "foo": {"username": "test-user"}, + } + + +@needs_pydanticv2 +def test_validator_is_cloned(client: TestClient): + with pytest.raises(ResponseValidationError) as err: + client.get("/model/modelX") + assert err.value.errors() == [ + IsDict( + { + "type": "value_error", + "loc": ("response", "name"), + "msg": "Value error, name must end in A", + "input": "modelX", + "ctx": {"error": HasRepr("ValueError('name must end in A')")}, + "url": match_pydantic_error_url("value_error"), + } + ) + | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "loc": ("response", "name"), + "msg": "name must end in A", + "type": "value_error", + } + ) + ] + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model/{name}": { + "get": { + "summary": "Get Model A", + "operationId": "get_model_a_model__name__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Name", "type": "string"}, + "name": "name", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ModelA"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ModelA": { + "title": "ModelA", + "required": IsOneOf( + ["name", "description", "foo"], + # TODO remove when deprecating Pydantic v1 + ["name", "foo"], + ), + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | + # TODO remove when deprecating Pydantic v1 + IsDict({"title": "Description", "type": "string"}), + "foo": {"$ref": "#/components/schemas/ModelB"}, + }, + }, + "ModelB": { + "title": "ModelB", + "required": ["username"], + "type": "object", + "properties": {"username": {"title": "Username", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_generate_unique_id_function.py b/tests/test_generate_unique_id_function.py index 0b519f8596..5aeec66367 100644 --- a/tests/test_generate_unique_id_function.py +++ b/tests/test_generate_unique_id_function.py @@ -48,7 +48,7 @@ def test_top_level_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -249,7 +249,7 @@ def test_router_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -450,7 +450,7 @@ def test_router_include_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -661,7 +661,7 @@ def test_subrouter_top_level_include_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -928,7 +928,7 @@ def test_router_path_operation_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -1136,7 +1136,7 @@ def test_app_path_operation_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -1353,7 +1353,7 @@ def test_callback_override_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -1626,6 +1626,9 @@ def test_warn_duplicate_operation_id(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") client.get("/openapi.json") - assert len(w) == 2 - assert issubclass(w[-1].category, UserWarning) - assert "Duplicate Operation ID" in str(w[-1].message) + assert len(w) >= 2 + duplicate_warnings = [ + warning for warning in w if issubclass(warning.category, UserWarning) + ] + assert len(duplicate_warnings) > 0 + assert "Duplicate Operation ID" in str(duplicate_warnings[0].message) diff --git a/tests/test_get_request_body.py b/tests/test_get_request_body.py index 52a052faab..cc567b88f6 100644 --- a/tests/test_get_request_body.py +++ b/tests/test_get_request_body.py @@ -19,90 +19,89 @@ async def create_item(product: Product): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/product": { - "get": { - "summary": "Create Item", - "operationId": "create_item_product_get", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Product"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Product": { - "title": "Product", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} +def test_get_with_body(): + body = {"name": "Foo", "description": "Some description", "price": 5.5} + response = client.request("GET", "/product", json=body) + assert response.json() == body def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_get_with_body(): - body = {"name": "Foo", "description": "Some description", "price": 5.5} - response = client.request("GET", "/product", json=body) - assert response.json() == body + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/product": { + "get": { + "summary": "Create Item", + "operationId": "create_item_product_get", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Product"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Product": { + "title": "Product", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_include_router_defaults_overrides.py b/tests/test_include_router_defaults_overrides.py index ccb6c7229d..33baa25e6a 100644 --- a/tests/test_include_router_defaults_overrides.py +++ b/tests/test_include_router_defaults_overrides.py @@ -343,16 +343,6 @@ app.include_router(router2_default) client = TestClient(app) -def test_openapi(): - client = TestClient(app) - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - response = client.get("/openapi.json") - assert issubclass(w[-1].category, UserWarning) - assert "Duplicate Operation ID" in str(w[-1].message) - assert response.json() == openapi_schema - - def test_level1_override(): response = client.get("/override1?level1=foo") assert response.json() == "foo" @@ -445,6179 +435,6863 @@ def test_paths_level5(override1, override2, override3, override4, override5): assert not override5 or "x-level5" in response.headers -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/override1": { - "get": { - "tags": ["path1a", "path1b"], - "summary": "Path1 Override", - "operationId": "path1_override_override1_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level1", "type": "string"}, - "name": "level1", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-1": {"schema": {}}}, +def test_openapi(): + client = TestClient(app) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + response = client.get("/openapi.json") + assert issubclass(w[-1].category, UserWarning) + assert "Duplicate Operation ID" in str(w[-1].message) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/override1": { + "get": { + "tags": ["path1a", "path1b"], + "summary": "Path1 Override", + "operationId": "path1_override_override1_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level1", "type": "string"}, + "name": "level1", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-1": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } } }, }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/default1": { - "get": { - "summary": "Path1 Default", - "operationId": "path1_default_default1_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level1", "type": "string"}, - "name": "level1", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-0": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } - }, - } - }, - "/level1/level2/override3": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "path3a", - "path3b", - ], - "summary": "Path3 Override Router2 Override", - "operationId": "path3_override_router2_override_level1_level2_override3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/default3": { - "get": { - "tags": ["level1a", "level1b", "level2a", "level2b"], - "summary": "Path3 Default Router2 Override", - "operationId": "path3_default_router2_override_level1_level2_default3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-2": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level3/level4/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level3a", - "level3b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level1_level2_level3_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level3/level4/default5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level3a", - "level3b", - "level4a", - "level4b", - ], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level1_level2_level3_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level3/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level3a", - "level3b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level1_level2_level3_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level3/default5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level3a", - "level3b", - ], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level1_level2_level3_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level4/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level1_level2_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level4/default5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level4a", - "level4b", - ], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level1_level2_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level1_level2_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/default5": { - "get": { - "tags": ["level1a", "level1b", "level2a", "level2b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level1_level2_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-2": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/override3": { - "get": { - "tags": ["level1a", "level1b", "path3a", "path3b"], - "summary": "Path3 Override Router2 Default", - "operationId": "path3_override_router2_default_level1_override3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/default3": { - "get": { - "tags": ["level1a", "level1b"], - "summary": "Path3 Default Router2 Default", - "operationId": "path3_default_router2_default_level1_default3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-1": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - }, - "/level1/level3/level4/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level3a", - "level3b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level1_level3_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level3/level4/default5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level3a", - "level3b", - "level4a", - "level4b", - ], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level1_level3_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level3/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level3a", - "level3b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level1_level3_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level3/default5": { - "get": { - "tags": ["level1a", "level1b", "level3a", "level3b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level1_level3_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - }, - "/level1/level4/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level1_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level4/default5": { - "get": { - "tags": ["level1a", "level1b", "level4a", "level4b"], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level1_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/override5": { - "get": { - "tags": ["level1a", "level1b", "path5a", "path5b"], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level1_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/default5": { - "get": { - "tags": ["level1a", "level1b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level1_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-1": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - }, - "/level2/override3": { - "get": { - "tags": ["level2a", "level2b", "path3a", "path3b"], - "summary": "Path3 Override Router2 Override", - "operationId": "path3_override_router2_override_level2_override3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/default3": { - "get": { - "tags": ["level2a", "level2b"], - "summary": "Path3 Default Router2 Override", - "operationId": "path3_default_router2_override_level2_default3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-2": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level3/level4/override5": { - "get": { - "tags": [ - "level2a", - "level2b", - "level3a", - "level3b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level2_level3_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level3/level4/default5": { - "get": { - "tags": [ - "level2a", - "level2b", - "level3a", - "level3b", - "level4a", - "level4b", - ], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level2_level3_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level3/override5": { - "get": { - "tags": [ - "level2a", - "level2b", - "level3a", - "level3b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level2_level3_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level3/default5": { - "get": { - "tags": ["level2a", "level2b", "level3a", "level3b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level2_level3_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level4/override5": { - "get": { - "tags": [ - "level2a", - "level2b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level2_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level4/default5": { - "get": { - "tags": ["level2a", "level2b", "level4a", "level4b"], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level2_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/override5": { - "get": { - "tags": ["level2a", "level2b", "path5a", "path5b"], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level2_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/default5": { - "get": { - "tags": ["level2a", "level2b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level2_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-2": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/override3": { - "get": { - "tags": ["path3a", "path3b"], - "summary": "Path3 Override Router2 Default", - "operationId": "path3_override_router2_default_override3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/default3": { - "get": { - "summary": "Path3 Default Router2 Default", - "operationId": "path3_default_router2_default_default3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-0": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } - }, - } - }, - "/level3/level4/override5": { - "get": { - "tags": [ - "level3a", - "level3b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level3_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level3/level4/default5": { - "get": { - "tags": ["level3a", "level3b", "level4a", "level4b"], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level3_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level3/override5": { - "get": { - "tags": ["level3a", "level3b", "path5a", "path5b"], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level3_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level3/default5": { - "get": { - "tags": ["level3a", "level3b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level3_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - }, - "/level4/override5": { - "get": { - "tags": ["level4a", "level4b", "path5a", "path5b"], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level4/default5": { - "get": { - "tags": ["level4a", "level4b"], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/override5": { - "get": { - "tags": ["path5a", "path5b"], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/default5": { - "get": { - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-0": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, + "deprecated": True, + } }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + "/default1": { + "get": { + "summary": "Path1 Default", + "operationId": "path1_default_default1_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level1", "type": "string"}, + "name": "level1", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-0": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + } + }, + } }, - } - }, -} + "/level1/level2/override3": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "path3a", + "path3b", + ], + "summary": "Path3 Override Router2 Override", + "operationId": "path3_override_router2_override_level1_level2_override3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/default3": { + "get": { + "tags": ["level1a", "level1b", "level2a", "level2b"], + "summary": "Path3 Default Router2 Override", + "operationId": "path3_default_router2_override_level1_level2_default3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-2": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level3/level4/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level3a", + "level3b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level1_level2_level3_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level3/level4/default5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level3a", + "level3b", + "level4a", + "level4b", + ], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level1_level2_level3_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level3/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level3a", + "level3b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level1_level2_level3_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level3/default5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level3a", + "level3b", + ], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level1_level2_level3_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level4/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level1_level2_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level4/default5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level4a", + "level4b", + ], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level1_level2_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level1_level2_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/default5": { + "get": { + "tags": ["level1a", "level1b", "level2a", "level2b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level1_level2_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-2": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/override3": { + "get": { + "tags": ["level1a", "level1b", "path3a", "path3b"], + "summary": "Path3 Override Router2 Default", + "operationId": "path3_override_router2_default_level1_override3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/default3": { + "get": { + "tags": ["level1a", "level1b"], + "summary": "Path3 Default Router2 Default", + "operationId": "path3_default_router2_default_level1_default3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-1": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + }, + "/level1/level3/level4/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level3a", + "level3b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level1_level3_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level3/level4/default5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level3a", + "level3b", + "level4a", + "level4b", + ], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level1_level3_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level3/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level3a", + "level3b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level1_level3_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level3/default5": { + "get": { + "tags": ["level1a", "level1b", "level3a", "level3b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level1_level3_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + }, + "/level1/level4/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level1_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level4/default5": { + "get": { + "tags": ["level1a", "level1b", "level4a", "level4b"], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level1_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/override5": { + "get": { + "tags": ["level1a", "level1b", "path5a", "path5b"], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level1_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/default5": { + "get": { + "tags": ["level1a", "level1b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level1_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-1": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + }, + "/level2/override3": { + "get": { + "tags": ["level2a", "level2b", "path3a", "path3b"], + "summary": "Path3 Override Router2 Override", + "operationId": "path3_override_router2_override_level2_override3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/default3": { + "get": { + "tags": ["level2a", "level2b"], + "summary": "Path3 Default Router2 Override", + "operationId": "path3_default_router2_override_level2_default3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-2": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level3/level4/override5": { + "get": { + "tags": [ + "level2a", + "level2b", + "level3a", + "level3b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level2_level3_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level3/level4/default5": { + "get": { + "tags": [ + "level2a", + "level2b", + "level3a", + "level3b", + "level4a", + "level4b", + ], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level2_level3_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level3/override5": { + "get": { + "tags": [ + "level2a", + "level2b", + "level3a", + "level3b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level2_level3_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level3/default5": { + "get": { + "tags": ["level2a", "level2b", "level3a", "level3b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level2_level3_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level4/override5": { + "get": { + "tags": [ + "level2a", + "level2b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level2_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level4/default5": { + "get": { + "tags": ["level2a", "level2b", "level4a", "level4b"], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level2_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/override5": { + "get": { + "tags": ["level2a", "level2b", "path5a", "path5b"], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level2_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/default5": { + "get": { + "tags": ["level2a", "level2b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level2_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-2": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/override3": { + "get": { + "tags": ["path3a", "path3b"], + "summary": "Path3 Override Router2 Default", + "operationId": "path3_override_router2_default_override3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/default3": { + "get": { + "summary": "Path3 Default Router2 Default", + "operationId": "path3_default_router2_default_default3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-0": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + } + }, + } + }, + "/level3/level4/override5": { + "get": { + "tags": [ + "level3a", + "level3b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level3_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level3/level4/default5": { + "get": { + "tags": ["level3a", "level3b", "level4a", "level4b"], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level3_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level3/override5": { + "get": { + "tags": ["level3a", "level3b", "path5a", "path5b"], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level3_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level3/default5": { + "get": { + "tags": ["level3a", "level3b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level3_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + }, + "/level4/override5": { + "get": { + "tags": ["level4a", "level4b", "path5a", "path5b"], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level4/default5": { + "get": { + "tags": ["level4a", "level4b"], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/override5": { + "get": { + "tags": ["path5a", "path5b"], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/default5": { + "get": { + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-0": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + } + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_infer_param_optionality.py b/tests/test_infer_param_optionality.py index 5e673d9c4d..e3d57bb428 100644 --- a/tests/test_infer_param_optionality.py +++ b/tests/test_infer_param_optionality.py @@ -1,5 +1,6 @@ from typing import Optional +from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient @@ -104,35 +105,253 @@ def test_get_users_item(): assert response.json() == {"item_id": "item01", "user_id": "abc123"} -def test_schema_1(): - """Check that the user_id is a required path parameter under /users""" +def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - r = response.json() - - d = { - "required": True, - "schema": {"title": "User Id", "type": "string"}, - "name": "user_id", - "in": "path", + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "summary": "Get Users", + "operationId": "get_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/users/{user_id}": { + "get": { + "summary": "Get User", + "operationId": "get_user_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "string"}, + "name": "user_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "summary": "Get Items", + "operationId": "get_items_items__get", + "parameters": [ + { + "required": False, + "name": "user_id", + "in": "query", + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User Id", "type": "string"} + ), + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "summary": "Get Item", + "operationId": "get_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "name": "user_id", + "in": "query", + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User Id", "type": "string"} + ), + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{user_id}/items/": { + "get": { + "summary": "Get Items", + "operationId": "get_items_users__user_id__items__get", + "parameters": [ + { + "required": True, + "name": "user_id", + "in": "path", + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User Id", "type": "string"} + ), + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{user_id}/items/{item_id}": { + "get": { + "summary": "Get Item", + "operationId": "get_item_users__user_id__items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "name": "user_id", + "in": "path", + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User Id", "type": "string"} + ), + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, } - - assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"] - assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"] - - -def test_schema_2(): - """Check that the user_id is an optional query parameter under /items""" - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - r = response.json() - - d = { - "required": False, - "schema": {"title": "User Id", "type": "string"}, - "name": "user_id", - "in": "query", - } - - assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"] - assert d in r["paths"]["/items/"]["get"]["parameters"] diff --git a/tests/test_inherited_custom_class.py b/tests/test_inherited_custom_class.py index bac7eec1b0..42b249211d 100644 --- a/tests/test_inherited_custom_class.py +++ b/tests/test_inherited_custom_class.py @@ -5,7 +5,7 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -app = FastAPI() +from .utils import needs_pydanticv1, needs_pydanticv2 class MyUuid: @@ -26,40 +26,78 @@ class MyUuid: raise TypeError("vars() argument must have __dict__ attribute") -@app.get("/fast_uuid") -def return_fast_uuid(): - # I don't want to import asyncpg for this test so I made my own UUID - # Import asyncpg and uncomment the two lines below for the actual bug +@needs_pydanticv2 +def test_pydanticv2(): + from pydantic import field_serializer - # from asyncpg.pgproto import pgproto - # asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51") + app = FastAPI() - asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") - assert isinstance(asyncpg_uuid, uuid.UUID) - assert type(asyncpg_uuid) != uuid.UUID - with pytest.raises(TypeError): - vars(asyncpg_uuid) - return {"fast_uuid": asyncpg_uuid} + @app.get("/fast_uuid") + def return_fast_uuid(): + asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") + assert isinstance(asyncpg_uuid, uuid.UUID) + assert type(asyncpg_uuid) != uuid.UUID + with pytest.raises(TypeError): + vars(asyncpg_uuid) + return {"fast_uuid": asyncpg_uuid} + class SomeCustomClass(BaseModel): + model_config = {"arbitrary_types_allowed": True} -class SomeCustomClass(BaseModel): - class Config: - arbitrary_types_allowed = True - json_encoders = {uuid.UUID: str} + a_uuid: MyUuid - a_uuid: MyUuid + @field_serializer("a_uuid") + def serialize_a_uuid(self, v): + return str(v) + @app.get("/get_custom_class") + def return_some_user(): + # Test that the fix also works for custom pydantic classes + return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) -@app.get("/get_custom_class") -def return_some_user(): - # Test that the fix also works for custom pydantic classes - return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) + client = TestClient(app) + + with client: + response_simple = client.get("/fast_uuid") + response_pydantic = client.get("/get_custom_class") + + assert response_simple.json() == { + "fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51" + } + + assert response_pydantic.json() == { + "a_uuid": "b8799909-f914-42de-91bc-95c819218d01" + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_pydanticv1(): + app = FastAPI() + + @app.get("/fast_uuid") + def return_fast_uuid(): + asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") + assert isinstance(asyncpg_uuid, uuid.UUID) + assert type(asyncpg_uuid) != uuid.UUID + with pytest.raises(TypeError): + vars(asyncpg_uuid) + return {"fast_uuid": asyncpg_uuid} + + class SomeCustomClass(BaseModel): + class Config: + arbitrary_types_allowed = True + json_encoders = {uuid.UUID: str} + + a_uuid: MyUuid + + @app.get("/get_custom_class") + def return_some_user(): + # Test that the fix also works for custom pydantic classes + return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) + + client = TestClient(app) - -client = TestClient(app) - - -def test_dt(): with client: response_simple = client.get("/fast_uuid") response_pydantic = client.get("/get_custom_class") diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index f4fdcf6014..7c8338ff37 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -1,12 +1,17 @@ +from collections import deque from dataclasses import dataclass from datetime import datetime, timezone +from decimal import Decimal from enum import Enum from pathlib import PurePath, PurePosixPath, PureWindowsPath from typing import Optional import pytest +from fastapi._compat import PYDANTIC_V2 from fastapi.encoders import jsonable_encoder -from pydantic import BaseModel, Field, ValidationError, create_model +from pydantic import BaseModel, Field, ValidationError + +from .utils import needs_pydanticv1, needs_pydanticv2 class Person: @@ -45,22 +50,6 @@ class Unserializable: raise NotImplementedError() -class ModelWithCustomEncoder(BaseModel): - dt_field: datetime - - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } - - -class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): - class Config: - pass - - class RoleEnum(Enum): admin = "admin" normal = "normal" @@ -69,8 +58,12 @@ class RoleEnum(Enum): class ModelWithConfig(BaseModel): role: Optional[RoleEnum] = None - class Config: - use_enum_values = True + if PYDANTIC_V2: + model_config = {"use_enum_values": True} + else: + + class Config: + use_enum_values = True class ModelWithAlias(BaseModel): @@ -83,23 +76,6 @@ class ModelWithDefault(BaseModel): bla: str = "bla" -class ModelWithRoot(BaseModel): - __root__: str - - -@pytest.fixture( - name="model_with_path", params=[PurePath, PurePosixPath, PureWindowsPath] -) -def fixture_model_with_path(request): - class Config: - arbitrary_types_allowed = True - - ModelWithPath = create_model( - "ModelWithPath", path=(request.param, ...), __config__=Config # type: ignore - ) - return ModelWithPath(path=request.param("/foo", "bar")) - - def test_encode_dict(): pet = {"name": "Firulais", "owner": {"name": "Foo"}} assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} @@ -153,14 +129,47 @@ def test_encode_unsupported(): jsonable_encoder(unserializable) -def test_encode_custom_json_encoders_model(): +@needs_pydanticv2 +def test_encode_custom_json_encoders_model_pydanticv2(): + from pydantic import field_serializer + + class ModelWithCustomEncoder(BaseModel): + dt_field: datetime + + @field_serializer("dt_field") + def serialize_dt_field(self, dt): + return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat() + + class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): + pass + model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} + subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) + assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} -def test_encode_custom_json_encoders_model_subclass(): - model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_encode_custom_json_encoders_model_pydanticv1(): + class ModelWithCustomEncoder(BaseModel): + dt_field: datetime + + class Config: + json_encoders = { + datetime: lambda dt: dt.replace( + microsecond=0, tzinfo=timezone.utc + ).isoformat() + } + + class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): + class Config: + pass + + model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} + subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) + assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} def test_encode_model_with_config(): @@ -196,6 +205,7 @@ def test_encode_model_with_default(): } +@needs_pydanticv1 def test_custom_encoders(): class safe_datetime(datetime): pass @@ -226,14 +236,77 @@ def test_custom_enum_encoders(): assert encoded_instance == custom_enum_encoder(instance) -def test_encode_model_with_path(model_with_path): - if isinstance(model_with_path.path, PureWindowsPath): - expected = "\\foo\\bar" - else: - expected = "/foo/bar" - assert jsonable_encoder(model_with_path) == {"path": expected} +def test_encode_model_with_pure_path(): + class ModelWithPath(BaseModel): + path: PurePath + + if PYDANTIC_V2: + model_config = {"arbitrary_types_allowed": True} + else: + + class Config: + arbitrary_types_allowed = True + + test_path = PurePath("/foo", "bar") + obj = ModelWithPath(path=test_path) + assert jsonable_encoder(obj) == {"path": str(test_path)} +def test_encode_model_with_pure_posix_path(): + class ModelWithPath(BaseModel): + path: PurePosixPath + + if PYDANTIC_V2: + model_config = {"arbitrary_types_allowed": True} + else: + + class Config: + arbitrary_types_allowed = True + + obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) + assert jsonable_encoder(obj) == {"path": "/foo/bar"} + + +def test_encode_model_with_pure_windows_path(): + class ModelWithPath(BaseModel): + path: PureWindowsPath + + if PYDANTIC_V2: + model_config = {"arbitrary_types_allowed": True} + else: + + class Config: + arbitrary_types_allowed = True + + obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) + assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} + + +@needs_pydanticv1 def test_encode_root(): + class ModelWithRoot(BaseModel): + __root__: str + model = ModelWithRoot(__root__="Foo") assert jsonable_encoder(model) == "Foo" + + +@needs_pydanticv2 +def test_decimal_encoder_float(): + data = {"value": Decimal(1.23)} + assert jsonable_encoder(data) == {"value": 1.23} + + +@needs_pydanticv2 +def test_decimal_encoder_int(): + data = {"value": Decimal(2)} + assert jsonable_encoder(data) == {"value": 2} + + +def test_encode_deque_encodes_child_models(): + class Model(BaseModel): + test: str + + dq = deque([Model(test="test")]) + + assert jsonable_encoder(dq)[0]["test"] == "test" diff --git a/tests/test_modules_same_name_body/test_main.py b/tests/test_modules_same_name_body/test_main.py index 8b1aea0316..cc165bdcaa 100644 --- a/tests/test_modules_same_name_body/test_main.py +++ b/tests/test_modules_same_name_body/test_main.py @@ -4,130 +4,6 @@ from .app.main import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a/compute": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Compute", - "operationId": "compute_a_compute_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_compute_a_compute_post" - } - } - }, - "required": True, - }, - } - }, - "/b/compute/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Compute", - "operationId": "compute_b_compute__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_compute_b_compute__post" - } - } - }, - "required": True, - }, - } - }, - }, - "components": { - "schemas": { - "Body_compute_b_compute__post": { - "title": "Body_compute_b_compute__post", - "required": ["a", "b"], - "type": "object", - "properties": { - "a": {"title": "A", "type": "integer"}, - "b": {"title": "B", "type": "string"}, - }, - }, - "Body_compute_a_compute_post": { - "title": "Body_compute_a_compute_post", - "required": ["a", "b"], - "type": "object", - "properties": { - "a": {"title": "A", "type": "integer"}, - "b": {"title": "B", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_a(): data = {"a": 2, "b": "foo"} @@ -153,3 +29,127 @@ def test_post_b_invalid(): data = {"a": "bar", "b": "foo"} response = client.post("/b/compute/", json=data) assert response.status_code == 422, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a/compute": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Compute", + "operationId": "compute_a_compute_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_compute_a_compute_post" + } + } + }, + "required": True, + }, + } + }, + "/b/compute/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Compute", + "operationId": "compute_b_compute__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_compute_b_compute__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_compute_b_compute__post": { + "title": "Body_compute_b_compute__post", + "required": ["a", "b"], + "type": "object", + "properties": { + "a": {"title": "A", "type": "integer"}, + "b": {"title": "B", "type": "string"}, + }, + }, + "Body_compute_a_compute_post": { + "title": "Body_compute_a_compute_post", + "required": ["a", "b"], + "type": "object", + "properties": { + "a": {"title": "A", "type": "integer"}, + "b": {"title": "B", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 31308ea85c..a51ca7253f 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -1,8 +1,10 @@ from decimal import Decimal from typing import List +from dirty_equals import IsDict, IsOneOf from fastapi import FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel, condecimal app = FastAPI() @@ -21,141 +23,215 @@ def save_item_no_body(item: List[Item]): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Save Item No Body", - "operationId": "save_item_no_body_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Item", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "age"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "age": {"title": "Age", "exclusiveMinimum": 0.0, "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - -single_error = { - "detail": [ - { - "ctx": {"limit_value": 0.0}, - "loc": ["body", 0, "age"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} - -multiple_errors = { - "detail": [ - { - "loc": ["body", 0, "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 0, "age"], - "msg": "value is not a valid decimal", - "type": "type_error.decimal", - }, - { - "loc": ["body", 1, "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 1, "age"], - "msg": "value is not a valid decimal", - "type": "type_error.decimal", - }, - ] -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_put_correct_body(): response = client.post("/items/", json=[{"name": "Foo", "age": 5}]) assert response.status_code == 200, response.text - assert response.json() == {"item": [{"name": "Foo", "age": 5}]} + assert response.json() == { + "item": [ + { + "name": "Foo", + "age": IsOneOf( + 5, + # TODO: remove when deprecating Pydantic v1 + "5", + ), + } + ] + } def test_jsonable_encoder_requiring_error(): response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}]) assert response.status_code == 422, response.text - assert response.json() == single_error + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", 0, "age"], + "msg": "Input should be greater than 0", + "input": -1.0, + "ctx": {"gt": 0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0.0}, + "loc": ["body", 0, "age"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) def test_put_incorrect_body_multiple(): response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}]) assert response.status_code == 422, response.text - assert response.json() == multiple_errors + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", 0, "name"], + "msg": "Field required", + "input": {"age": "five"}, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "decimal_parsing", + "loc": ["body", 0, "age"], + "msg": "Input should be a valid decimal", + "input": "five", + "url": match_pydantic_error_url("decimal_parsing"), + }, + { + "type": "missing", + "loc": ["body", 1, "name"], + "msg": "Field required", + "input": {"age": "six"}, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "decimal_parsing", + "loc": ["body", 1, "age"], + "msg": "Input should be a valid decimal", + "input": "six", + "url": match_pydantic_error_url("decimal_parsing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", 0, "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", 0, "age"], + "msg": "value is not a valid decimal", + "type": "type_error.decimal", + }, + { + "loc": ["body", 1, "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", 1, "age"], + "msg": "value is not a valid decimal", + "type": "type_error.decimal", + }, + ] + } + ) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Save Item No Body", + "operationId": "save_item_no_body_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Item", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "age"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [ + {"exclusiveMinimum": 0.0, "type": "number"}, + {"type": "string"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Age", + "exclusiveMinimum": 0.0, + "type": "number", + } + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index 3da461af5a..470a358083 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -1,7 +1,9 @@ from typing import List +from dirty_equals import IsDict from fastapi import FastAPI, Query from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url app = FastAPI() @@ -14,98 +16,6 @@ def read_items(q: List[int] = Query(default=None)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "integer"}, - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - -multiple_errors = { - "detail": [ - { - "loc": ["query", "q", 0], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "q", 1], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_multi_query(): response = client.get("/items/?q=5&q=6") assert response.status_code == 200, response.text @@ -115,4 +25,115 @@ def test_multi_query(): def test_multi_query_incorrect(): response = client.get("/items/?q=five&q=six") assert response.status_code == 422, response.text - assert response.json() == multiple_errors + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "q", 0], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "five", + "url": match_pydantic_error_url("int_parsing"), + }, + { + "type": "int_parsing", + "loc": ["query", "q", 1], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "six", + "url": match_pydantic_error_url("int_parsing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q", 0], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "q", 1], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + ] + } + ) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "integer"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py new file mode 100644 index 0000000000..6597e5058b --- /dev/null +++ b/tests/test_openapi_examples.py @@ -0,0 +1,458 @@ +from typing import Union + +from dirty_equals import IsDict +from fastapi import Body, Cookie, FastAPI, Header, Path, Query +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + data: str + + +@app.post("/examples/") +def examples( + item: Item = Body( + examples=[ + {"data": "Data in Body examples, example1"}, + ], + openapi_examples={ + "Example One": { + "summary": "Example One Summary", + "description": "Example One Description", + "value": {"data": "Data in Body examples, example1"}, + }, + "Example Two": { + "value": {"data": "Data in Body examples, example2"}, + }, + }, + ), +): + return item + + +@app.get("/path_examples/{item_id}") +def path_examples( + item_id: str = Path( + examples=[ + "json_schema_item_1", + "json_schema_item_2", + ], + openapi_examples={ + "Path One": { + "summary": "Path One Summary", + "description": "Path One Description", + "value": "item_1", + }, + "Path Two": { + "value": "item_2", + }, + }, + ), +): + return item_id + + +@app.get("/query_examples/") +def query_examples( + data: Union[str, None] = Query( + default=None, + examples=[ + "json_schema_query1", + "json_schema_query2", + ], + openapi_examples={ + "Query One": { + "summary": "Query One Summary", + "description": "Query One Description", + "value": "query1", + }, + "Query Two": { + "value": "query2", + }, + }, + ), +): + return data + + +@app.get("/header_examples/") +def header_examples( + data: Union[str, None] = Header( + default=None, + examples=[ + "json_schema_header1", + "json_schema_header2", + ], + openapi_examples={ + "Header One": { + "summary": "Header One Summary", + "description": "Header One Description", + "value": "header1", + }, + "Header Two": { + "value": "header2", + }, + }, + ), +): + return data + + +@app.get("/cookie_examples/") +def cookie_examples( + data: Union[str, None] = Cookie( + default=None, + examples=["json_schema_cookie1", "json_schema_cookie2"], + openapi_examples={ + "Cookie One": { + "summary": "Cookie One Summary", + "description": "Cookie One Description", + "value": "cookie1", + }, + "Cookie Two": { + "value": "cookie2", + }, + }, + ), +): + return data + + +client = TestClient(app) + + +def test_call_api(): + response = client.post("/examples/", json={"data": "example1"}) + assert response.status_code == 200, response.text + + response = client.get("/path_examples/foo") + assert response.status_code == 200, response.text + + response = client.get("/query_examples/") + assert response.status_code == 200, response.text + + response = client.get("/header_examples/") + assert response.status_code == 200, response.text + + response = client.get("/cookie_examples/") + assert response.status_code == 200, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/examples/": { + "post": { + "summary": "Examples", + "operationId": "examples_examples__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + {"data": "Data in Body examples, example1"} + ], + }, + "examples": { + "Example One": { + "summary": "Example One Summary", + "description": "Example One Description", + "value": { + "data": "Data in Body examples, example1" + }, + }, + "Example Two": { + "value": { + "data": "Data in Body examples, example2" + } + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/path_examples/{item_id}": { + "get": { + "summary": "Path Examples", + "operationId": "path_examples_path_examples__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": { + "type": "string", + "examples": [ + "json_schema_item_1", + "json_schema_item_2", + ], + "title": "Item Id", + }, + "examples": { + "Path One": { + "summary": "Path One Summary", + "description": "Path One Description", + "value": "item_1", + }, + "Path Two": {"value": "item_2"}, + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query_examples/": { + "get": { + "summary": "Query Examples", + "operationId": "query_examples_query_examples__get", + "parameters": [ + { + "name": "data", + "in": "query", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_query1", + "json_schema_query2", + ], + "title": "Data", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "examples": [ + "json_schema_query1", + "json_schema_query2", + ], + "type": "string", + "title": "Data", + } + ), + "examples": { + "Query One": { + "summary": "Query One Summary", + "description": "Query One Description", + "value": "query1", + }, + "Query Two": {"value": "query2"}, + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/header_examples/": { + "get": { + "summary": "Header Examples", + "operationId": "header_examples_header_examples__get", + "parameters": [ + { + "name": "data", + "in": "header", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_header1", + "json_schema_header2", + ], + "title": "Data", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "examples": [ + "json_schema_header1", + "json_schema_header2", + ], + "title": "Data", + } + ), + "examples": { + "Header One": { + "summary": "Header One Summary", + "description": "Header One Description", + "value": "header1", + }, + "Header Two": {"value": "header2"}, + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookie_examples/": { + "get": { + "summary": "Cookie Examples", + "operationId": "cookie_examples_cookie_examples__get", + "parameters": [ + { + "name": "data", + "in": "cookie", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_cookie1", + "json_schema_cookie2", + ], + "title": "Data", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "examples": [ + "json_schema_cookie1", + "json_schema_cookie2", + ], + "title": "Data", + } + ), + "examples": { + "Cookie One": { + "summary": "Cookie One Summary", + "description": "Cookie One Description", + "value": "cookie1", + }, + "Cookie Two": {"value": "cookie2"}, + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": {"data": {"type": "string", "title": "Data"}}, + "type": "object", + "required": ["data"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py index d3996f26ee..dc7147c712 100644 --- a/tests/test_openapi_query_parameter_extension.py +++ b/tests/test_openapi_query_parameter_extension.py @@ -1,5 +1,6 @@ from typing import Optional +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient @@ -32,96 +33,105 @@ def route_with_extra_query_parameters(standard_query_param: Optional[int] = 50): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "summary": "Route With Extra Query Parameters", - "operationId": "route_with_extra_query_parameters__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Standard Query Param", - "type": "integer", - "default": 50, - }, - "name": "standard_query_param", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Extra Param 1"}, - "name": "extra_param_1", - "in": "query", - }, - { - "required": True, - "schema": {"title": "Extra Param 2"}, - "name": "extra_param_2", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} +def test_get_route(): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {} def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_get_route(): - response = client.get("/") - assert response.status_code == 200, response.text - assert response.json() == {} + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Route With Extra Query Parameters", + "operationId": "route_with_extra_query_parameters__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "default": 50, + "title": "Standard Query Param", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Standard Query Param", + "type": "integer", + "default": 50, + } + ), + "name": "standard_query_param", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Extra Param 1"}, + "name": "extra_param_1", + "in": "query", + }, + { + "required": True, + "schema": {"title": "Extra Param 2"}, + "name": "extra_param_2", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_openapi_route_extensions.py b/tests/test_openapi_route_extensions.py index 8a1080d697..3a30994367 100644 --- a/tests/test_openapi_route_extensions.py +++ b/tests/test_openapi_route_extensions.py @@ -12,34 +12,31 @@ def route_with_extras(): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "Route With Extras", - "operationId": "route_with_extras__get", - "x-custom-extension": "value", - } - }, - }, -} +def test_get_route(): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {} def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_get_route(): - response = client.get("/") - assert response.status_code == 200, response.text - assert response.json() == {} + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "Route With Extras", + "operationId": "route_with_extras__get", + "x-custom-extension": "value", + } + }, + }, + } diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py new file mode 100644 index 0000000000..aeb85f735b --- /dev/null +++ b/tests/test_openapi_separate_input_output_schemas.py @@ -0,0 +1,494 @@ +from typing import List, Optional + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + +from .utils import PYDANTIC_V2, needs_pydanticv2 + + +class SubItem(BaseModel): + subname: str + sub_description: Optional[str] = None + tags: List[str] = [] + if PYDANTIC_V2: + model_config = {"json_schema_serialization_defaults_required": True} + + +class Item(BaseModel): + name: str + description: Optional[str] = None + sub: Optional[SubItem] = None + if PYDANTIC_V2: + model_config = {"json_schema_serialization_defaults_required": True} + + +def get_app_client(separate_input_output_schemas: bool = True) -> TestClient: + app = FastAPI(separate_input_output_schemas=separate_input_output_schemas) + + @app.post("/items/") + def create_item(item: Item): + return item + + @app.post("/items-list/") + def create_item_list(item: List[Item]): + return item + + @app.get("/items/") + def read_items() -> List[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + sub=SubItem(subname="subname"), + ), + Item(name="Plumbus"), + ] + + client = TestClient(app) + return client + + +def test_create_item(): + client = get_app_client() + client_no = get_app_client(separate_input_output_schemas=False) + response = client.post("/items/", json={"name": "Plumbus"}) + response2 = client_no.post("/items/", json={"name": "Plumbus"}) + assert response.status_code == response2.status_code == 200, response.text + assert ( + response.json() + == response2.json() + == {"name": "Plumbus", "description": None, "sub": None} + ) + + +def test_create_item_with_sub(): + client = get_app_client() + client_no = get_app_client(separate_input_output_schemas=False) + data = { + "name": "Plumbus", + "sub": {"subname": "SubPlumbus", "sub_description": "Sub WTF"}, + } + response = client.post("/items/", json=data) + response2 = client_no.post("/items/", json=data) + assert response.status_code == response2.status_code == 200, response.text + assert ( + response.json() + == response2.json() + == { + "name": "Plumbus", + "description": None, + "sub": {"subname": "SubPlumbus", "sub_description": "Sub WTF", "tags": []}, + } + ) + + +def test_create_item_list(): + client = get_app_client() + client_no = get_app_client(separate_input_output_schemas=False) + data = [ + {"name": "Plumbus"}, + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + ] + response = client.post("/items-list/", json=data) + response2 = client_no.post("/items-list/", json=data) + assert response.status_code == response2.status_code == 200, response.text + assert ( + response.json() + == response2.json() + == [ + {"name": "Plumbus", "description": None, "sub": None}, + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + "sub": None, + }, + ] + ) + + +def test_read_items(): + client = get_app_client() + client_no = get_app_client(separate_input_output_schemas=False) + response = client.get("/items/") + response2 = client_no.get("/items/") + assert response.status_code == response2.status_code == 200, response.text + assert ( + response.json() + == response2.json() + == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + "sub": {"subname": "subname", "sub_description": None, "tags": []}, + }, + {"name": "Plumbus", "description": None, "sub": None}, + ] + ) + + +@needs_pydanticv2 +def test_openapi_schema(): + client = get_app_client() + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item-Output" + }, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item-Input"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/items-list/": { + "post": { + "summary": "Create Item List", + "operationId": "create_item_list_items_list__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item-Input" + }, + "type": "array", + "title": "Item", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item-Input": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "sub": { + "anyOf": [ + {"$ref": "#/components/schemas/SubItem-Input"}, + {"type": "null"}, + ] + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "Item-Output": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "sub": { + "anyOf": [ + {"$ref": "#/components/schemas/SubItem-Output"}, + {"type": "null"}, + ] + }, + }, + "type": "object", + "required": ["name", "description", "sub"], + "title": "Item", + }, + "SubItem-Input": { + "properties": { + "subname": {"type": "string", "title": "Subname"}, + "sub_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Sub Description", + }, + "tags": { + "items": {"type": "string"}, + "type": "array", + "title": "Tags", + "default": [], + }, + }, + "type": "object", + "required": ["subname"], + "title": "SubItem", + }, + "SubItem-Output": { + "properties": { + "subname": {"type": "string", "title": "Subname"}, + "sub_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Sub Description", + }, + "tags": { + "items": {"type": "string"}, + "type": "array", + "title": "Tags", + "default": [], + }, + }, + "type": "object", + "required": ["subname", "sub_description", "tags"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + + +@needs_pydanticv2 +def test_openapi_schema_no_separate(): + client = get_app_client(separate_input_output_schemas=False) + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/items-list/": { + "post": { + "summary": "Create Item List", + "operationId": "create_item_list_items_list__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Item", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "sub": { + "anyOf": [ + {"$ref": "#/components/schemas/SubItem"}, + {"type": "null"}, + ] + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "SubItem": { + "properties": { + "subname": {"type": "string", "title": "Subname"}, + "sub_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Sub Description", + }, + "tags": { + "items": {"type": "string"}, + "type": "array", + "title": "Tags", + "default": [], + }, + }, + "type": "object", + "required": ["subname"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_openapi_servers.py b/tests/test_openapi_servers.py index a210154f60..8697c8438b 100644 --- a/tests/test_openapi_servers.py +++ b/tests/test_openapi_servers.py @@ -1,3 +1,4 @@ +from dirty_equals import IsOneOf from fastapi import FastAPI from fastapi.testclient import TestClient @@ -21,40 +22,47 @@ def foo(): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "servers": [ - {"url": "/", "description": "Default, relative server"}, - { - "url": "http://staging.localhost.tiangolo.com:8000", - "description": "Staging but actually localhost still", - }, - {"url": "https://prod.example.com"}, - ], - "paths": { - "/foo": { - "get": { - "summary": "Foo", - "operationId": "foo_foo_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} - - -def test_openapi_servers(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_app(): response = client.get("/foo") assert response.status_code == 200, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + {"url": "/", "description": "Default, relative server"}, + { + "url": IsOneOf( + "http://staging.localhost.tiangolo.com:8000/", + # TODO: remove when deprecating Pydantic v1 + "http://staging.localhost.tiangolo.com:8000", + ), + "description": "Staging but actually localhost still", + }, + { + "url": IsOneOf( + "https://prod.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://prod.example.com", + ) + }, + ], + "paths": { + "/foo": { + "get": { + "summary": "Foo", + "operationId": "foo_foo_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_param_in_path_and_dependency.py b/tests/test_param_in_path_and_dependency.py index 4d85afbcee..08eb0f40f3 100644 --- a/tests/test_param_in_path_and_dependency.py +++ b/tests/test_param_in_path_and_dependency.py @@ -15,79 +15,79 @@ async def read_users(user_id: int): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/{user_id}": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users__user_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_reused_param(): - response = client.get("/openapi.json") - data = response.json() - assert data == openapi_schema - def test_read_users(): response = client.get("/users/42") assert response.status_code == 200, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + data = response.json() + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/{user_id}": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_param_include_in_schema.py b/tests/test_param_include_in_schema.py index cb182a1cd4..26201e9e2d 100644 --- a/tests/test_param_include_in_schema.py +++ b/tests/test_param_include_in_schema.py @@ -33,8 +33,8 @@ async def hidden_query( return {"hidden_query": hidden_query} -openapi_shema = { - "openapi": "3.0.2", +openapi_schema = { + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/hidden_cookie": { @@ -162,7 +162,7 @@ def test_openapi_schema(): client = TestClient(app) response = client.get("/openapi.json") assert response.status_code == 200 - assert response.json() == openapi_shema + assert response.json() == openapi_schema @pytest.mark.parametrize( diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py index d721257d76..bfc7bed096 100644 --- a/tests/test_params_repr.py +++ b/tests/test_params_repr.py @@ -1,6 +1,6 @@ from typing import Any, List -import pytest +from dirty_equals import IsOneOf from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query test_data: List[Any] = ["teststr", None, ..., 1, []] @@ -10,33 +10,137 @@ def get_user(): return {} # pragma: no cover -@pytest.fixture(scope="function", params=test_data) -def params(request): - return request.param +def test_param_repr_str(): + assert repr(Param("teststr")) == "Param(teststr)" -def test_param_repr(params): - assert repr(Param(params)) == "Param(" + str(params) + ")" +def test_param_repr_none(): + assert repr(Param(None)) == "Param(None)" -def test_path_repr(params): - assert repr(Path(params)) == "Path(Ellipsis)" +def test_param_repr_ellipsis(): + assert repr(Param(...)) == IsOneOf( + "Param(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Param(Ellipsis)", + ) -def test_query_repr(params): - assert repr(Query(params)) == "Query(" + str(params) + ")" +def test_param_repr_number(): + assert repr(Param(1)) == "Param(1)" -def test_header_repr(params): - assert repr(Header(params)) == "Header(" + str(params) + ")" +def test_param_repr_list(): + assert repr(Param([])) == "Param([])" -def test_cookie_repr(params): - assert repr(Cookie(params)) == "Cookie(" + str(params) + ")" +def test_path_repr(): + assert repr(Path()) == IsOneOf( + "Path(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Path(Ellipsis)", + ) + assert repr(Path(...)) == IsOneOf( + "Path(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Path(Ellipsis)", + ) -def test_body_repr(params): - assert repr(Body(params)) == "Body(" + str(params) + ")" +def test_query_repr_str(): + assert repr(Query("teststr")) == "Query(teststr)" + + +def test_query_repr_none(): + assert repr(Query(None)) == "Query(None)" + + +def test_query_repr_ellipsis(): + assert repr(Query(...)) == IsOneOf( + "Query(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Query(Ellipsis)", + ) + + +def test_query_repr_number(): + assert repr(Query(1)) == "Query(1)" + + +def test_query_repr_list(): + assert repr(Query([])) == "Query([])" + + +def test_header_repr_str(): + assert repr(Header("teststr")) == "Header(teststr)" + + +def test_header_repr_none(): + assert repr(Header(None)) == "Header(None)" + + +def test_header_repr_ellipsis(): + assert repr(Header(...)) == IsOneOf( + "Header(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Header(Ellipsis)", + ) + + +def test_header_repr_number(): + assert repr(Header(1)) == "Header(1)" + + +def test_header_repr_list(): + assert repr(Header([])) == "Header([])" + + +def test_cookie_repr_str(): + assert repr(Cookie("teststr")) == "Cookie(teststr)" + + +def test_cookie_repr_none(): + assert repr(Cookie(None)) == "Cookie(None)" + + +def test_cookie_repr_ellipsis(): + assert repr(Cookie(...)) == IsOneOf( + "Cookie(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Cookie(Ellipsis)", + ) + + +def test_cookie_repr_number(): + assert repr(Cookie(1)) == "Cookie(1)" + + +def test_cookie_repr_list(): + assert repr(Cookie([])) == "Cookie([])" + + +def test_body_repr_str(): + assert repr(Body("teststr")) == "Body(teststr)" + + +def test_body_repr_none(): + assert repr(Body(None)) == "Body(None)" + + +def test_body_repr_ellipsis(): + assert repr(Body(...)) == IsOneOf( + "Body(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Body(Ellipsis)", + ) + + +def test_body_repr_number(): + assert repr(Body(1)) == "Body(1)" + + +def test_body_repr_list(): + assert repr(Body([])) == "Body([])" def test_depends_repr(): diff --git a/tests/test_path.py b/tests/test_path.py index d1a58bc661..848b245e2c 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -1,5 +1,6 @@ -import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from .main import app @@ -18,236 +19,1259 @@ def test_nonexistent(): assert response.json() == {"detail": "Not Found"} -response_not_valid_bool = { - "detail": [ +def test_path_foobar(): + response = client.get("/path/foobar") + assert response.status_code == 200 + assert response.json() == "foobar" + + +def test_path_str_foobar(): + response = client.get("/path/str/foobar") + assert response.status_code == 200 + assert response.json() == "foobar" + + +def test_path_str_42(): + response = client.get("/path/str/42") + assert response.status_code == 200 + assert response.json() == "42" + + +def test_path_str_True(): + response = client.get("/path/str/True") + assert response.status_code == 200 + assert response.json() == "True" + + +def test_path_int_foobar(): + response = client.get("/path/int/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "value could not be parsed to a boolean", - "type": "type_error.bool", + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foobar", + "url": match_pydantic_error_url("int_parsing"), + } + ] } - ] -} - -response_not_valid_int = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] } - ] -} + ) -response_not_valid_float = { - "detail": [ + +def test_path_int_True(): + response = client.get("/path/int/True") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "value is not a valid float", - "type": "type_error.float", + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "True", + "url": match_pydantic_error_url("int_parsing"), + } + ] } - ] -} - -response_at_least_3 = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value has at least 3 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 3}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] } - ] -} + ) -response_at_least_2 = { - "detail": [ +def test_path_int_42(): + response = client.get("/path/int/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_int_42_5(): + response = client.get("/path/int/42.5") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "ensure this value has at least 2 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 2}, + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "42.5", + "url": match_pydantic_error_url("int_parsing"), + } + ] } - ] -} - - -response_maximum_3 = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value has at most 3 characters", - "type": "value_error.any_str.max_length", - "ctx": {"limit_value": 3}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] } - ] -} + ) -response_greater_than_3 = { - "detail": [ +def test_path_float_foobar(): + response = client.get("/path/float/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 3", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 3}, + "detail": [ + { + "type": "float_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "foobar", + "url": match_pydantic_error_url("float_parsing"), + } + ] } - ] -} - - -response_greater_than_0 = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] } - ] -} + ) -response_greater_than_1 = { - "detail": [ +def test_path_float_True(): + response = client.get("/path/float/True") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 1", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 1}, + "detail": [ + { + "type": "float_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "True", + "url": match_pydantic_error_url("float_parsing"), + } + ] } - ] -} - - -response_greater_than_equal_3 = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than or equal to 3", - "type": "value_error.number.not_ge", - "ctx": {"limit_value": 3}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] } - ] -} + ) -response_less_than_3 = { - "detail": [ +def test_path_float_42(): + response = client.get("/path/float/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_float_42_5(): + response = client.get("/path/float/42.5") + assert response.status_code == 200 + assert response.json() == 42.5 + + +def test_path_bool_foobar(): + response = client.get("/path/bool/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "foobar", + "url": match_pydantic_error_url("bool_parsing"), + } + ] } - ] -} - - -response_less_than_0 = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 0", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 0}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value could not be parsed to a boolean", + "type": "type_error.bool", + } + ] } - ] -} + ) -response_less_than_equal_3 = { - "detail": [ +def test_path_bool_True(): + response = client.get("/path/bool/True") + assert response.status_code == 200 + assert response.json() is True + + +def test_path_bool_42(): + response = client.get("/path/bool/42") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "42", + "url": match_pydantic_error_url("bool_parsing"), + } + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value could not be parsed to a boolean", + "type": "type_error.bool", + } + ] + } + ) -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/path/foobar", 200, "foobar"), - ("/path/str/foobar", 200, "foobar"), - ("/path/str/42", 200, "42"), - ("/path/str/True", 200, "True"), - ("/path/int/foobar", 422, response_not_valid_int), - ("/path/int/True", 422, response_not_valid_int), - ("/path/int/42", 200, 42), - ("/path/int/42.5", 422, response_not_valid_int), - ("/path/float/foobar", 422, response_not_valid_float), - ("/path/float/True", 422, response_not_valid_float), - ("/path/float/42", 200, 42), - ("/path/float/42.5", 200, 42.5), - ("/path/bool/foobar", 422, response_not_valid_bool), - ("/path/bool/True", 200, True), - ("/path/bool/42", 422, response_not_valid_bool), - ("/path/bool/42.5", 422, response_not_valid_bool), - ("/path/bool/1", 200, True), - ("/path/bool/0", 200, False), - ("/path/bool/true", 200, True), - ("/path/bool/False", 200, False), - ("/path/bool/false", 200, False), - ("/path/param/foo", 200, "foo"), - ("/path/param-required/foo", 200, "foo"), - ("/path/param-minlength/foo", 200, "foo"), - ("/path/param-minlength/fo", 422, response_at_least_3), - ("/path/param-maxlength/foo", 200, "foo"), - ("/path/param-maxlength/foobar", 422, response_maximum_3), - ("/path/param-min_maxlength/foo", 200, "foo"), - ("/path/param-min_maxlength/foobar", 422, response_maximum_3), - ("/path/param-min_maxlength/f", 422, response_at_least_2), - ("/path/param-gt/42", 200, 42), - ("/path/param-gt/2", 422, response_greater_than_3), - ("/path/param-gt0/0.05", 200, 0.05), - ("/path/param-gt0/0", 422, response_greater_than_0), - ("/path/param-ge/42", 200, 42), - ("/path/param-ge/3", 200, 3), - ("/path/param-ge/2", 422, response_greater_than_equal_3), - ("/path/param-lt/42", 422, response_less_than_3), - ("/path/param-lt/2", 200, 2), - ("/path/param-lt0/-1", 200, -1), - ("/path/param-lt0/0", 422, response_less_than_0), - ("/path/param-le/42", 422, response_less_than_equal_3), - ("/path/param-le/3", 200, 3), - ("/path/param-le/2", 200, 2), - ("/path/param-lt-gt/2", 200, 2), - ("/path/param-lt-gt/4", 422, response_less_than_3), - ("/path/param-lt-gt/0", 422, response_greater_than_1), - ("/path/param-le-ge/2", 200, 2), - ("/path/param-le-ge/1", 200, 1), - ("/path/param-le-ge/3", 200, 3), - ("/path/param-le-ge/4", 422, response_less_than_equal_3), - ("/path/param-lt-int/2", 200, 2), - ("/path/param-lt-int/42", 422, response_less_than_3), - ("/path/param-lt-int/2.7", 422, response_not_valid_int), - ("/path/param-gt-int/42", 200, 42), - ("/path/param-gt-int/2", 422, response_greater_than_3), - ("/path/param-gt-int/2.7", 422, response_not_valid_int), - ("/path/param-le-int/42", 422, response_less_than_equal_3), - ("/path/param-le-int/3", 200, 3), - ("/path/param-le-int/2", 200, 2), - ("/path/param-le-int/2.7", 422, response_not_valid_int), - ("/path/param-ge-int/42", 200, 42), - ("/path/param-ge-int/3", 200, 3), - ("/path/param-ge-int/2", 422, response_greater_than_equal_3), - ("/path/param-ge-int/2.7", 422, response_not_valid_int), - ("/path/param-lt-gt-int/2", 200, 2), - ("/path/param-lt-gt-int/4", 422, response_less_than_3), - ("/path/param-lt-gt-int/0", 422, response_greater_than_1), - ("/path/param-lt-gt-int/2.7", 422, response_not_valid_int), - ("/path/param-le-ge-int/2", 200, 2), - ("/path/param-le-ge-int/1", 200, 1), - ("/path/param-le-ge-int/3", 200, 3), - ("/path/param-le-ge-int/4", 422, response_less_than_equal_3), - ("/path/param-le-ge-int/2.7", 422, response_not_valid_int), - ], -) -def test_get_path(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_path_bool_42_5(): + response = client.get("/path/bool/42.5") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "42.5", + "url": match_pydantic_error_url("bool_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value could not be parsed to a boolean", + "type": "type_error.bool", + } + ] + } + ) + + +def test_path_bool_1(): + response = client.get("/path/bool/1") + assert response.status_code == 200 + assert response.json() is True + + +def test_path_bool_0(): + response = client.get("/path/bool/0") + assert response.status_code == 200 + assert response.json() is False + + +def test_path_bool_true(): + response = client.get("/path/bool/true") + assert response.status_code == 200 + assert response.json() is True + + +def test_path_bool_False(): + response = client.get("/path/bool/False") + assert response.status_code == 200 + assert response.json() is False + + +def test_path_bool_false(): + response = client.get("/path/bool/false") + assert response.status_code == 200 + assert response.json() is False + + +def test_path_param_foo(): + response = client.get("/path/param/foo") + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_path_param_minlength_foo(): + response = client.get("/path/param-minlength/foo") + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_path_param_minlength_fo(): + response = client.get("/path/param-minlength/fo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_too_short", + "loc": ["path", "item_id"], + "msg": "String should have at least 3 characters", + "input": "fo", + "ctx": {"min_length": 3}, + "url": match_pydantic_error_url("string_too_short"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value has at least 3 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_maxlength_foo(): + response = client.get("/path/param-maxlength/foo") + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_path_param_maxlength_foobar(): + response = client.get("/path/param-maxlength/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_too_long", + "loc": ["path", "item_id"], + "msg": "String should have at most 3 characters", + "input": "foobar", + "ctx": {"max_length": 3}, + "url": match_pydantic_error_url("string_too_long"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value has at most 3 characters", + "type": "value_error.any_str.max_length", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_min_maxlength_foo(): + response = client.get("/path/param-min_maxlength/foo") + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_path_param_min_maxlength_foobar(): + response = client.get("/path/param-min_maxlength/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_too_long", + "loc": ["path", "item_id"], + "msg": "String should have at most 3 characters", + "input": "foobar", + "ctx": {"max_length": 3}, + "url": match_pydantic_error_url("string_too_long"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value has at most 3 characters", + "type": "value_error.any_str.max_length", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_min_maxlength_f(): + response = client.get("/path/param-min_maxlength/f") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_too_short", + "loc": ["path", "item_id"], + "msg": "String should have at least 2 characters", + "input": "f", + "ctx": {"min_length": 2}, + "url": match_pydantic_error_url("string_too_short"), + } + ] + } + ) | IsDict( + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value has at least 2 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 2}, + } + ] + } + ) + + +def test_path_param_gt_42(): + response = client.get("/path/param-gt/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_param_gt_2(): + response = client.get("/path/param-gt/2") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 3", + "input": "2", + "ctx": {"gt": 3.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 3", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_gt0_0_05(): + response = client.get("/path/param-gt0/0.05") + assert response.status_code == 200 + assert response.json() == 0.05 + + +def test_path_param_gt0_0(): + response = client.get("/path/param-gt0/0") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 0", + "input": "0", + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 0}, + } + ] + } + ) + + +def test_path_param_ge_42(): + response = client.get("/path/param-ge/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_param_ge_3(): + response = client.get("/path/param-ge/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_ge_2(): + response = client.get("/path/param-ge/2") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be greater than or equal to 3", + "input": "2", + "ctx": {"ge": 3.0}, + "url": match_pydantic_error_url("greater_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than or equal to 3", + "type": "value_error.number.not_ge", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_42(): + response = client.get("/path/param-lt/42") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "42", + "ctx": {"lt": 3.0}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 3", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_2(): + response = client.get("/path/param-lt/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt0__1(): + response = client.get("/path/param-lt0/-1") + assert response.status_code == 200 + assert response.json() == -1 + + +def test_path_param_lt0_0(): + response = client.get("/path/param-lt0/0") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 0", + "input": "0", + "ctx": {"lt": 0.0}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 0", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 0}, + } + ] + } + ) + + +def test_path_param_le_42(): + response = client.get("/path/param-le/42") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "42", + "ctx": {"le": 3.0}, + "url": match_pydantic_error_url("less_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than or equal to 3", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_le_3(): + response = client.get("/path/param-le/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_le_2(): + response = client.get("/path/param-le/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt_gt_2(): + response = client.get("/path/param-lt-gt/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt_gt_4(): + response = client.get("/path/param-lt-gt/4") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "4", + "ctx": {"lt": 3.0}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 3", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_gt_0(): + response = client.get("/path/param-lt-gt/0") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 1", + "input": "0", + "ctx": {"gt": 1.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 1", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 1}, + } + ] + } + ) + + +def test_path_param_le_ge_2(): + response = client.get("/path/param-le-ge/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_le_ge_1(): + response = client.get("/path/param-le-ge/1") + assert response.status_code == 200 + + +def test_path_param_le_ge_3(): + response = client.get("/path/param-le-ge/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_le_ge_4(): + response = client.get("/path/param-le-ge/4") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "4", + "ctx": {"le": 3.0}, + "url": match_pydantic_error_url("less_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than or equal to 3", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_int_2(): + response = client.get("/path/param-lt-int/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt_int_42(): + response = client.get("/path/param-lt-int/42") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "42", + "ctx": {"lt": 3}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 3", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_int_2_7(): + response = client.get("/path/param-lt-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_gt_int_42(): + response = client.get("/path/param-gt-int/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_param_gt_int_2(): + response = client.get("/path/param-gt-int/2") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 3", + "input": "2", + "ctx": {"gt": 3}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 3", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_gt_int_2_7(): + response = client.get("/path/param-gt-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_le_int_42(): + response = client.get("/path/param-le-int/42") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "42", + "ctx": {"le": 3}, + "url": match_pydantic_error_url("less_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than or equal to 3", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_le_int_3(): + response = client.get("/path/param-le-int/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_le_int_2(): + response = client.get("/path/param-le-int/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_le_int_2_7(): + response = client.get("/path/param-le-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_ge_int_42(): + response = client.get("/path/param-ge-int/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_param_ge_int_3(): + response = client.get("/path/param-ge-int/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_ge_int_2(): + response = client.get("/path/param-ge-int/2") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be greater than or equal to 3", + "input": "2", + "ctx": {"ge": 3}, + "url": match_pydantic_error_url("greater_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than or equal to 3", + "type": "value_error.number.not_ge", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_ge_int_2_7(): + response = client.get("/path/param-ge-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_lt_gt_int_2(): + response = client.get("/path/param-lt-gt-int/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt_gt_int_4(): + response = client.get("/path/param-lt-gt-int/4") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "4", + "ctx": {"lt": 3}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 3", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_gt_int_0(): + response = client.get("/path/param-lt-gt-int/0") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 1", + "input": "0", + "ctx": {"gt": 1}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 1", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 1}, + } + ] + } + ) + + +def test_path_param_lt_gt_int_2_7(): + response = client.get("/path/param-lt-gt-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_le_ge_int_2(): + response = client.get("/path/param-le-ge-int/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_le_ge_int_1(): + response = client.get("/path/param-le-ge-int/1") + assert response.status_code == 200 + assert response.json() == 1 + + +def test_path_param_le_ge_int_3(): + response = client.get("/path/param-le-ge-int/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_le_ge_int_4(): + response = client.get("/path/param-le-ge-int/4") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "4", + "ctx": {"le": 3}, + "url": match_pydantic_error_url("less_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than or equal to 3", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_le_ge_int_2_7(): + response = client.get("/path/param-le-ge-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) diff --git a/tests/test_put_no_body.py b/tests/test_put_no_body.py index 3da294ccf7..8f4c82532c 100644 --- a/tests/test_put_no_body.py +++ b/tests/test_put_no_body.py @@ -12,79 +12,6 @@ def save_item_no_body(item_id: str): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Save Item No Body", - "operationId": "save_item_no_body_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_put_no_body(): response = client.put("/items/foo") assert response.status_code == 200, response.text @@ -95,3 +22,75 @@ def test_put_no_body_with_body(): response = client.put("/items/foo", json={"name": "Foo"}) assert response.status_code == 200, response.text assert response.json() == {"item_id": "foo"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Save Item No Body", + "operationId": "save_item_no_body_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_query.py b/tests/test_query.py index 0c73eb665e..5bb9995d6c 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,62 +1,410 @@ -import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from .main import app client = TestClient(app) -response_missing = { - "detail": [ + +def test_query(): + response = client.get("/query") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} - -response_not_valid_int = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] } - ] -} + ) -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/query", 422, response_missing), - ("/query?query=baz", 200, "foo bar baz"), - ("/query?not_declared=baz", 422, response_missing), - ("/query/optional", 200, "foo bar"), - ("/query/optional?query=baz", 200, "foo bar baz"), - ("/query/optional?not_declared=baz", 200, "foo bar"), - ("/query/int", 422, response_missing), - ("/query/int?query=42", 200, "foo bar 42"), - ("/query/int?query=42.5", 422, response_not_valid_int), - ("/query/int?query=baz", 422, response_not_valid_int), - ("/query/int?not_declared=baz", 422, response_missing), - ("/query/int/optional", 200, "foo bar"), - ("/query/int/optional?query=50", 200, "foo bar 50"), - ("/query/int/optional?query=foo", 422, response_not_valid_int), - ("/query/int/default", 200, "foo bar 10"), - ("/query/int/default?query=50", 200, "foo bar 50"), - ("/query/int/default?query=foo", 422, response_not_valid_int), - ("/query/param", 200, "foo bar"), - ("/query/param?query=50", 200, "foo bar 50"), - ("/query/param-required", 422, response_missing), - ("/query/param-required?query=50", 200, "foo bar 50"), - ("/query/param-required/int", 422, response_missing), - ("/query/param-required/int?query=50", 200, "foo bar 50"), - ("/query/param-required/int?query=foo", 422, response_not_valid_int), - ("/query/frozenset/?query=1&query=1&query=2", 200, "1,2"), - ], -) -def test_get_path(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_query_query_baz(): + response = client.get("/query?query=baz") + assert response.status_code == 200 + assert response.json() == "foo bar baz" + + +def test_query_not_declared_baz(): + response = client.get("/query?not_declared=baz") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_optional(): + response = client.get("/query/optional") + assert response.status_code == 200 + assert response.json() == "foo bar" + + +def test_query_optional_query_baz(): + response = client.get("/query/optional?query=baz") + assert response.status_code == 200 + assert response.json() == "foo bar baz" + + +def test_query_optional_not_declared_baz(): + response = client.get("/query/optional?not_declared=baz") + assert response.status_code == 200 + assert response.json() == "foo bar" + + +def test_query_int(): + response = client.get("/query/int") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_int_query_42(): + response = client.get("/query/int?query=42") + assert response.status_code == 200 + assert response.json() == "foo bar 42" + + +def test_query_int_query_42_5(): + response = client.get("/query/int?query=42.5") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "42.5", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_int_query_baz(): + response = client.get("/query/int?query=baz") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "baz", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_int_not_declared_baz(): + response = client.get("/query/int?not_declared=baz") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_int_optional(): + response = client.get("/query/int/optional") + assert response.status_code == 200 + assert response.json() == "foo bar" + + +def test_query_int_optional_query_50(): + response = client.get("/query/int/optional?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_int_optional_query_foo(): + response = client.get("/query/int/optional?query=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_int_default(): + response = client.get("/query/int/default") + assert response.status_code == 200 + assert response.json() == "foo bar 10" + + +def test_query_int_default_query_50(): + response = client.get("/query/int/default?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_int_default_query_foo(): + response = client.get("/query/int/default?query=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_param(): + response = client.get("/query/param") + assert response.status_code == 200 + assert response.json() == "foo bar" + + +def test_query_param_query_50(): + response = client.get("/query/param?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_param_required(): + response = client.get("/query/param-required") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_param_required_query_50(): + response = client.get("/query/param-required?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_param_required_int(): + response = client.get("/query/param-required/int") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_param_required_int_query_50(): + response = client.get("/query/param-required/int?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_param_required_int_query_foo(): + response = client.get("/query/param-required/int?query=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_frozenset_query_1_query_1_query_2(): + response = client.get("/query/frozenset/?query=1&query=1&query=2") + assert response.status_code == 200 + assert response.json() == "1,2" diff --git a/tests/test_read_with_orm_mode.py b/tests/test_read_with_orm_mode.py index 360ad25035..b359874439 100644 --- a/tests/test_read_with_orm_mode.py +++ b/tests/test_read_with_orm_mode.py @@ -2,48 +2,83 @@ from typing import Any from fastapi import FastAPI from fastapi.testclient import TestClient -from pydantic import BaseModel - - -class PersonBase(BaseModel): - name: str - lastname: str - - -class Person(PersonBase): - @property - def full_name(self) -> str: - return f"{self.name} {self.lastname}" - - class Config: - orm_mode = True - read_with_orm_mode = True - - -class PersonCreate(PersonBase): - pass - - -class PersonRead(PersonBase): - full_name: str - - class Config: - orm_mode = True - - -app = FastAPI() - - -@app.post("/people/", response_model=PersonRead) -def create_person(person: PersonCreate) -> Any: - db_person = Person.from_orm(person) - return db_person - - -client = TestClient(app) +from pydantic import BaseModel, ConfigDict + +from .utils import needs_pydanticv1, needs_pydanticv2 +@needs_pydanticv2 def test_read_with_orm_mode() -> None: + class PersonBase(BaseModel): + name: str + lastname: str + + class Person(PersonBase): + @property + def full_name(self) -> str: + return f"{self.name} {self.lastname}" + + model_config = ConfigDict(from_attributes=True) + + class PersonCreate(PersonBase): + pass + + class PersonRead(PersonBase): + full_name: str + + model_config = {"from_attributes": True} + + app = FastAPI() + + @app.post("/people/", response_model=PersonRead) + def create_person(person: PersonCreate) -> Any: + db_person = Person.model_validate(person) + return db_person + + client = TestClient(app) + + person_data = {"name": "Dive", "lastname": "Wilson"} + response = client.post("/people/", json=person_data) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == person_data["name"] + assert data["lastname"] == person_data["lastname"] + assert data["full_name"] == person_data["name"] + " " + person_data["lastname"] + + +@needs_pydanticv1 +def test_read_with_orm_mode_pv1() -> None: + class PersonBase(BaseModel): + name: str + lastname: str + + class Person(PersonBase): + @property + def full_name(self) -> str: + return f"{self.name} {self.lastname}" + + class Config: + orm_mode = True + read_with_orm_mode = True + + class PersonCreate(PersonBase): + pass + + class PersonRead(PersonBase): + full_name: str + + class Config: + orm_mode = True + + app = FastAPI() + + @app.post("/people/", response_model=PersonRead) + def create_person(person: PersonCreate) -> Any: + db_person = Person.from_orm(person) + return db_person + + client = TestClient(app) + person_data = {"name": "Dive", "lastname": "Wilson"} response = client.post("/people/", json=person_data) data = response.json() diff --git a/tests/test_regex_deprecated_body.py b/tests/test_regex_deprecated_body.py new file mode 100644 index 0000000000..ca1ab514c8 --- /dev/null +++ b/tests/test_regex_deprecated_body.py @@ -0,0 +1,182 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url +from typing_extensions import Annotated + +from .utils import needs_py310 + + +def get_client(): + app = FastAPI() + with pytest.warns(DeprecationWarning): + + @app.post("/items/") + async def read_items( + q: Annotated[str | None, Form(regex="^fixedquery$")] = None + ): + if q: + return f"Hello {q}" + else: + return "Hello World" + + client = TestClient(app) + return client + + +@needs_py310 +def test_no_query(): + client = get_client() + response = client.post("/items/") + assert response.status_code == 200 + assert response.json() == "Hello World" + + +@needs_py310 +def test_q_fixedquery(): + client = get_client() + response = client.post("/items/", data={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == "Hello fixedquery" + + +@needs_py310 +def test_query_nonregexquery(): + client = get_client() + response = client.post("/items/", data={"q": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["body", "q"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) + + +@needs_py310 +def test_openapi_schema(): + client = get_client() + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Read Items", + "operationId": "read_items_items__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__post": { + "properties": { + "q": IsDict( + { + "anyOf": [ + {"type": "string", "pattern": "^fixedquery$"}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"type": "string", "pattern": "^fixedquery$", "title": "Q"} + ) + }, + "type": "object", + "title": "Body_read_items_items__post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_regex_deprecated_params.py b/tests/test_regex_deprecated_params.py new file mode 100644 index 0000000000..79a6533533 --- /dev/null +++ b/tests/test_regex_deprecated_params.py @@ -0,0 +1,165 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url +from typing_extensions import Annotated + +from .utils import needs_py310 + + +def get_client(): + app = FastAPI() + with pytest.warns(DeprecationWarning): + + @app.get("/items/") + async def read_items( + q: Annotated[str | None, Query(regex="^fixedquery$")] = None + ): + if q: + return f"Hello {q}" + else: + return "Hello World" + + client = TestClient(app) + return client + + +@needs_py310 +def test_query_params_str_validations_no_query(): + client = get_client() + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == "Hello World" + + +@needs_py310 +def test_query_params_str_validations_q_fixedquery(): + client = get_client() + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == "Hello fixedquery" + + +@needs_py310 +def test_query_params_str_validations_item_query_nonregexquery(): + client = get_client() + response = client.get("/items/", params={"q": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "q"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) + + +@needs_py310 +def test_openapi_schema(): + client = get_client() + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "q", + "in": "query", + "required": False, + "schema": IsDict( + { + "anyOf": [ + {"type": "string", "pattern": "^fixedquery$"}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "pattern": "^fixedquery$", + "title": "Q", + } + ), + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_repeated_dependency_schema.py b/tests/test_repeated_dependency_schema.py index ca0305184a..d7d0dfa052 100644 --- a/tests/test_repeated_dependency_schema.py +++ b/tests/test_repeated_dependency_schema.py @@ -50,7 +50,7 @@ schema = { } }, "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.0.2", + "openapi": "3.1.0", "paths": { "/": { "get": { diff --git a/tests/test_repeated_parameter_alias.py b/tests/test_repeated_parameter_alias.py index 823f53a95a..fd72eaab29 100644 --- a/tests/test_repeated_parameter_alias.py +++ b/tests/test_repeated_parameter_alias.py @@ -14,87 +14,87 @@ def get_parameters_with_repeated_aliases( client = TestClient(app) -openapi_schema = { - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "title": "Detail", - "type": "array", - } - }, - "title": "HTTPValidationError", - "type": "object", - }, - "ValidationError": { - "properties": { - "loc": { - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - "title": "Location", - "type": "array", - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - "required": ["loc", "msg", "type"], - "title": "ValidationError", - "type": "object", - }, - } - }, - "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.0.2", - "paths": { - "/{repeated_alias}": { - "get": { - "operationId": "get_parameters_with_repeated_aliases__repeated_alias__get", - "parameters": [ - { - "in": "path", - "name": "repeated_alias", - "required": True, - "schema": {"title": "Repeated Alias", "type": "string"}, - }, - { - "in": "query", - "name": "repeated_alias", - "required": True, - "schema": {"title": "Repeated Alias", "type": "string"}, - }, - ], - "responses": { - "200": { - "content": {"application/json": {"schema": {}}}, - "description": "Successful Response", - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error", - }, - }, - "summary": "Get Parameters With Repeated Aliases", - } - } - }, -} + +def test_get_parameters(): + response = client.get("/test_path", params={"repeated_alias": "test_query"}) + assert response.status_code == 200, response.text + assert response.json() == {"path": "test_path", "query": "test_query"} def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == status.HTTP_200_OK actual_schema = response.json() - assert actual_schema == openapi_schema - - -def test_get_parameters(): - response = client.get("/test_path", params={"repeated_alias": "test_query"}) - assert response.status_code == 200, response.text - assert response.json() == {"path": "test_path", "query": "test_query"} + assert actual_schema == { + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "title": "Detail", + "type": "array", + } + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "title": "Location", + "type": "array", + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + "required": ["loc", "msg", "type"], + "title": "ValidationError", + "type": "object", + }, + } + }, + "info": {"title": "FastAPI", "version": "0.1.0"}, + "openapi": "3.1.0", + "paths": { + "/{repeated_alias}": { + "get": { + "operationId": "get_parameters_with_repeated_aliases__repeated_alias__get", + "parameters": [ + { + "in": "path", + "name": "repeated_alias", + "required": True, + "schema": {"title": "Repeated Alias", "type": "string"}, + }, + { + "in": "query", + "name": "repeated_alias", + "required": True, + "schema": {"title": "Repeated Alias", "type": "string"}, + }, + ], + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error", + }, + }, + "summary": "Get Parameters With Repeated Aliases", + } + } + }, + } diff --git a/tests/test_reponse_set_reponse_code_empty.py b/tests/test_reponse_set_reponse_code_empty.py index 50ec753a00..bf3aa758c3 100644 --- a/tests/test_reponse_set_reponse_code_empty.py +++ b/tests/test_reponse_set_reponse_code_empty.py @@ -22,77 +22,76 @@ async def delete_deployment( client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/{id}": { - "delete": { - "summary": "Delete Deployment", - "operationId": "delete_deployment__id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Id", "type": "integer"}, - "name": "id", - "in": "path", - } - ], - "responses": { - "204": {"description": "Successful Response"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} +def test_dependency_set_status_code(): + response = client.delete("/1") + assert response.status_code == 400 and response.content + assert response.json() == {"msg": "Status overwritten", "id": 1} def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_dependency_set_status_code(): - response = client.delete("/1") - assert response.status_code == 400 and response.content - assert response.json() == {"msg": "Status overwritten", "id": 1} + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/{id}": { + "delete": { + "summary": "Delete Deployment", + "operationId": "delete_deployment__id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Id", "type": "integer"}, + "name": "id", + "in": "path", + } + ], + "responses": { + "204": {"description": "Successful Response"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_request_body_parameters_media_type.py b/tests/test_request_body_parameters_media_type.py index e9cf4006d9..8c72fee54a 100644 --- a/tests/test_request_body_parameters_media_type.py +++ b/tests/test_request_body_parameters_media_type.py @@ -33,36 +33,145 @@ async def create_shop( pass # pragma: no cover -create_product_request_body = { - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/Body_create_product_products_post"} - } - }, - "required": True, -} - -create_shop_request_body = { - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/Body_create_shop_shops_post"} - } - }, - "required": True, -} - client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - openapi_schema = response.json() - assert ( - openapi_schema["paths"]["/products"]["post"]["requestBody"] - == create_product_request_body - ) - assert ( - openapi_schema["paths"]["/shops"]["post"]["requestBody"] - == create_shop_request_body - ) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/products": { + "post": { + "summary": "Create Product", + "operationId": "create_product_products_post", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/Body_create_product_products_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/shops": { + "post": { + "summary": "Create Shop", + "operationId": "create_shop_shops_post", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/Body_create_shop_shops_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_product_products_post": { + "title": "Body_create_product_products_post", + "required": ["data"], + "type": "object", + "properties": {"data": {"$ref": "#/components/schemas/Product"}}, + }, + "Body_create_shop_shops_post": { + "title": "Body_create_shop_shops_post", + "required": ["data"], + "type": "object", + "properties": { + "data": {"$ref": "#/components/schemas/Shop"}, + "included": { + "title": "Included", + "type": "array", + "items": {"$ref": "#/components/schemas/Product"}, + "default": [], + }, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Product": { + "title": "Product", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "Shop": { + "title": "Shop", + "required": ["name"], + "type": "object", + "properties": {"name": {"title": "Name", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_response_by_alias.py b/tests/test_response_by_alias.py index de45e0880f..e162cd39b5 100644 --- a/tests/test_response_by_alias.py +++ b/tests/test_response_by_alias.py @@ -1,8 +1,9 @@ from typing import List from fastapi import FastAPI +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field app = FastAPI() @@ -14,13 +15,24 @@ class Model(BaseModel): class ModelNoAlias(BaseModel): name: str - class Config: - schema_extra = { - "description": ( - "response_model_by_alias=False is basically a quick hack, to support " - "proper OpenAPI use another model with the correct field names" - ) - } + if PYDANTIC_V2: + model_config = ConfigDict( + json_schema_extra={ + "description": ( + "response_model_by_alias=False is basically a quick hack, to support " + "proper OpenAPI use another model with the correct field names" + ) + } + ) + else: + + class Config: + schema_extra = { + "description": ( + "response_model_by_alias=False is basically a quick hack, to support " + "proper OpenAPI use another model with the correct field names" + ) + } @app.get("/dict", response_model=Model, response_model_by_alias=False) @@ -68,198 +80,9 @@ def no_alias_list(): return [{"name": "Foo"}, {"name": "Bar"}] -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/dict": { - "get": { - "summary": "Read Dict", - "operationId": "read_dict_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - } - }, - } - }, - "/model": { - "get": { - "summary": "Read Model", - "operationId": "read_model_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - } - }, - } - }, - "/list": { - "get": { - "summary": "Read List", - "operationId": "read_list_list_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read List List Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Model"}, - } - } - }, - } - }, - } - }, - "/by-alias/dict": { - "get": { - "summary": "By Alias Dict", - "operationId": "by_alias_dict_by_alias_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - } - }, - } - }, - "/by-alias/model": { - "get": { - "summary": "By Alias Model", - "operationId": "by_alias_model_by_alias_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - } - }, - } - }, - "/by-alias/list": { - "get": { - "summary": "By Alias List", - "operationId": "by_alias_list_by_alias_list_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response By Alias List By Alias List Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Model"}, - } - } - }, - } - }, - } - }, - "/no-alias/dict": { - "get": { - "summary": "No Alias Dict", - "operationId": "no_alias_dict_no_alias_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ModelNoAlias"} - } - }, - } - }, - } - }, - "/no-alias/model": { - "get": { - "summary": "No Alias Model", - "operationId": "no_alias_model_no_alias_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ModelNoAlias"} - } - }, - } - }, - } - }, - "/no-alias/list": { - "get": { - "summary": "No Alias List", - "operationId": "no_alias_list_no_alias_list_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Alias List No Alias List Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/ModelNoAlias" - }, - } - } - }, - } - }, - } - }, - }, - "components": { - "schemas": { - "Model": { - "title": "Model", - "required": ["alias"], - "type": "object", - "properties": {"alias": {"title": "Alias", "type": "string"}}, - }, - "ModelNoAlias": { - "title": "ModelNoAlias", - "required": ["name"], - "type": "object", - "properties": {"name": {"title": "Name", "type": "string"}}, - "description": "response_model_by_alias=False is basically a quick hack, to support proper OpenAPI use another model with the correct field names", - }, - } - }, -} - - client = TestClient(app) -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_read_dict(): response = client.get("/dict") assert response.status_code == 200, response.text @@ -321,3 +144,193 @@ def test_read_list_no_alias(): {"name": "Foo"}, {"name": "Bar"}, ] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/dict": { + "get": { + "summary": "Read Dict", + "operationId": "read_dict_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/model": { + "get": { + "summary": "Read Model", + "operationId": "read_model_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/list": { + "get": { + "summary": "Read List", + "operationId": "read_list_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read List List Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Model"}, + } + } + }, + } + }, + } + }, + "/by-alias/dict": { + "get": { + "summary": "By Alias Dict", + "operationId": "by_alias_dict_by_alias_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/by-alias/model": { + "get": { + "summary": "By Alias Model", + "operationId": "by_alias_model_by_alias_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/by-alias/list": { + "get": { + "summary": "By Alias List", + "operationId": "by_alias_list_by_alias_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response By Alias List By Alias List Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Model"}, + } + } + }, + } + }, + } + }, + "/no-alias/dict": { + "get": { + "summary": "No Alias Dict", + "operationId": "no_alias_dict_no_alias_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelNoAlias" + } + } + }, + } + }, + } + }, + "/no-alias/model": { + "get": { + "summary": "No Alias Model", + "operationId": "no_alias_model_no_alias_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelNoAlias" + } + } + }, + } + }, + } + }, + "/no-alias/list": { + "get": { + "summary": "No Alias List", + "operationId": "no_alias_list_no_alias_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Alias List No Alias List Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelNoAlias" + }, + } + } + }, + } + }, + } + }, + }, + "components": { + "schemas": { + "Model": { + "title": "Model", + "required": ["alias"], + "type": "object", + "properties": {"alias": {"title": "Alias", "type": "string"}}, + }, + "ModelNoAlias": { + "title": "ModelNoAlias", + "required": ["name"], + "type": "object", + "properties": {"name": {"title": "Name", "type": "string"}}, + "description": "response_model_by_alias=False is basically a quick hack, to support proper OpenAPI use another model with the correct field names", + }, + } + }, + } diff --git a/tests/test_response_class_no_mediatype.py b/tests/test_response_class_no_mediatype.py index eb8500f3a8..706929ac36 100644 --- a/tests/test_response_class_no_mediatype.py +++ b/tests/test_response_class_no_mediatype.py @@ -35,80 +35,79 @@ async def b(): pass # pragma: no cover -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/JsonApiError"} - } - }, - }, - "200": {"description": "Successful Response"}, - }, - "summary": "A", - "operationId": "a_a_get", - } - }, - "/b": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Error"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "B", - "operationId": "b_b_get", - } - }, - }, - "components": { - "schemas": { - "Error": { - "title": "Error", - "required": ["status", "title"], - "type": "object", - "properties": { - "status": {"title": "Status", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - }, - }, - "JsonApiError": { - "title": "JsonApiError", - "required": ["errors"], - "type": "object", - "properties": { - "errors": { - "title": "Errors", - "type": "array", - "items": {"$ref": "#/components/schemas/Error"}, - } - }, - }, - } - }, -} - - client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JsonApiError" + } + } + }, + }, + "200": {"description": "Successful Response"}, + }, + "summary": "A", + "operationId": "a_a_get", + } + }, + "/b": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Error"} + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "B", + "operationId": "b_b_get", + } + }, + }, + "components": { + "schemas": { + "Error": { + "title": "Error", + "required": ["status", "title"], + "type": "object", + "properties": { + "status": {"title": "Status", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + }, + }, + "JsonApiError": { + "title": "JsonApiError", + "required": ["errors"], + "type": "object", + "properties": { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "#/components/schemas/Error"}, + } + }, + }, + } + }, + } diff --git a/tests/test_response_code_no_body.py b/tests/test_response_code_no_body.py index 6d9b5c3334..3ca8708f12 100644 --- a/tests/test_response_code_no_body.py +++ b/tests/test_response_code_no_body.py @@ -36,80 +36,79 @@ async def b(): pass # pragma: no cover -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/JsonApiError"} - } - }, - }, - "204": {"description": "Successful Response"}, - }, - "summary": "A", - "operationId": "a_a_get", - } - }, - "/b": { - "get": { - "responses": { - "204": {"description": "No Content"}, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "B", - "operationId": "b_b_get", - } - }, - }, - "components": { - "schemas": { - "Error": { - "title": "Error", - "required": ["status", "title"], - "type": "object", - "properties": { - "status": {"title": "Status", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - }, - }, - "JsonApiError": { - "title": "JsonApiError", - "required": ["errors"], - "type": "object", - "properties": { - "errors": { - "title": "Errors", - "type": "array", - "items": {"$ref": "#/components/schemas/Error"}, - } - }, - }, - } - }, -} - - client = TestClient(app) -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_get_response(): response = client.get("/a") assert response.status_code == 204, response.text assert "content-length" not in response.headers assert response.content == b"" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/JsonApiError" + } + } + }, + }, + "204": {"description": "Successful Response"}, + }, + "summary": "A", + "operationId": "a_a_get", + } + }, + "/b": { + "get": { + "responses": { + "204": {"description": "No Content"}, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "B", + "operationId": "b_b_get", + } + }, + }, + "components": { + "schemas": { + "Error": { + "title": "Error", + "required": ["status", "title"], + "type": "object", + "properties": { + "status": {"title": "Status", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + }, + }, + "JsonApiError": { + "title": "JsonApiError", + "required": ["errors"], + "type": "object", + "properties": { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "#/components/schemas/Error"}, + } + }, + }, + } + }, + } diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index f2056fecdd..6948430a13 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -2,8 +2,10 @@ from typing import List, Union import pytest from fastapi import FastAPI +from fastapi.exceptions import FastAPIError, ResponseValidationError +from fastapi.responses import JSONResponse, Response from fastapi.testclient import TestClient -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel class BaseUser(BaseModel): @@ -237,593 +239,19 @@ def no_response_model_annotation_union_return_model2() -> Union[User, Item]: return Item(name="Foo", price=42.0) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/no_response_model-no_annotation-return_model": { - "get": { - "summary": "No Response Model No Annotation Return Model", - "operationId": "no_response_model_no_annotation_return_model_no_response_model_no_annotation_return_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/no_response_model-no_annotation-return_dict": { - "get": { - "summary": "No Response Model No Annotation Return Dict", - "operationId": "no_response_model_no_annotation_return_dict_no_response_model_no_annotation_return_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model-no_annotation-return_same_model": { - "get": { - "summary": "Response Model No Annotation Return Same Model", - "operationId": "response_model_no_annotation_return_same_model_response_model_no_annotation_return_same_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_exact_dict": { - "get": { - "summary": "Response Model No Annotation Return Exact Dict", - "operationId": "response_model_no_annotation_return_exact_dict_response_model_no_annotation_return_exact_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_invalid_dict": { - "get": { - "summary": "Response Model No Annotation Return Invalid Dict", - "operationId": "response_model_no_annotation_return_invalid_dict_response_model_no_annotation_return_invalid_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_invalid_model": { - "get": { - "summary": "Response Model No Annotation Return Invalid Model", - "operationId": "response_model_no_annotation_return_invalid_model_response_model_no_annotation_return_invalid_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_dict_with_extra_data": { - "get": { - "summary": "Response Model No Annotation Return Dict With Extra Data", - "operationId": "response_model_no_annotation_return_dict_with_extra_data_response_model_no_annotation_return_dict_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_submodel_with_extra_data": { - "get": { - "summary": "Response Model No Annotation Return Submodel With Extra Data", - "operationId": "response_model_no_annotation_return_submodel_with_extra_data_response_model_no_annotation_return_submodel_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_same_model": { - "get": { - "summary": "No Response Model Annotation Return Same Model", - "operationId": "no_response_model_annotation_return_same_model_no_response_model_annotation_return_same_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_exact_dict": { - "get": { - "summary": "No Response Model Annotation Return Exact Dict", - "operationId": "no_response_model_annotation_return_exact_dict_no_response_model_annotation_return_exact_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_invalid_dict": { - "get": { - "summary": "No Response Model Annotation Return Invalid Dict", - "operationId": "no_response_model_annotation_return_invalid_dict_no_response_model_annotation_return_invalid_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_invalid_model": { - "get": { - "summary": "No Response Model Annotation Return Invalid Model", - "operationId": "no_response_model_annotation_return_invalid_model_no_response_model_annotation_return_invalid_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_dict_with_extra_data": { - "get": { - "summary": "No Response Model Annotation Return Dict With Extra Data", - "operationId": "no_response_model_annotation_return_dict_with_extra_data_no_response_model_annotation_return_dict_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_submodel_with_extra_data": { - "get": { - "summary": "No Response Model Annotation Return Submodel With Extra Data", - "operationId": "no_response_model_annotation_return_submodel_with_extra_data_no_response_model_annotation_return_submodel_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_none-annotation-return_same_model": { - "get": { - "summary": "Response Model None Annotation Return Same Model", - "operationId": "response_model_none_annotation_return_same_model_response_model_none_annotation_return_same_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_exact_dict": { - "get": { - "summary": "Response Model None Annotation Return Exact Dict", - "operationId": "response_model_none_annotation_return_exact_dict_response_model_none_annotation_return_exact_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_invalid_dict": { - "get": { - "summary": "Response Model None Annotation Return Invalid Dict", - "operationId": "response_model_none_annotation_return_invalid_dict_response_model_none_annotation_return_invalid_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_invalid_model": { - "get": { - "summary": "Response Model None Annotation Return Invalid Model", - "operationId": "response_model_none_annotation_return_invalid_model_response_model_none_annotation_return_invalid_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_dict_with_extra_data": { - "get": { - "summary": "Response Model None Annotation Return Dict With Extra Data", - "operationId": "response_model_none_annotation_return_dict_with_extra_data_response_model_none_annotation_return_dict_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_submodel_with_extra_data": { - "get": { - "summary": "Response Model None Annotation Return Submodel With Extra Data", - "operationId": "response_model_none_annotation_return_submodel_with_extra_data_response_model_none_annotation_return_submodel_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_same_model": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Same Model", - "operationId": "response_model_model1_annotation_model2_return_same_model_response_model_model1_annotation_model2_return_same_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_exact_dict": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Exact Dict", - "operationId": "response_model_model1_annotation_model2_return_exact_dict_response_model_model1_annotation_model2_return_exact_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_invalid_dict": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Invalid Dict", - "operationId": "response_model_model1_annotation_model2_return_invalid_dict_response_model_model1_annotation_model2_return_invalid_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_invalid_model": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Invalid Model", - "operationId": "response_model_model1_annotation_model2_return_invalid_model_response_model_model1_annotation_model2_return_invalid_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_dict_with_extra_data": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Dict With Extra Data", - "operationId": "response_model_model1_annotation_model2_return_dict_with_extra_data_response_model_model1_annotation_model2_return_dict_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_submodel_with_extra_data": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Submodel With Extra Data", - "operationId": "response_model_model1_annotation_model2_return_submodel_with_extra_data_response_model_model1_annotation_model2_return_submodel_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_filtering_model-annotation_submodel-return_submodel": { - "get": { - "summary": "Response Model Filtering Model Annotation Submodel Return Submodel", - "operationId": "response_model_filtering_model_annotation_submodel_return_submodel_response_model_filtering_model_annotation_submodel_return_submodel_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_list_of_model-no_annotation": { - "get": { - "summary": "Response Model List Of Model No Annotation", - "operationId": "response_model_list_of_model_no_annotation_response_model_list_of_model_no_annotation_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Response Model List Of Model No Annotation Response Model List Of Model No Annotation Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_list_of_model": { - "get": { - "summary": "No Response Model Annotation List Of Model", - "operationId": "no_response_model_annotation_list_of_model_no_response_model_annotation_list_of_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Response Model Annotation List Of Model No Response Model Annotation List Of Model Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_forward_ref_list_of_model": { - "get": { - "summary": "No Response Model Annotation Forward Ref List Of Model", - "operationId": "no_response_model_annotation_forward_ref_list_of_model_no_response_model_annotation_forward_ref_list_of_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Response Model Annotation Forward Ref List Of Model No Response Model Annotation Forward Ref List Of Model Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - } - }, - } - }, - "/response_model_union-no_annotation-return_model1": { - "get": { - "summary": "Response Model Union No Annotation Return Model1", - "operationId": "response_model_union_no_annotation_return_model1_response_model_union_no_annotation_return_model1_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Response Model Union No Annotation Return Model1 Response Model Union No Annotation Return Model1 Get", - "anyOf": [ - {"$ref": "#/components/schemas/User"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - } - }, - } - }, - "/response_model_union-no_annotation-return_model2": { - "get": { - "summary": "Response Model Union No Annotation Return Model2", - "operationId": "response_model_union_no_annotation_return_model2_response_model_union_no_annotation_return_model2_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Response Model Union No Annotation Return Model2 Response Model Union No Annotation Return Model2 Get", - "anyOf": [ - {"$ref": "#/components/schemas/User"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_union-return_model1": { - "get": { - "summary": "No Response Model Annotation Union Return Model1", - "operationId": "no_response_model_annotation_union_return_model1_no_response_model_annotation_union_return_model1_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Response Model Annotation Union Return Model1 No Response Model Annotation Union Return Model1 Get", - "anyOf": [ - {"$ref": "#/components/schemas/User"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_union-return_model2": { - "get": { - "summary": "No Response Model Annotation Union Return Model2", - "operationId": "no_response_model_annotation_union_return_model2_no_response_model_annotation_union_return_model2_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Response Model Annotation Union Return Model2 No Response Model Annotation Union Return Model2 Get", - "anyOf": [ - {"$ref": "#/components/schemas/User"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - } - }, - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "User": { - "title": "User", - "required": ["name", "surname"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "surname": {"title": "Surname", "type": "string"}, - }, - }, - } - }, -} +@app.get("/no_response_model-annotation_response_class") +def no_response_model_annotation_response_class() -> Response: + return Response(content="Foo") + + +@app.get("/no_response_model-annotation_json_response_class") +def no_response_model_annotation_json_response_class() -> JSONResponse: + return JSONResponse(content={"foo": "bar"}) client = TestClient(app) -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_no_response_model_no_annotation_return_model(): response = client.get("/no_response_model-no_annotation-return_model") assert response.status_code == 200, response.text @@ -849,13 +277,15 @@ def test_response_model_no_annotation_return_exact_dict(): def test_response_model_no_annotation_return_invalid_dict(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError) as excinfo: client.get("/response_model-no_annotation-return_invalid_dict") + assert "missing" in str(excinfo.value) def test_response_model_no_annotation_return_invalid_model(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError) as excinfo: client.get("/response_model-no_annotation-return_invalid_model") + assert "missing" in str(excinfo.value) def test_response_model_no_annotation_return_dict_with_extra_data(): @@ -885,13 +315,15 @@ def test_no_response_model_annotation_return_exact_dict(): def test_no_response_model_annotation_return_invalid_dict(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError) as excinfo: client.get("/no_response_model-annotation-return_invalid_dict") + assert "missing" in str(excinfo.value) def test_no_response_model_annotation_return_invalid_model(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError) as excinfo: client.get("/no_response_model-annotation-return_invalid_model") + assert "missing" in str(excinfo.value) def test_no_response_model_annotation_return_dict_with_extra_data(): @@ -967,13 +399,15 @@ def test_response_model_model1_annotation_model2_return_exact_dict(): def test_response_model_model1_annotation_model2_return_invalid_dict(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError) as excinfo: client.get("/response_model_model1-annotation_model2-return_invalid_dict") + assert "missing" in str(excinfo.value) def test_response_model_model1_annotation_model2_return_invalid_model(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError) as excinfo: client.get("/response_model_model1-annotation_model2-return_invalid_model") + assert "missing" in str(excinfo.value) def test_response_model_model1_annotation_model2_return_dict_with_extra_data(): @@ -1049,3 +483,632 @@ def test_no_response_model_annotation_union_return_model2(): response = client.get("/no_response_model-annotation_union-return_model2") assert response.status_code == 200, response.text assert response.json() == {"name": "Foo", "price": 42.0} + + +def test_no_response_model_annotation_return_class(): + response = client.get("/no_response_model-annotation_response_class") + assert response.status_code == 200, response.text + assert response.text == "Foo" + + +def test_no_response_model_annotation_json_response_class(): + response = client.get("/no_response_model-annotation_json_response_class") + assert response.status_code == 200, response.text + assert response.json() == {"foo": "bar"} + + +def test_invalid_response_model_field(): + app = FastAPI() + with pytest.raises(FastAPIError) as e: + + @app.get("/") + def read_root() -> Union[Response, None]: + return Response(content="Foo") # pragma: no cover + + assert "valid Pydantic field type" in e.value.args[0] + assert "parameter response_model=None" in e.value.args[0] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/no_response_model-no_annotation-return_model": { + "get": { + "summary": "No Response Model No Annotation Return Model", + "operationId": "no_response_model_no_annotation_return_model_no_response_model_no_annotation_return_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/no_response_model-no_annotation-return_dict": { + "get": { + "summary": "No Response Model No Annotation Return Dict", + "operationId": "no_response_model_no_annotation_return_dict_no_response_model_no_annotation_return_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model-no_annotation-return_same_model": { + "get": { + "summary": "Response Model No Annotation Return Same Model", + "operationId": "response_model_no_annotation_return_same_model_response_model_no_annotation_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_exact_dict": { + "get": { + "summary": "Response Model No Annotation Return Exact Dict", + "operationId": "response_model_no_annotation_return_exact_dict_response_model_no_annotation_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_invalid_dict": { + "get": { + "summary": "Response Model No Annotation Return Invalid Dict", + "operationId": "response_model_no_annotation_return_invalid_dict_response_model_no_annotation_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_invalid_model": { + "get": { + "summary": "Response Model No Annotation Return Invalid Model", + "operationId": "response_model_no_annotation_return_invalid_model_response_model_no_annotation_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_dict_with_extra_data": { + "get": { + "summary": "Response Model No Annotation Return Dict With Extra Data", + "operationId": "response_model_no_annotation_return_dict_with_extra_data_response_model_no_annotation_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_submodel_with_extra_data": { + "get": { + "summary": "Response Model No Annotation Return Submodel With Extra Data", + "operationId": "response_model_no_annotation_return_submodel_with_extra_data_response_model_no_annotation_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_same_model": { + "get": { + "summary": "No Response Model Annotation Return Same Model", + "operationId": "no_response_model_annotation_return_same_model_no_response_model_annotation_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_exact_dict": { + "get": { + "summary": "No Response Model Annotation Return Exact Dict", + "operationId": "no_response_model_annotation_return_exact_dict_no_response_model_annotation_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_invalid_dict": { + "get": { + "summary": "No Response Model Annotation Return Invalid Dict", + "operationId": "no_response_model_annotation_return_invalid_dict_no_response_model_annotation_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_invalid_model": { + "get": { + "summary": "No Response Model Annotation Return Invalid Model", + "operationId": "no_response_model_annotation_return_invalid_model_no_response_model_annotation_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_dict_with_extra_data": { + "get": { + "summary": "No Response Model Annotation Return Dict With Extra Data", + "operationId": "no_response_model_annotation_return_dict_with_extra_data_no_response_model_annotation_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_submodel_with_extra_data": { + "get": { + "summary": "No Response Model Annotation Return Submodel With Extra Data", + "operationId": "no_response_model_annotation_return_submodel_with_extra_data_no_response_model_annotation_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_none-annotation-return_same_model": { + "get": { + "summary": "Response Model None Annotation Return Same Model", + "operationId": "response_model_none_annotation_return_same_model_response_model_none_annotation_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_exact_dict": { + "get": { + "summary": "Response Model None Annotation Return Exact Dict", + "operationId": "response_model_none_annotation_return_exact_dict_response_model_none_annotation_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_invalid_dict": { + "get": { + "summary": "Response Model None Annotation Return Invalid Dict", + "operationId": "response_model_none_annotation_return_invalid_dict_response_model_none_annotation_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_invalid_model": { + "get": { + "summary": "Response Model None Annotation Return Invalid Model", + "operationId": "response_model_none_annotation_return_invalid_model_response_model_none_annotation_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_dict_with_extra_data": { + "get": { + "summary": "Response Model None Annotation Return Dict With Extra Data", + "operationId": "response_model_none_annotation_return_dict_with_extra_data_response_model_none_annotation_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_submodel_with_extra_data": { + "get": { + "summary": "Response Model None Annotation Return Submodel With Extra Data", + "operationId": "response_model_none_annotation_return_submodel_with_extra_data_response_model_none_annotation_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_same_model": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Same Model", + "operationId": "response_model_model1_annotation_model2_return_same_model_response_model_model1_annotation_model2_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_exact_dict": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Exact Dict", + "operationId": "response_model_model1_annotation_model2_return_exact_dict_response_model_model1_annotation_model2_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_invalid_dict": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Invalid Dict", + "operationId": "response_model_model1_annotation_model2_return_invalid_dict_response_model_model1_annotation_model2_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_invalid_model": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Invalid Model", + "operationId": "response_model_model1_annotation_model2_return_invalid_model_response_model_model1_annotation_model2_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_dict_with_extra_data": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Dict With Extra Data", + "operationId": "response_model_model1_annotation_model2_return_dict_with_extra_data_response_model_model1_annotation_model2_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_submodel_with_extra_data": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Submodel With Extra Data", + "operationId": "response_model_model1_annotation_model2_return_submodel_with_extra_data_response_model_model1_annotation_model2_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_filtering_model-annotation_submodel-return_submodel": { + "get": { + "summary": "Response Model Filtering Model Annotation Submodel Return Submodel", + "operationId": "response_model_filtering_model_annotation_submodel_return_submodel_response_model_filtering_model_annotation_submodel_return_submodel_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_list_of_model-no_annotation": { + "get": { + "summary": "Response Model List Of Model No Annotation", + "operationId": "response_model_list_of_model_no_annotation_response_model_list_of_model_no_annotation_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Response Model List Of Model No Annotation Response Model List Of Model No Annotation Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_list_of_model": { + "get": { + "summary": "No Response Model Annotation List Of Model", + "operationId": "no_response_model_annotation_list_of_model_no_response_model_annotation_list_of_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation List Of Model No Response Model Annotation List Of Model Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_forward_ref_list_of_model": { + "get": { + "summary": "No Response Model Annotation Forward Ref List Of Model", + "operationId": "no_response_model_annotation_forward_ref_list_of_model_no_response_model_annotation_forward_ref_list_of_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation Forward Ref List Of Model No Response Model Annotation Forward Ref List Of Model Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + } + }, + } + }, + "/response_model_union-no_annotation-return_model1": { + "get": { + "summary": "Response Model Union No Annotation Return Model1", + "operationId": "response_model_union_no_annotation_return_model1_response_model_union_no_annotation_return_model1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Response Model Union No Annotation Return Model1 Response Model Union No Annotation Return Model1 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/response_model_union-no_annotation-return_model2": { + "get": { + "summary": "Response Model Union No Annotation Return Model2", + "operationId": "response_model_union_no_annotation_return_model2_response_model_union_no_annotation_return_model2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Response Model Union No Annotation Return Model2 Response Model Union No Annotation Return Model2 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_union-return_model1": { + "get": { + "summary": "No Response Model Annotation Union Return Model1", + "operationId": "no_response_model_annotation_union_return_model1_no_response_model_annotation_union_return_model1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation Union Return Model1 No Response Model Annotation Union Return Model1 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_union-return_model2": { + "get": { + "summary": "No Response Model Annotation Union Return Model2", + "operationId": "no_response_model_annotation_union_return_model2_no_response_model_annotation_union_return_model2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation Union Return Model2 No Response Model Annotation Union Return Model2 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_response_class": { + "get": { + "summary": "No Response Model Annotation Response Class", + "operationId": "no_response_model_annotation_response_class_no_response_model_annotation_response_class_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/no_response_model-annotation_json_response_class": { + "get": { + "summary": "No Response Model Annotation Json Response Class", + "operationId": "no_response_model_annotation_json_response_class_no_response_model_annotation_json_response_class_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["name", "surname"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "surname": {"title": "Surname", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_response_model_data_filter.py b/tests/test_response_model_data_filter.py new file mode 100644 index 0000000000..a3e0f95f04 --- /dev/null +++ b/tests/test_response_model_data_filter.py @@ -0,0 +1,81 @@ +from typing import List + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class UserBase(BaseModel): + email: str + + +class UserCreate(UserBase): + password: str + + +class UserDB(UserBase): + hashed_password: str + + +class PetDB(BaseModel): + name: str + owner: UserDB + + +class PetOut(BaseModel): + name: str + owner: UserBase + + +@app.post("/users/", response_model=UserBase) +async def create_user(user: UserCreate): + return user + + +@app.get("/pets/{pet_id}", response_model=PetOut) +async def read_pet(pet_id: int): + user = UserDB( + email="johndoe@example.com", + hashed_password="secrethashed", + ) + pet = PetDB(name="Nibbler", owner=user) + return pet + + +@app.get("/pets/", response_model=List[PetOut]) +async def read_pets(): + user = UserDB( + email="johndoe@example.com", + hashed_password="secrethashed", + ) + pet1 = PetDB(name="Nibbler", owner=user) + pet2 = PetDB(name="Zoidberg", owner=user) + return [pet1, pet2] + + +client = TestClient(app) + + +def test_filter_top_level_model(): + response = client.post( + "/users", json={"email": "johndoe@example.com", "password": "secret"} + ) + assert response.json() == {"email": "johndoe@example.com"} + + +def test_filter_second_level_model(): + response = client.get("/pets/1") + assert response.json() == { + "name": "Nibbler", + "owner": {"email": "johndoe@example.com"}, + } + + +def test_list_of_models(): + response = client.get("/pets/") + assert response.json() == [ + {"name": "Nibbler", "owner": {"email": "johndoe@example.com"}}, + {"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}}, + ] diff --git a/tests/test_response_model_data_filter_no_inheritance.py b/tests/test_response_model_data_filter_no_inheritance.py new file mode 100644 index 0000000000..64003a8417 --- /dev/null +++ b/tests/test_response_model_data_filter_no_inheritance.py @@ -0,0 +1,83 @@ +from typing import List + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class UserCreate(BaseModel): + email: str + password: str + + +class UserDB(BaseModel): + email: str + hashed_password: str + + +class User(BaseModel): + email: str + + +class PetDB(BaseModel): + name: str + owner: UserDB + + +class PetOut(BaseModel): + name: str + owner: User + + +@app.post("/users/", response_model=User) +async def create_user(user: UserCreate): + return user + + +@app.get("/pets/{pet_id}", response_model=PetOut) +async def read_pet(pet_id: int): + user = UserDB( + email="johndoe@example.com", + hashed_password="secrethashed", + ) + pet = PetDB(name="Nibbler", owner=user) + return pet + + +@app.get("/pets/", response_model=List[PetOut]) +async def read_pets(): + user = UserDB( + email="johndoe@example.com", + hashed_password="secrethashed", + ) + pet1 = PetDB(name="Nibbler", owner=user) + pet2 = PetDB(name="Zoidberg", owner=user) + return [pet1, pet2] + + +client = TestClient(app) + + +def test_filter_top_level_model(): + response = client.post( + "/users", json={"email": "johndoe@example.com", "password": "secret"} + ) + assert response.json() == {"email": "johndoe@example.com"} + + +def test_filter_second_level_model(): + response = client.get("/pets/1") + assert response.json() == { + "name": "Nibbler", + "owner": {"email": "johndoe@example.com"}, + } + + +def test_list_of_models(): + response = client.get("/pets/") + assert response.json() == [ + {"name": "Nibbler", "owner": {"email": "johndoe@example.com"}}, + {"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}}, + ] diff --git a/tests/test_response_model_sub_types.py b/tests/test_response_model_sub_types.py index fd972e6a3d..660bcee1bb 100644 --- a/tests/test_response_model_sub_types.py +++ b/tests/test_response_model_sub_types.py @@ -32,123 +32,9 @@ def valid4(): pass -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/valid1": { - "get": { - "summary": "Valid1", - "operationId": "valid1_valid1_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "title": "Response 500 Valid1 Valid1 Get", - "type": "integer", - } - } - }, - }, - }, - } - }, - "/valid2": { - "get": { - "summary": "Valid2", - "operationId": "valid2_valid2_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "title": "Response 500 Valid2 Valid2 Get", - "type": "array", - "items": {"type": "integer"}, - } - } - }, - }, - }, - } - }, - "/valid3": { - "get": { - "summary": "Valid3", - "operationId": "valid3_valid3_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - }, - }, - } - }, - "/valid4": { - "get": { - "summary": "Valid4", - "operationId": "valid4_valid4_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "title": "Response 500 Valid4 Valid4 Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Model"}, - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Model": { - "title": "Model", - "required": ["name"], - "type": "object", - "properties": {"name": {"title": "Name", "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 - - def test_path_operations(): response = client.get("/valid1") assert response.status_code == 200, response.text @@ -158,3 +44,115 @@ def test_path_operations(): assert response.status_code == 200, response.text response = client.get("/valid4") assert response.status_code == 200, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/valid1": { + "get": { + "summary": "Valid1", + "operationId": "valid1_valid1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "title": "Response 500 Valid1 Valid1 Get", + "type": "integer", + } + } + }, + }, + }, + } + }, + "/valid2": { + "get": { + "summary": "Valid2", + "operationId": "valid2_valid2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "title": "Response 500 Valid2 Valid2 Get", + "type": "array", + "items": {"type": "integer"}, + } + } + }, + }, + }, + } + }, + "/valid3": { + "get": { + "summary": "Valid3", + "operationId": "valid3_valid3_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + }, + }, + } + }, + "/valid4": { + "get": { + "summary": "Valid4", + "operationId": "valid4_valid4_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "title": "Response 500 Valid4 Valid4 Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Model"}, + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Model": { + "title": "Model", + "required": ["name"], + "type": "object", + "properties": {"name": {"title": "Name", "type": "string"}}, + } + } + }, + } diff --git a/tests/test_route_scope.py b/tests/test_route_scope.py index a188e9a5fe..2021c828f4 100644 --- a/tests/test_route_scope.py +++ b/tests/test_route_scope.py @@ -46,5 +46,5 @@ def test_websocket(): def test_websocket_invalid_path_doesnt_match(): with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/itemsx/portal-gun") as websocket: - websocket.receive_json() + with client.websocket_connect("/itemsx/portal-gun"): + pass diff --git a/tests/test_router_events.py b/tests/test_router_events.py index 5ff1fdf9f2..1b9de18aea 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -1,3 +1,7 @@ +from contextlib import asynccontextmanager +from typing import AsyncGenerator, Dict + +import pytest from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -12,57 +16,52 @@ class State(BaseModel): sub_router_shutdown: bool = False -state = State() - -app = FastAPI() +@pytest.fixture +def state() -> State: + return State() -@app.on_event("startup") -def app_startup(): - state.app_startup = True +@pytest.mark.filterwarnings( + r"ignore:\s*on_event is deprecated, use lifespan event handlers instead.*:DeprecationWarning" +) +def test_router_events(state: State) -> None: + app = FastAPI() + @app.get("/") + def main() -> Dict[str, str]: + return {"message": "Hello World"} -@app.on_event("shutdown") -def app_shutdown(): - state.app_shutdown = True + @app.on_event("startup") + def app_startup() -> None: + state.app_startup = True + @app.on_event("shutdown") + def app_shutdown() -> None: + state.app_shutdown = True -router = APIRouter() + router = APIRouter() + @router.on_event("startup") + def router_startup() -> None: + state.router_startup = True -@router.on_event("startup") -def router_startup(): - state.router_startup = True + @router.on_event("shutdown") + def router_shutdown() -> None: + state.router_shutdown = True + sub_router = APIRouter() -@router.on_event("shutdown") -def router_shutdown(): - state.router_shutdown = True + @sub_router.on_event("startup") + def sub_router_startup() -> None: + state.sub_router_startup = True + @sub_router.on_event("shutdown") + def sub_router_shutdown() -> None: + state.sub_router_shutdown = True -sub_router = APIRouter() + router.include_router(sub_router) + app.include_router(router) - -@sub_router.on_event("startup") -def sub_router_startup(): - state.sub_router_startup = True - - -@sub_router.on_event("shutdown") -def sub_router_shutdown(): - state.sub_router_shutdown = True - - -@sub_router.get("/") -def main(): - return {"message": "Hello World"} - - -router.include_router(sub_router) -app.include_router(router) - - -def test_router_events(): assert state.app_startup is False assert state.router_startup is False assert state.sub_router_startup is False @@ -85,3 +84,28 @@ def test_router_events(): assert state.app_shutdown is True assert state.router_shutdown is True assert state.sub_router_shutdown is True + + +def test_app_lifespan_state(state: State) -> None: + @asynccontextmanager + async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + state.app_startup = True + yield + state.app_shutdown = True + + app = FastAPI(lifespan=lifespan) + + @app.get("/") + def main() -> Dict[str, str]: + return {"message": "Hello World"} + + assert state.app_startup is False + assert state.app_shutdown is False + with TestClient(app) as client: + assert state.app_startup is True + assert state.app_shutdown is False + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello World"} + assert state.app_startup is True + assert state.app_shutdown is True diff --git a/tests/test_router_redirect_slashes.py b/tests/test_router_redirect_slashes.py new file mode 100644 index 0000000000..086665c040 --- /dev/null +++ b/tests/test_router_redirect_slashes.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, FastAPI +from fastapi.testclient import TestClient + + +def test_redirect_slashes_enabled(): + app = FastAPI() + router = APIRouter() + + @router.get("/hello/") + def hello_page() -> str: + return "Hello, World!" + + app.include_router(router) + + client = TestClient(app) + + response = client.get("/hello/", follow_redirects=False) + assert response.status_code == 200 + + response = client.get("/hello", follow_redirects=False) + assert response.status_code == 307 + + +def test_redirect_slashes_disabled(): + app = FastAPI(redirect_slashes=False) + router = APIRouter() + + @router.get("/hello/") + def hello_page() -> str: + return "Hello, World!" + + app.include_router(router) + + client = TestClient(app) + + response = client.get("/hello/", follow_redirects=False) + assert response.status_code == 200 + + response = client.get("/hello", follow_redirects=False) + assert response.status_code == 404 diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index f07d2c3b8c..b313f47e90 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -1,857 +1,228 @@ from typing import Union +import pytest +from dirty_equals import IsDict from fastapi import Body, Cookie, FastAPI, Header, Path, Query +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient -from pydantic import BaseModel - -app = FastAPI() +from pydantic import BaseModel, ConfigDict -class Item(BaseModel): - data: str +def create_app(): + app = FastAPI() - class Config: - schema_extra = {"example": {"data": "Data in schema_extra"}} + class Item(BaseModel): + data: str + if PYDANTIC_V2: + model_config = ConfigDict( + json_schema_extra={"example": {"data": "Data in schema_extra"}} + ) + else: -@app.post("/schema_extra/") -def schema_extra(item: Item): - return item + class Config: + schema_extra = {"example": {"data": "Data in schema_extra"}} + @app.post("/schema_extra/") + def schema_extra(item: Item): + return item -@app.post("/example/") -def example(item: Item = Body(example={"data": "Data in Body example"})): - return item + with pytest.warns(DeprecationWarning): + @app.post("/example/") + def example(item: Item = Body(example={"data": "Data in Body example"})): + return item -@app.post("/examples/") -def examples( - item: Item = Body( - examples={ - "example1": { - "summary": "example1 summary", - "value": {"data": "Data in Body examples, example1"}, - }, - "example2": {"value": {"data": "Data in Body examples, example2"}}, - }, - ) -): - return item + @app.post("/examples/") + def examples( + item: Item = Body( + examples=[ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + ), + ): + return item + with pytest.warns(DeprecationWarning): -@app.post("/example_examples/") -def example_examples( - item: Item = Body( - example={"data": "Overriden example"}, - examples={ - "example1": {"value": {"data": "examples example_examples 1"}}, - "example2": {"value": {"data": "examples example_examples 2"}}, - }, - ) -): - return item - - -# TODO: enable these tests once/if Form(embed=False) is supported -# TODO: In that case, define if File() should support example/examples too -# @app.post("/form_example") -# def form_example(firstname: str = Form(example="John")): -# return firstname - - -# @app.post("/form_examples") -# def form_examples( -# lastname: str = Form( -# ..., -# examples={ -# "example1": {"summary": "last name summary", "value": "Doe"}, -# "example2": {"value": "Doesn't"}, -# }, -# ), -# ): -# return lastname - - -# @app.post("/form_example_examples") -# def form_example_examples( -# lastname: str = Form( -# ..., -# example="Doe overriden", -# examples={ -# "example1": {"summary": "last name summary", "value": "Doe"}, -# "example2": {"value": "Doesn't"}, -# }, -# ), -# ): -# return lastname - - -@app.get("/path_example/{item_id}") -def path_example( - item_id: str = Path( - example="item_1", - ), -): - return item_id - - -@app.get("/path_examples/{item_id}") -def path_examples( - item_id: str = Path( - examples={ - "example1": {"summary": "item ID summary", "value": "item_1"}, - "example2": {"value": "item_2"}, - }, - ), -): - return item_id - - -@app.get("/path_example_examples/{item_id}") -def path_example_examples( - item_id: str = Path( - example="item_overriden", - examples={ - "example1": {"summary": "item ID summary", "value": "item_1"}, - "example2": {"value": "item_2"}, - }, - ), -): - return item_id - - -@app.get("/query_example/") -def query_example( - data: Union[str, None] = Query( - default=None, - example="query1", - ), -): - return data - - -@app.get("/query_examples/") -def query_examples( - data: Union[str, None] = Query( - default=None, - examples={ - "example1": {"summary": "Query example 1", "value": "query1"}, - "example2": {"value": "query2"}, - }, - ), -): - return data - - -@app.get("/query_example_examples/") -def query_example_examples( - data: Union[str, None] = Query( - default=None, - example="query_overriden", - examples={ - "example1": {"summary": "Query example 1", "value": "query1"}, - "example2": {"value": "query2"}, - }, - ), -): - return data - - -@app.get("/header_example/") -def header_example( - data: Union[str, None] = Header( - default=None, - example="header1", - ), -): - return data - - -@app.get("/header_examples/") -def header_examples( - data: Union[str, None] = Header( - default=None, - examples={ - "example1": {"summary": "header example 1", "value": "header1"}, - "example2": {"value": "header2"}, - }, - ), -): - return data - - -@app.get("/header_example_examples/") -def header_example_examples( - data: Union[str, None] = Header( - default=None, - example="header_overriden", - examples={ - "example1": {"summary": "Query example 1", "value": "header1"}, - "example2": {"value": "header2"}, - }, - ), -): - return data - - -@app.get("/cookie_example/") -def cookie_example( - data: Union[str, None] = Cookie( - default=None, - example="cookie1", - ), -): - return data - - -@app.get("/cookie_examples/") -def cookie_examples( - data: Union[str, None] = Cookie( - default=None, - examples={ - "example1": {"summary": "cookie example 1", "value": "cookie1"}, - "example2": {"value": "cookie2"}, - }, - ), -): - return data - - -@app.get("/cookie_example_examples/") -def cookie_example_examples( - data: Union[str, None] = Cookie( - default=None, - example="cookie_overriden", - examples={ - "example1": {"summary": "Query example 1", "value": "cookie1"}, - "example2": {"value": "cookie2"}, - }, - ), -): - return data - - -client = TestClient(app) - - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/schema_extra/": { - "post": { - "summary": "Schema Extra", - "operationId": "schema_extra_schema_extra__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/example/": { - "post": { - "summary": "Example", - "operationId": "example_example__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "example": {"data": "Data in Body example"}, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/examples/": { - "post": { - "summary": "Examples", - "operationId": "examples_examples__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "example1": { - "summary": "example1 summary", - "value": { - "data": "Data in Body examples, example1" - }, - }, - "example2": { - "value": {"data": "Data in Body examples, example2"} - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/example_examples/": { - "post": { - "summary": "Example Examples", - "operationId": "example_examples_example_examples__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "example1": { - "value": {"data": "examples example_examples 1"} - }, - "example2": { - "value": {"data": "examples example_examples 2"} - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/path_example/{item_id}": { - "get": { - "summary": "Path Example", - "operationId": "path_example_path_example__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "example": "item_1", - "name": "item_id", - "in": "path", - } + @app.post("/example_examples/") + def example_examples( + item: Item = Body( + example={"data": "Overridden example"}, + examples=[ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/path_examples/{item_id}": { - "get": { - "summary": "Path Examples", - "operationId": "path_examples_path_examples__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "examples": { - "example1": { - "summary": "item ID summary", - "value": "item_1", - }, - "example2": {"value": "item_2"}, - }, - "name": "item_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/path_example_examples/{item_id}": { - "get": { - "summary": "Path Example Examples", - "operationId": "path_example_examples_path_example_examples__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "examples": { - "example1": { - "summary": "item ID summary", - "value": "item_1", - }, - "example2": {"value": "item_2"}, - }, - "name": "item_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/query_example/": { - "get": { - "summary": "Query Example", - "operationId": "query_example_query_example__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "example": "query1", - "name": "data", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/query_examples/": { - "get": { - "summary": "Query Examples", - "operationId": "query_examples_query_examples__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "query1", - }, - "example2": {"value": "query2"}, - }, - "name": "data", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/query_example_examples/": { - "get": { - "summary": "Query Example Examples", - "operationId": "query_example_examples_query_example_examples__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "query1", - }, - "example2": {"value": "query2"}, - }, - "name": "data", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/header_example/": { - "get": { - "summary": "Header Example", - "operationId": "header_example_header_example__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "example": "header1", - "name": "data", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/header_examples/": { - "get": { - "summary": "Header Examples", - "operationId": "header_examples_header_examples__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "header example 1", - "value": "header1", - }, - "example2": {"value": "header2"}, - }, - "name": "data", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/header_example_examples/": { - "get": { - "summary": "Header Example Examples", - "operationId": "header_example_examples_header_example_examples__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "header1", - }, - "example2": {"value": "header2"}, - }, - "name": "data", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookie_example/": { - "get": { - "summary": "Cookie Example", - "operationId": "cookie_example_cookie_example__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "example": "cookie1", - "name": "data", - "in": "cookie", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookie_examples/": { - "get": { - "summary": "Cookie Examples", - "operationId": "cookie_examples_cookie_examples__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "cookie example 1", - "value": "cookie1", - }, - "example2": {"value": "cookie2"}, - }, - "name": "data", - "in": "cookie", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookie_example_examples/": { - "get": { - "summary": "Cookie Example Examples", - "operationId": "cookie_example_examples_cookie_example_examples__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "cookie1", - }, - "example2": {"value": "cookie2"}, - }, - "name": "data", - "in": "cookie", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["data"], - "type": "object", - "properties": {"data": {"title": "Data", "type": "string"}}, - "example": {"data": "Data in schema_extra"}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} + ), + ): + return item + # TODO: enable these tests once/if Form(embed=False) is supported + # TODO: In that case, define if File() should support example/examples too + # @app.post("/form_example") + # def form_example(firstname: str = Form(example="John")): + # return firstname -def test_openapi_schema(): - """ - Test that example overrides work: + # @app.post("/form_examples") + # def form_examples( + # lastname: str = Form( + # ..., + # examples={ + # "example1": {"summary": "last name summary", "value": "Doe"}, + # "example2": {"value": "Doesn't"}, + # }, + # ), + # ): + # return lastname - * pydantic model schema_extra is included - * Body(example={}) overrides schema_extra in pydantic model - * Body(examples{}) overrides Body(example={}) and schema_extra in pydantic model - """ - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema + # @app.post("/form_example_examples") + # def form_example_examples( + # lastname: str = Form( + # ..., + # example="Doe overridden", + # examples={ + # "example1": {"summary": "last name summary", "value": "Doe"}, + # "example2": {"value": "Doesn't"}, + # }, + # ), + # ): + # return lastname + + with pytest.warns(DeprecationWarning): + + @app.get("/path_example/{item_id}") + def path_example( + item_id: str = Path( + example="item_1", + ), + ): + return item_id + + @app.get("/path_examples/{item_id}") + def path_examples( + item_id: str = Path( + examples=["item_1", "item_2"], + ), + ): + return item_id + + with pytest.warns(DeprecationWarning): + + @app.get("/path_example_examples/{item_id}") + def path_example_examples( + item_id: str = Path( + example="item_overridden", + examples=["item_1", "item_2"], + ), + ): + return item_id + + with pytest.warns(DeprecationWarning): + + @app.get("/query_example/") + def query_example( + data: Union[str, None] = Query( + default=None, + example="query1", + ), + ): + return data + + @app.get("/query_examples/") + def query_examples( + data: Union[str, None] = Query( + default=None, + examples=["query1", "query2"], + ), + ): + return data + + with pytest.warns(DeprecationWarning): + + @app.get("/query_example_examples/") + def query_example_examples( + data: Union[str, None] = Query( + default=None, + example="query_overridden", + examples=["query1", "query2"], + ), + ): + return data + + with pytest.warns(DeprecationWarning): + + @app.get("/header_example/") + def header_example( + data: Union[str, None] = Header( + default=None, + example="header1", + ), + ): + return data + + @app.get("/header_examples/") + def header_examples( + data: Union[str, None] = Header( + default=None, + examples=[ + "header1", + "header2", + ], + ), + ): + return data + + with pytest.warns(DeprecationWarning): + + @app.get("/header_example_examples/") + def header_example_examples( + data: Union[str, None] = Header( + default=None, + example="header_overridden", + examples=["header1", "header2"], + ), + ): + return data + + with pytest.warns(DeprecationWarning): + + @app.get("/cookie_example/") + def cookie_example( + data: Union[str, None] = Cookie( + default=None, + example="cookie1", + ), + ): + return data + + @app.get("/cookie_examples/") + def cookie_examples( + data: Union[str, None] = Cookie( + default=None, + examples=["cookie1", "cookie2"], + ), + ): + return data + + with pytest.warns(DeprecationWarning): + + @app.get("/cookie_example_examples/") + def cookie_example_examples( + data: Union[str, None] = Cookie( + default=None, + example="cookie_overridden", + examples=["cookie1", "cookie2"], + ), + ): + return data + + return app def test_call_api(): + app = create_app() + client = TestClient(app) response = client.post("/schema_extra/", json={"data": "Foo"}) assert response.status_code == 200, response.text response = client.post("/example/", json={"data": "Foo"}) @@ -884,3 +255,712 @@ def test_call_api(): assert response.status_code == 200, response.text response = client.get("/cookie_example_examples/") assert response.status_code == 200, response.text + + +def test_openapi_schema(): + """ + Test that example overrides work: + + * pydantic model schema_extra is included + * Body(example={}) overrides schema_extra in pydantic model + * Body(examples{}) overrides Body(example={}) and schema_extra in pydantic model + """ + app = create_app() + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/schema_extra/": { + "post": { + "summary": "Schema Extra", + "operationId": "schema_extra_schema_extra__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/example/": { + "post": { + "summary": "Example", + "operationId": "example_example__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "example": {"data": "Data in Body example"}, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/examples/": { + "post": { + "summary": "Examples", + "operationId": "examples_examples__post", + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + } + ) + | IsDict( + # TODO: remove this when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + } + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/example_examples/": { + "post": { + "summary": "Example Examples", + "operationId": "example_examples_example_examples__post", + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], + } + ) + | IsDict( + # TODO: remove this when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], + }, + ), + "example": {"data": "Overridden example"}, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/path_example/{item_id}": { + "get": { + "summary": "Path Example", + "operationId": "path_example_path_example__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "example": "item_1", + "name": "item_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/path_examples/{item_id}": { + "get": { + "summary": "Path Examples", + "operationId": "path_examples_path_examples__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "examples": ["item_1", "item_2"], + }, + "name": "item_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/path_example_examples/{item_id}": { + "get": { + "summary": "Path Example Examples", + "operationId": "path_example_examples_path_example_examples__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "examples": ["item_1", "item_2"], + }, + "example": "item_overridden", + "name": "item_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query_example/": { + "get": { + "summary": "Query Example", + "operationId": "query_example_query_example__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + {"title": "Data", "type": "string"} + ), + "example": "query1", + "name": "data", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query_examples/": { + "get": { + "summary": "Query Examples", + "operationId": "query_examples_query_examples__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "type": "string", + "title": "Data", + "examples": ["query1", "query2"], + } + ), + "name": "data", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query_example_examples/": { + "get": { + "summary": "Query Example Examples", + "operationId": "query_example_examples_query_example_examples__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "type": "string", + "title": "Data", + "examples": ["query1", "query2"], + } + ), + "example": "query_overridden", + "name": "data", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/header_example/": { + "get": { + "summary": "Header Example", + "operationId": "header_example_header_example__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + {"title": "Data", "type": "string"} + ), + "example": "header1", + "name": "data", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/header_examples/": { + "get": { + "summary": "Header Examples", + "operationId": "header_examples_header_examples__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "type": "string", + "title": "Data", + "examples": ["header1", "header2"], + } + ), + "name": "data", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/header_example_examples/": { + "get": { + "summary": "Header Example Examples", + "operationId": "header_example_examples_header_example_examples__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "title": "Data", + "type": "string", + "examples": ["header1", "header2"], + } + ), + "example": "header_overridden", + "name": "data", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookie_example/": { + "get": { + "summary": "Cookie Example", + "operationId": "cookie_example_cookie_example__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + {"title": "Data", "type": "string"} + ), + "example": "cookie1", + "name": "data", + "in": "cookie", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookie_examples/": { + "get": { + "summary": "Cookie Examples", + "operationId": "cookie_examples_cookie_examples__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "title": "Data", + "type": "string", + "examples": ["cookie1", "cookie2"], + } + ), + "name": "data", + "in": "cookie", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookie_example_examples/": { + "get": { + "summary": "Cookie Example Examples", + "operationId": "cookie_example_examples_cookie_example_examples__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "title": "Data", + "type": "string", + "examples": ["cookie1", "cookie2"], + } + ), + "example": "cookie_overridden", + "name": "data", + "in": "cookie", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["data"], + "type": "object", + "properties": {"data": {"title": "Data", "type": "string"}}, + "example": {"data": "Data in schema_extra"}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py index 0bf4e9bb3a..4ddb8e2eeb 100644 --- a/tests/test_security_api_key_cookie.py +++ b/tests/test_security_api_key_cookie.py @@ -22,39 +22,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): return current_user -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyCookie": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} - } - }, -} - - -def test_openapi_schema(): - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_security_api_key(): client = TestClient(app, cookies={"key": "secret"}) response = client.get("/users/me") @@ -67,3 +34,33 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyCookie": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} + } + }, + } diff --git a/tests/test_security_api_key_cookie_description.py b/tests/test_security_api_key_cookie_description.py index ed4e652394..d99d616e06 100644 --- a/tests/test_security_api_key_cookie_description.py +++ b/tests/test_security_api_key_cookie_description.py @@ -22,44 +22,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): return current_user -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyCookie": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyCookie": { - "type": "apiKey", - "name": "key", - "in": "cookie", - "description": "An API Cookie Key", - } - } - }, -} - - -def test_openapi_schema(): - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_security_api_key(): client = TestClient(app, cookies={"key": "secret"}) response = client.get("/users/me") @@ -72,3 +34,38 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyCookie": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyCookie": { + "type": "apiKey", + "name": "key", + "in": "cookie", + "description": "An API Cookie Key", + } + } + }, + } diff --git a/tests/test_security_api_key_cookie_optional.py b/tests/test_security_api_key_cookie_optional.py index 3e7aa81c07..cb5590168d 100644 --- a/tests/test_security_api_key_cookie_optional.py +++ b/tests/test_security_api_key_cookie_optional.py @@ -29,39 +29,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): return current_user -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyCookie": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} - } - }, -} - - -def test_openapi_schema(): - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_security_api_key(): client = TestClient(app, cookies={"key": "secret"}) response = client.get("/users/me") @@ -74,3 +41,33 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyCookie": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} + } + }, + } diff --git a/tests/test_security_api_key_header.py b/tests/test_security_api_key_header.py index d53395f999..1ff883703d 100644 --- a/tests/test_security_api_key_header.py +++ b/tests/test_security_api_key_header.py @@ -24,37 +24,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyHeader": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"} - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_api_key(): response = client.get("/users/me", headers={"key": "secret"}) @@ -66,3 +35,32 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyHeader": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"} + } + }, + } diff --git a/tests/test_security_api_key_header_description.py b/tests/test_security_api_key_header_description.py index cc98027080..27f9d0f29e 100644 --- a/tests/test_security_api_key_header_description.py +++ b/tests/test_security_api_key_header_description.py @@ -24,42 +24,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyHeader": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyHeader": { - "type": "apiKey", - "name": "key", - "in": "header", - "description": "An API Key Header", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_api_key(): response = client.get("/users/me", headers={"key": "secret"}) @@ -71,3 +35,37 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyHeader": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyHeader": { + "type": "apiKey", + "name": "key", + "in": "header", + "description": "An API Key Header", + } + } + }, + } diff --git a/tests/test_security_api_key_header_optional.py b/tests/test_security_api_key_header_optional.py index 4ab599c2dd..6f9682a64f 100644 --- a/tests/test_security_api_key_header_optional.py +++ b/tests/test_security_api_key_header_optional.py @@ -30,37 +30,6 @@ def read_current_user(current_user: Optional[User] = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyHeader": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"} - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_api_key(): response = client.get("/users/me", headers={"key": "secret"}) @@ -72,3 +41,32 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyHeader": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"} + } + }, + } diff --git a/tests/test_security_api_key_query.py b/tests/test_security_api_key_query.py index 4844c65e22..dc7a0a621a 100644 --- a/tests/test_security_api_key_query.py +++ b/tests/test_security_api_key_query.py @@ -24,37 +24,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyQuery": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_api_key(): response = client.get("/users/me?key=secret") @@ -66,3 +35,32 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyQuery": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} + } + }, + } diff --git a/tests/test_security_api_key_query_description.py b/tests/test_security_api_key_query_description.py index 9b608233a5..35dc7743a2 100644 --- a/tests/test_security_api_key_query_description.py +++ b/tests/test_security_api_key_query_description.py @@ -24,42 +24,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyQuery": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyQuery": { - "type": "apiKey", - "name": "key", - "in": "query", - "description": "API Key Query", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_api_key(): response = client.get("/users/me?key=secret") @@ -71,3 +35,37 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyQuery": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyQuery": { + "type": "apiKey", + "name": "key", + "in": "query", + "description": "API Key Query", + } + } + }, + } diff --git a/tests/test_security_api_key_query_optional.py b/tests/test_security_api_key_query_optional.py index 9339b7b3aa..4cc134bd49 100644 --- a/tests/test_security_api_key_query_optional.py +++ b/tests/test_security_api_key_query_optional.py @@ -30,37 +30,6 @@ def read_current_user(current_user: Optional[User] = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyQuery": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_api_key(): response = client.get("/users/me?key=secret") @@ -72,3 +41,32 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyQuery": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} + } + }, + } diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index 8947162796..51928bafd5 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -14,35 +14,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBase": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_base(): response = client.get("/users/me", headers={"Authorization": "Other foobar"}) @@ -54,3 +25,30 @@ def test_security_http_base_no_credentials(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBase": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}} + }, + } diff --git a/tests/test_security_http_base_description.py b/tests/test_security_http_base_description.py index 5855e8df40..bc79f32424 100644 --- a/tests/test_security_http_base_description.py +++ b/tests/test_security_http_base_description.py @@ -14,41 +14,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBase": []}], - } - } - }, - "components": { - "securitySchemes": { - "HTTPBase": { - "type": "http", - "scheme": "Other", - "description": "Other Security Scheme", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_base(): response = client.get("/users/me", headers={"Authorization": "Other foobar"}) @@ -60,3 +25,36 @@ def test_security_http_base_no_credentials(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBase": []}], + } + } + }, + "components": { + "securitySchemes": { + "HTTPBase": { + "type": "http", + "scheme": "Other", + "description": "Other Security Scheme", + } + } + }, + } diff --git a/tests/test_security_http_base_optional.py b/tests/test_security_http_base_optional.py index 5a50f9b88c..dd4d76843a 100644 --- a/tests/test_security_http_base_optional.py +++ b/tests/test_security_http_base_optional.py @@ -20,35 +20,6 @@ def read_current_user( client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBase": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_base(): response = client.get("/users/me", headers={"Authorization": "Other foobar"}) @@ -60,3 +31,30 @@ def test_security_http_base_no_credentials(): response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBase": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}} + }, + } diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py index 91824d2234..9b6cb6c455 100644 --- a/tests/test_security_http_basic_optional.py +++ b/tests/test_security_http_basic_optional.py @@ -19,35 +19,6 @@ def read_current_user(credentials: Optional[HTTPBasicCredentials] = Security(sec client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_basic(): response = client.get("/users/me", auth=("john", "secret")) @@ -77,3 +48,30 @@ def test_security_http_basic_non_basic_credentials(): assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } diff --git a/tests/test_security_http_basic_realm.py b/tests/test_security_http_basic_realm.py index 6d760c0f92..9fc33971ae 100644 --- a/tests/test_security_http_basic_realm.py +++ b/tests/test_security_http_basic_realm.py @@ -16,35 +16,6 @@ def read_current_user(credentials: HTTPBasicCredentials = Security(security)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_basic(): response = client.get("/users/me", auth=("john", "secret")) @@ -75,3 +46,30 @@ def test_security_http_basic_non_basic_credentials(): assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } diff --git a/tests/test_security_http_basic_realm_description.py b/tests/test_security_http_basic_realm_description.py index 7cc5475614..02122442eb 100644 --- a/tests/test_security_http_basic_realm_description.py +++ b/tests/test_security_http_basic_realm_description.py @@ -16,41 +16,6 @@ def read_current_user(credentials: HTTPBasicCredentials = Security(security)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], - } - } - }, - "components": { - "securitySchemes": { - "HTTPBasic": { - "type": "http", - "scheme": "basic", - "description": "HTTPBasic scheme", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_basic(): response = client.get("/users/me", auth=("john", "secret")) @@ -81,3 +46,36 @@ def test_security_http_basic_non_basic_credentials(): assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": { + "HTTPBasic": { + "type": "http", + "scheme": "basic", + "description": "HTTPBasic scheme", + } + } + }, + } diff --git a/tests/test_security_http_bearer.py b/tests/test_security_http_bearer.py index 39d8c84029..5b9e2d6919 100644 --- a/tests/test_security_http_bearer.py +++ b/tests/test_security_http_bearer.py @@ -14,35 +14,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBearer": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_bearer(): response = client.get("/users/me", headers={"Authorization": "Bearer foobar"}) @@ -60,3 +31,30 @@ def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBearer": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} + }, + } diff --git a/tests/test_security_http_bearer_description.py b/tests/test_security_http_bearer_description.py index 132e720fc1..2f11c3a148 100644 --- a/tests/test_security_http_bearer_description.py +++ b/tests/test_security_http_bearer_description.py @@ -14,41 +14,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "HTTPBearer": { - "type": "http", - "scheme": "bearer", - "description": "HTTP Bearer token scheme", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_bearer(): response = client.get("/users/me", headers={"Authorization": "Bearer foobar"}) @@ -66,3 +31,36 @@ def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "HTTPBearer": { + "type": "http", + "scheme": "bearer", + "description": "HTTP Bearer token scheme", + } + } + }, + } diff --git a/tests/test_security_http_bearer_optional.py b/tests/test_security_http_bearer_optional.py index 2e7dfb8a4e..943da2ee2e 100644 --- a/tests/test_security_http_bearer_optional.py +++ b/tests/test_security_http_bearer_optional.py @@ -20,35 +20,6 @@ def read_current_user( client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBearer": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_bearer(): response = client.get("/users/me", headers={"Authorization": "Bearer foobar"}) @@ -66,3 +37,30 @@ def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBearer": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} + }, + } diff --git a/tests/test_security_http_digest.py b/tests/test_security_http_digest.py index 8388824ff2..133d35763c 100644 --- a/tests/test_security_http_digest.py +++ b/tests/test_security_http_digest.py @@ -14,35 +14,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPDigest": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_digest(): response = client.get("/users/me", headers={"Authorization": "Digest foobar"}) @@ -62,3 +33,30 @@ def test_security_http_digest_incorrect_scheme_credentials(): ) assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPDigest": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}} + }, + } diff --git a/tests/test_security_http_digest_description.py b/tests/test_security_http_digest_description.py index d00aa1b6e3..4e31a0c008 100644 --- a/tests/test_security_http_digest_description.py +++ b/tests/test_security_http_digest_description.py @@ -14,41 +14,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPDigest": []}], - } - } - }, - "components": { - "securitySchemes": { - "HTTPDigest": { - "type": "http", - "scheme": "digest", - "description": "HTTPDigest scheme", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_digest(): response = client.get("/users/me", headers={"Authorization": "Digest foobar"}) @@ -68,3 +33,36 @@ def test_security_http_digest_incorrect_scheme_credentials(): ) assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPDigest": []}], + } + } + }, + "components": { + "securitySchemes": { + "HTTPDigest": { + "type": "http", + "scheme": "digest", + "description": "HTTPDigest scheme", + } + } + }, + } diff --git a/tests/test_security_http_digest_optional.py b/tests/test_security_http_digest_optional.py index 2177b819f3..1e6eb8bd7f 100644 --- a/tests/test_security_http_digest_optional.py +++ b/tests/test_security_http_digest_optional.py @@ -20,35 +20,6 @@ def read_current_user( client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPDigest": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_digest(): response = client.get("/users/me", headers={"Authorization": "Digest foobar"}) @@ -68,3 +39,30 @@ def test_security_http_digest_incorrect_scheme_credentials(): ) assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPDigest": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}} + }, + } diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index b9ac488eea..e98f80ebfb 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -1,7 +1,8 @@ -import pytest +from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel app = FastAPI() @@ -40,124 +41,6 @@ def read_current_user(current_user: "User" = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"OAuth2": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_login_post": { - "title": "Body_login_login_post", - "required": ["grant_type", "username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "read:users": "Read the users", - "write:users": "Create users", - }, - "tokenUrl": "token", - } - }, - } - }, - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) @@ -177,73 +60,269 @@ def test_security_oauth2_password_bearer_no_header(): assert response.json() == {"detail": "Not authenticated"} -required_params = { - "detail": [ +def test_strict_login_no_data(): + response = client.post("/login") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -grant_type_required = { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] } - ] -} - -grant_type_incorrect = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', - "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] } - ] -} + ) -@pytest.mark.parametrize( - "data,expected_status,expected_response", - [ - (None, 422, required_params), - ({"username": "johndoe", "password": "secret"}, 422, grant_type_required), - ( - {"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, - 422, - grant_type_incorrect, - ), - ( - {"username": "johndoe", "password": "secret", "grant_type": "password"}, - 200, - { - "grant_type": "password", - "username": "johndoe", - "password": "secret", - "scopes": [], - "client_id": None, - "client_secret": None, +def test_strict_login_no_grant_type(): + response = client.post("/login", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_strict_login_incorrect_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern 'password'", + "input": "incorrect", + "ctx": {"pattern": "password"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": 'string does not match regex "password"', + "type": "value_error.str.regex", + "ctx": {"pattern": "password"}, + } + ] + } + ) + + +def test_strict_login_correct_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "password"}, + ) + assert response.status_code == 200 + assert response.json() == { + "grant_type": "password", + "username": "johndoe", + "password": "secret", + "scopes": [], + "client_id": None, + "client_secret": None, + } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_login_post" + } + } + }, + "required": True, + }, + } }, - ), - ], -) -def test_strict_login(data, expected_status, expected_response): - response = client.post("/login", data=data) - assert response.status_code == expected_status - assert response.json() == expected_response + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"OAuth2": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_login_post": { + "title": "Body_login_login_post", + "required": ["grant_type", "username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "read:users": "Read the users", + "write:users": "Create users", + }, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_security_oauth2_authorization_code_bearer.py b/tests/test_security_oauth2_authorization_code_bearer.py index ad9a39ded7..f2097b1490 100644 --- a/tests/test_security_oauth2_authorization_code_bearer.py +++ b/tests/test_security_oauth2_authorization_code_bearer.py @@ -18,46 +18,6 @@ async def read_items(token: Optional[str] = Security(oauth2_scheme)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2AuthorizationCodeBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2AuthorizationCodeBearer": { - "type": "oauth2", - "flows": { - "authorizationCode": { - "authorizationUrl": "authorize", - "tokenUrl": "token", - "scopes": {}, - } - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_no_token(): response = client.get("/items") @@ -75,3 +35,41 @@ def test_token(): response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) assert response.status_code == 200, response.text assert response.json() == {"token": "testtoken"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2AuthorizationCodeBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2AuthorizationCodeBearer": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "authorize", + "tokenUrl": "token", + "scopes": {}, + } + }, + } + } + }, + } diff --git a/tests/test_security_oauth2_authorization_code_bearer_description.py b/tests/test_security_oauth2_authorization_code_bearer_description.py index bdaa543fc3..5386fbbd9d 100644 --- a/tests/test_security_oauth2_authorization_code_bearer_description.py +++ b/tests/test_security_oauth2_authorization_code_bearer_description.py @@ -21,47 +21,6 @@ async def read_items(token: Optional[str] = Security(oauth2_scheme)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2AuthorizationCodeBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2AuthorizationCodeBearer": { - "type": "oauth2", - "flows": { - "authorizationCode": { - "authorizationUrl": "authorize", - "tokenUrl": "token", - "scopes": {}, - } - }, - "description": "OAuth2 Code Bearer", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_no_token(): response = client.get("/items") @@ -79,3 +38,42 @@ def test_token(): response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) assert response.status_code == 200, response.text assert response.json() == {"token": "testtoken"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2AuthorizationCodeBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2AuthorizationCodeBearer": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "authorize", + "tokenUrl": "token", + "scopes": {}, + } + }, + "description": "OAuth2 Code Bearer", + } + } + }, + } diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index a5fd49b8c7..d06c01bba0 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -1,9 +1,10 @@ from typing import Optional -import pytest +from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel app = FastAPI() @@ -44,124 +45,6 @@ def read_users_me(current_user: Optional[User] = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_login_post": { - "title": "Body_login_login_post", - "required": ["grant_type", "username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "read:users": "Read the users", - "write:users": "Create users", - }, - "tokenUrl": "token", - } - }, - } - }, - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) @@ -181,73 +64,269 @@ def test_security_oauth2_password_bearer_no_header(): assert response.json() == {"msg": "Create an account first"} -required_params = { - "detail": [ +def test_strict_login_no_data(): + response = client.post("/login") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -grant_type_required = { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] } - ] -} - -grant_type_incorrect = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', - "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] } - ] -} + ) -@pytest.mark.parametrize( - "data,expected_status,expected_response", - [ - (None, 422, required_params), - ({"username": "johndoe", "password": "secret"}, 422, grant_type_required), - ( - {"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, - 422, - grant_type_incorrect, - ), - ( - {"username": "johndoe", "password": "secret", "grant_type": "password"}, - 200, - { - "grant_type": "password", - "username": "johndoe", - "password": "secret", - "scopes": [], - "client_id": None, - "client_secret": None, +def test_strict_login_no_grant_type(): + response = client.post("/login", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_strict_login_incorrect_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern 'password'", + "input": "incorrect", + "ctx": {"pattern": "password"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": 'string does not match regex "password"', + "type": "value_error.str.regex", + "ctx": {"pattern": "password"}, + } + ] + } + ) + + +def test_strict_login_correct_data(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "password"}, + ) + assert response.status_code == 200 + assert response.json() == { + "grant_type": "password", + "username": "johndoe", + "password": "secret", + "scopes": [], + "client_id": None, + "client_secret": None, + } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_login_post" + } + } + }, + "required": True, + }, + } }, - ), - ], -) -def test_strict_login(data, expected_status, expected_response): - response = client.post("/login", data=data) - assert response.status_code == expected_status - assert response.json() == expected_response + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_login_post": { + "title": "Body_login_login_post", + "required": ["grant_type", "username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "read:users": "Read the users", + "write:users": "Create users", + }, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index 171f96b762..9287e4366e 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -1,9 +1,10 @@ from typing import Optional -import pytest +from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel app = FastAPI() @@ -45,125 +46,6 @@ def read_users_me(current_user: Optional[User] = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_login_post": { - "title": "Body_login_login_post", - "required": ["grant_type", "username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "read:users": "Read the users", - "write:users": "Create users", - }, - "tokenUrl": "token", - } - }, - "description": "OAuth2 security scheme", - } - }, - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) @@ -183,73 +65,270 @@ def test_security_oauth2_password_bearer_no_header(): assert response.json() == {"msg": "Create an account first"} -required_params = { - "detail": [ +def test_strict_login_None(): + response = client.post("/login", data=None) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -grant_type_required = { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] } - ] -} - -grant_type_incorrect = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', - "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] } - ] -} + ) -@pytest.mark.parametrize( - "data,expected_status,expected_response", - [ - (None, 422, required_params), - ({"username": "johndoe", "password": "secret"}, 422, grant_type_required), - ( - {"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, - 422, - grant_type_incorrect, - ), - ( - {"username": "johndoe", "password": "secret", "grant_type": "password"}, - 200, - { - "grant_type": "password", - "username": "johndoe", - "password": "secret", - "scopes": [], - "client_id": None, - "client_secret": None, +def test_strict_login_no_grant_type(): + response = client.post("/login", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_strict_login_incorrect_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern 'password'", + "input": "incorrect", + "ctx": {"pattern": "password"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": 'string does not match regex "password"', + "type": "value_error.str.regex", + "ctx": {"pattern": "password"}, + } + ] + } + ) + + +def test_strict_login_correct_correct_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "password"}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "grant_type": "password", + "username": "johndoe", + "password": "secret", + "scopes": [], + "client_id": None, + "client_secret": None, + } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_login_post" + } + } + }, + "required": True, + }, + } }, - ), - ], -) -def test_strict_login(data, expected_status, expected_response): - response = client.post("/login", data=data) - assert response.status_code == expected_status - assert response.json() == expected_response + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_login_post": { + "title": "Body_login_login_post", + "required": ["grant_type", "username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "read:users": "Read the users", + "write:users": "Create users", + }, + "tokenUrl": "token", + } + }, + "description": "OAuth2 security scheme", + } + }, + }, + } diff --git a/tests/test_security_oauth2_password_bearer_optional.py b/tests/test_security_oauth2_password_bearer_optional.py index 3d6637d4a5..4c9362c3eb 100644 --- a/tests/test_security_oauth2_password_bearer_optional.py +++ b/tests/test_security_oauth2_password_bearer_optional.py @@ -18,40 +18,6 @@ async def read_items(token: Optional[str] = Security(oauth2_scheme)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2PasswordBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}}, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_no_token(): response = client.get("/items") @@ -69,3 +35,35 @@ def test_incorrect_token(): response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}}, + } + } + }, + } diff --git a/tests/test_security_oauth2_password_bearer_optional_description.py b/tests/test_security_oauth2_password_bearer_optional_description.py index 9d6a862e3c..6e6ea846cd 100644 --- a/tests/test_security_oauth2_password_bearer_optional_description.py +++ b/tests/test_security_oauth2_password_bearer_optional_description.py @@ -22,41 +22,6 @@ async def read_items(token: Optional[str] = Security(oauth2_scheme)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2PasswordBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}}, - "description": "OAuth2PasswordBearer security scheme", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_no_token(): response = client.get("/items") @@ -74,3 +39,36 @@ def test_incorrect_token(): response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}}, + "description": "OAuth2PasswordBearer security scheme", + } + } + }, + } diff --git a/tests/test_security_openid_connect.py b/tests/test_security_openid_connect.py index 8203961bed..1e322e640e 100644 --- a/tests/test_security_openid_connect.py +++ b/tests/test_security_openid_connect.py @@ -24,37 +24,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"OpenIdConnect": []}], - } - } - }, - "components": { - "securitySchemes": { - "OpenIdConnect": {"type": "openIdConnect", "openIdConnectUrl": "/openid"} - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) @@ -72,3 +41,35 @@ def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"OpenIdConnect": []}], + } + } + }, + "components": { + "securitySchemes": { + "OpenIdConnect": { + "type": "openIdConnect", + "openIdConnectUrl": "/openid", + } + } + }, + } diff --git a/tests/test_security_openid_connect_description.py b/tests/test_security_openid_connect_description.py index 218cbfc8f7..44cf57f862 100644 --- a/tests/test_security_openid_connect_description.py +++ b/tests/test_security_openid_connect_description.py @@ -26,41 +26,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"OpenIdConnect": []}], - } - } - }, - "components": { - "securitySchemes": { - "OpenIdConnect": { - "type": "openIdConnect", - "openIdConnectUrl": "/openid", - "description": "OpenIdConnect security scheme", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) @@ -78,3 +43,36 @@ def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"OpenIdConnect": []}], + } + } + }, + "components": { + "securitySchemes": { + "OpenIdConnect": { + "type": "openIdConnect", + "openIdConnectUrl": "/openid", + "description": "OpenIdConnect security scheme", + } + } + }, + } diff --git a/tests/test_security_openid_connect_optional.py b/tests/test_security_openid_connect_optional.py index 4577dfebb0..e817434b0d 100644 --- a/tests/test_security_openid_connect_optional.py +++ b/tests/test_security_openid_connect_optional.py @@ -30,37 +30,6 @@ def read_current_user(current_user: Optional[User] = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"OpenIdConnect": []}], - } - } - }, - "components": { - "securitySchemes": { - "OpenIdConnect": {"type": "openIdConnect", "openIdConnectUrl": "/openid"} - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) @@ -78,3 +47,35 @@ def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"OpenIdConnect": []}], + } + } + }, + "components": { + "securitySchemes": { + "OpenIdConnect": { + "type": "openIdConnect", + "openIdConnectUrl": "/openid", + } + } + }, + } diff --git a/tests/test_skip_defaults.py b/tests/test_skip_defaults.py index 181fff612e..02765291cb 100644 --- a/tests/test_skip_defaults.py +++ b/tests/test_skip_defaults.py @@ -12,7 +12,7 @@ class SubModel(BaseModel): class Model(BaseModel): - x: Optional[int] + x: Optional[int] = None sub: SubModel diff --git a/tests/test_starlette_exception.py b/tests/test_starlette_exception.py index 418ddff7dd..229fe80163 100644 --- a/tests/test_starlette_exception.py +++ b/tests/test_starlette_exception.py @@ -37,132 +37,6 @@ async def read_starlette_item(item_id: str): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/http-no-body-statuscode-exception": { - "get": { - "operationId": "no_body_status_code_exception_http_no_body_statuscode_exception_get", - "responses": { - "200": { - "content": {"application/json": {"schema": {}}}, - "description": "Successful Response", - } - }, - "summary": "No Body Status Code Exception", - } - }, - "/http-no-body-statuscode-with-detail-exception": { - "get": { - "operationId": "no_body_status_code_with_detail_exception_http_no_body_statuscode_with_detail_exception_get", - "responses": { - "200": { - "content": {"application/json": {"schema": {}}}, - "description": "Successful Response", - } - }, - "summary": "No Body Status Code With Detail Exception", - } - }, - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/starlette-items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Starlette Item", - "operationId": "read_starlette_item_starlette_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_item(): response = client.get("/items/foo") @@ -200,3 +74,129 @@ def test_no_body_status_code_with_detail_exception_handlers(): response = client.get("/http-no-body-statuscode-with-detail-exception") assert response.status_code == 204 assert not response.content + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/http-no-body-statuscode-exception": { + "get": { + "operationId": "no_body_status_code_exception_http_no_body_statuscode_exception_get", + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + } + }, + "summary": "No Body Status Code Exception", + } + }, + "/http-no-body-statuscode-with-detail-exception": { + "get": { + "operationId": "no_body_status_code_with_detail_exception_http_no_body_statuscode_with_detail_exception_get", + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + } + }, + "summary": "No Body Status Code With Detail Exception", + } + }, + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/starlette-items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Starlette Item", + "operationId": "read_starlette_item_starlette_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index 7574d6fbc5..ed7f4efe8a 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -1,5 +1,6 @@ from typing import Optional +from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, HttpUrl @@ -74,205 +75,6 @@ app.include_router(subrouter, callbacks=events_callback_router.routes) client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/invoices/": { - "post": { - "summary": "Create Invoice", - "description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").', - "operationId": "create_invoice_invoices__post", - "parameters": [ - { - "required": False, - "schema": { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - }, - "name": "callback_url", - "in": "query", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Invoice"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "callbacks": { - "event_callback": { - "{$callback_url}/events/{$request.body.title}": { - "get": { - "summary": "Event Callback", - "operationId": "event_callback__callback_url__events___request_body_title__get", - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Event" - } - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "invoice_notification": { - "{$callback_url}/invoices/{$request.body.id}": { - "post": { - "summary": "Invoice Notification", - "operationId": "invoice_notification__callback_url__invoices___request_body_id__post", - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceEvent" - } - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceEventReceived" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - } - }, - "components": { - "schemas": { - "Event": { - "title": "Event", - "required": ["name", "total"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "total": {"title": "Total", "type": "number"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Invoice": { - "title": "Invoice", - "required": ["id", "customer", "total"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - "customer": {"title": "Customer", "type": "string"}, - "total": {"title": "Total", "type": "number"}, - }, - }, - "InvoiceEvent": { - "title": "InvoiceEvent", - "required": ["description", "paid"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "paid": {"title": "Paid", "type": "boolean"}, - }, - }, - "InvoiceEventReceived": { - "title": "InvoiceEventReceived", - "required": ["ok"], - "type": "object", - "properties": {"ok": {"title": "Ok", "type": "boolean"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi(): - with client: - response = client.get("/openapi.json") - - assert response.json() == openapi_schema - def test_get(): response = client.post( @@ -280,3 +82,231 @@ def test_get(): ) assert response.status_code == 200, response.text assert response.json() == {"msg": "Invoice received"} + + +def test_openapi_schema(): + with client: + response = client.get("/openapi.json") + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/invoices/": { + "post": { + "summary": "Create Invoice", + "description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").', + "operationId": "create_invoice_invoices__post", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "title": "Callback Url", + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Callback Url", + "maxLength": 2083, + "minLength": 1, + "type": "string", + "format": "uri", + } + ), + "name": "callback_url", + "in": "query", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Invoice"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "callbacks": { + "event_callback": { + "{$callback_url}/events/{$request.body.title}": { + "get": { + "summary": "Event Callback", + "operationId": "event_callback__callback_url__events___request_body_title__get", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "invoice_notification": { + "{$callback_url}/invoices/{$request.body.id}": { + "post": { + "summary": "Invoice Notification", + "operationId": "invoice_notification__callback_url__invoices___request_body_id__post", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceEvent" + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceEventReceived" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + } + }, + "components": { + "schemas": { + "Event": { + "title": "Event", + "required": ["name", "total"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "total": {"title": "Total", "type": "number"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "Invoice": { + "title": "Invoice", + "required": ["id", "customer", "total"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "title": IsDict( + { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Title", "type": "string"} + ), + "customer": {"title": "Customer", "type": "string"}, + "total": {"title": "Total", "type": "number"}, + }, + }, + "InvoiceEvent": { + "title": "InvoiceEvent", + "required": ["description", "paid"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "paid": {"title": "Paid", "type": "boolean"}, + }, + }, + "InvoiceEventReceived": { + "title": "InvoiceEventReceived", + "required": ["ok"], + "type": "object", + "properties": {"ok": {"title": "Ok", "type": "boolean"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tuples.py b/tests/test_tuples.py index 6e2cc0db67..ca33d2580c 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -1,5 +1,6 @@ from typing import List, Tuple +from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel @@ -33,189 +34,6 @@ def hello(values: Tuple[int, int] = Form()): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/model-with-tuple/": { - "post": { - "summary": "Post Model With Tuple", - "operationId": "post_model_with_tuple_model_with_tuple__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemGroup"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/tuple-of-models/": { - "post": { - "summary": "Post Tuple Of Models", - "operationId": "post_tuple_of_models_tuple_of_models__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Square", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [ - {"$ref": "#/components/schemas/Coordinate"}, - {"$ref": "#/components/schemas/Coordinate"}, - ], - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/tuple-form/": { - "post": { - "summary": "Hello", - "operationId": "hello_tuple_form__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_hello_tuple_form__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_hello_tuple_form__post": { - "title": "Body_hello_tuple_form__post", - "required": ["values"], - "type": "object", - "properties": { - "values": { - "title": "Values", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "integer"}, {"type": "integer"}], - } - }, - }, - "Coordinate": { - "title": "Coordinate", - "required": ["x", "y"], - "type": "object", - "properties": { - "x": {"title": "X", "type": "number"}, - "y": {"title": "Y", "type": "number"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ItemGroup": { - "title": "ItemGroup", - "required": ["items"], - "type": "object", - "properties": { - "items": { - "title": "Items", - "type": "array", - "items": { - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "string"}, {"type": "string"}], - }, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_model_with_tuple_valid(): data = {"items": [["foo", "bar"], ["baz", "whatelse"]]} @@ -263,3 +81,230 @@ def test_tuple_form_invalid(): response = client.post("/tuple-form/", data={"values": ("1")}) assert response.status_code == 422, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model-with-tuple/": { + "post": { + "summary": "Post Model With Tuple", + "operationId": "post_model_with_tuple_model_with_tuple__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemGroup"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/tuple-of-models/": { + "post": { + "summary": "Post Tuple Of Models", + "operationId": "post_tuple_of_models_tuple_of_models__post", + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], + } + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/tuple-form/": { + "post": { + "summary": "Hello", + "operationId": "hello_tuple_form__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_hello_tuple_form__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_hello_tuple_form__post": { + "title": "Body_hello_tuple_form__post", + "required": ["values"], + "type": "object", + "properties": { + "values": IsDict( + { + "title": "Values", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "integer"}, + {"type": "integer"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Values", + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [{"type": "integer"}, {"type": "integer"}], + } + ) + }, + }, + "Coordinate": { + "title": "Coordinate", + "required": ["x", "y"], + "type": "object", + "properties": { + "x": {"title": "X", "type": "number"}, + "y": {"title": "Y", "type": "number"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ItemGroup": { + "title": "ItemGroup", + "required": ["items"], + "type": "object", + "properties": { + "items": { + "title": "Items", + "type": "array", + "items": IsDict( + { + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "string"}, + {"type": "string"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [{"type": "string"}, {"type": "string"}], + } + ), + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial001.py b/tests/test_tutorial/test_additional_responses/test_tutorial001.py index 1a8acb5231..3afeaff840 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial001.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial001.py @@ -4,105 +4,6 @@ from docs_src.additional_responses.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Message"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["id", "value"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "value": {"title": "Value", "type": "string"}, - }, - }, - "Message": { - "title": "Message", - "required": ["message"], - "type": "object", - "properties": {"message": {"title": "Message", "type": "string"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_path_operation(): response = client.get("/items/foo") @@ -114,3 +15,102 @@ def test_path_operation_not_found(): response = client.get("/items/bar") assert response.status_code == 404, response.text assert response.json() == {"message": "Item not found"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Message"} + } + }, + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["id", "value"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "value": {"title": "Value", "type": "string"}, + }, + }, + "Message": { + "title": "Message", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index 2adcf15d07..588a3160a9 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -1,104 +1,13 @@ import os import shutil +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.additional_responses.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Return the JSON item or an image.", - "content": { - "image/png": {}, - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - }, - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": {"title": "Img", "type": "boolean"}, - "name": "img", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["id", "value"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "value": {"title": "Value", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_path_operation(): response = client.get("/items/foo") @@ -113,3 +22,104 @@ def test_path_operation_img(): assert response.headers["Content-Type"] == "image/png" assert len(response.content) os.remove("./image.png") + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Return the JSON item or an image.", + "content": { + "image/png": {}, + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + }, + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Img", "type": "boolean"} + ), + "name": "img", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["id", "value"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "value": {"title": "Value", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial003.py b/tests/test_tutorial/test_additional_responses/test_tutorial003.py index 8b2167de05..bd34d2938c 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial003.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial003.py @@ -4,106 +4,6 @@ from docs_src.additional_responses.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "404": { - "description": "The item was not found", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Message"} - } - }, - }, - "200": { - "description": "Item requested by ID", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "example": {"id": "bar", "value": "The bar tenders"}, - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["id", "value"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "value": {"title": "Value", "type": "string"}, - }, - }, - "Message": { - "title": "Message", - "required": ["message"], - "type": "object", - "properties": {"message": {"title": "Message", "type": "string"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_path_operation(): response = client.get("/items/foo") @@ -115,3 +15,106 @@ def test_path_operation_not_found(): response = client.get("/items/bar") assert response.status_code == 404, response.text assert response.json() == {"message": "Item not found"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "404": { + "description": "The item was not found", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Message"} + } + }, + }, + "200": { + "description": "Item requested by ID", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "example": { + "id": "bar", + "value": "The bar tenders", + }, + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["id", "value"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "value": {"title": "Value", "type": "string"}, + }, + }, + "Message": { + "title": "Message", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index 990d5235aa..55b556d8e1 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -1,107 +1,13 @@ import os import shutil +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.additional_responses.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "404": {"description": "Item not found"}, - "302": {"description": "The item was moved"}, - "403": {"description": "Not enough privileges"}, - "200": { - "description": "Successful Response", - "content": { - "image/png": {}, - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - }, - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": {"title": "Img", "type": "boolean"}, - "name": "img", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["id", "value"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "value": {"title": "Value", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_path_operation(): response = client.get("/items/foo") @@ -116,3 +22,107 @@ def test_path_operation_img(): assert response.headers["Content-Type"] == "image/png" assert len(response.content) os.remove("./image.png") + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "404": {"description": "Item not found"}, + "302": {"description": "The item was moved"}, + "403": {"description": "Not enough privileges"}, + "200": { + "description": "Successful Response", + "content": { + "image/png": {}, + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + }, + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Img", "type": "boolean"} + ), + "name": "img", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["id", "value"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "value": {"title": "Value", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an.py new file mode 100644 index 0000000000..2cb2bb9930 --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an.py @@ -0,0 +1,17 @@ +from fastapi.testclient import TestClient + +from docs_src.additional_status_codes.tutorial001_an import app + +client = TestClient(app) + + +def test_update(): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +def test_create(): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py new file mode 100644 index 0000000000..c7660a3925 --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py @@ -0,0 +1,26 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.additional_status_codes.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_update(client: TestClient): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +@needs_py310 +def test_create(client: TestClient): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py new file mode 100644 index 0000000000..303c5dbae2 --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py @@ -0,0 +1,26 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.additional_status_codes.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_update(client: TestClient): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +@needs_py39 +def test_create(client: TestClient): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py new file mode 100644 index 0000000000..02f2e188c7 --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py @@ -0,0 +1,26 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.additional_status_codes.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_update(client: TestClient): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +@needs_py310 +def test_create(client: TestClient): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py index 1ad625db6a..13568a5328 100644 --- a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py @@ -1,123 +1,20 @@ +import pytest +from fastapi import FastAPI from fastapi.testclient import TestClient -from docs_src.async_sql_databases.tutorial001 import app - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/notes/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Notes Notes Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Note"}, - } - } - }, - } - }, - "summary": "Read Notes", - "operationId": "read_notes_notes__get", - }, - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Note"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Note", - "operationId": "create_note_notes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NoteIn"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "NoteIn": { - "title": "NoteIn", - "required": ["text", "completed"], - "type": "object", - "properties": { - "text": {"title": "Text", "type": "string"}, - "completed": {"title": "Completed", "type": "boolean"}, - }, - }, - "Note": { - "title": "Note", - "required": ["id", "text", "completed"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "integer"}, - "text": {"title": "Text", "type": "string"}, - "completed": {"title": "Completed", "type": "boolean"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from ...utils import needs_pydanticv1 -def test_openapi_schema(): - with TestClient(app) as client: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema +@pytest.fixture(name="app", scope="module") +def get_app(): + with pytest.warns(DeprecationWarning): + from docs_src.async_sql_databases.tutorial001 import app + yield app -def test_create_read(): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_create_read(app: FastAPI): with TestClient(app) as client: note = {"text": "Foo bar", "completed": False} response = client.post("/notes/", json=note) @@ -129,3 +26,121 @@ def test_create_read(): response = client.get("/notes/") assert response.status_code == 200, response.text assert data in response.json() + + +def test_openapi_schema(app: FastAPI): + with TestClient(app) as client: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/notes/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Notes Notes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Note" + }, + } + } + }, + } + }, + "summary": "Read Notes", + "operationId": "read_notes_notes__get", + }, + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Note"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Note", + "operationId": "create_note_notes__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/NoteIn"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "NoteIn": { + "title": "NoteIn", + "required": ["text", "completed"], + "type": "object", + "properties": { + "text": {"title": "Text", "type": "string"}, + "completed": {"title": "Completed", "type": "boolean"}, + }, + }, + "Note": { + "title": "Note", + "required": ["id", "text", "completed"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "text": {"title": "Text", "type": "string"}, + "completed": {"title": "Completed", "type": "boolean"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py new file mode 100644 index 0000000000..af682ecff1 --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py @@ -0,0 +1,19 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from docs_src.background_tasks.tutorial002_an import app + +client = TestClient(app) + + +def test(): + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py new file mode 100644 index 0000000000..067b2787e9 --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@needs_py310 +def test(): + from docs_src.background_tasks.tutorial002_an_py310 import app + + client = TestClient(app) + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py new file mode 100644 index 0000000000..06b5a2f57a --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@needs_py39 +def test(): + from docs_src.background_tasks.tutorial002_an_py39 import app + + client = TestClient(app) + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py index be9e499bf8..a070f850f7 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py @@ -4,34 +4,32 @@ from docs_src.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": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, - "servers": [{"url": "/api/v1"}], -} - - -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"} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + "servers": [{"url": "/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 index ac192e3db7..ce791e2157 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py @@ -4,34 +4,32 @@ from docs_src.behind_a_proxy.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, - "servers": [{"url": "/api/v1"}], -} - - -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"} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + "servers": [{"url": "/api/v1"}], + } diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py index 2727525ca4..ec17b41790 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py @@ -1,41 +1,54 @@ +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from docs_src.behind_a_proxy.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "servers": [ - {"url": "/api/v1"}, - {"url": "https://stag.example.com", "description": "Staging environment"}, - {"url": "https://prod.example.com", "description": "Production environment"}, - ], - "paths": { - "/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"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + {"url": "/api/v1"}, + { + "url": IsOneOf( + "https://stag.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://stag.example.com", + ), + "description": "Staging environment", + }, + { + "url": IsOneOf( + "https://prod.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://prod.example.com", + ), + "description": "Production environment", + }, + ], + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py index 4c4e4b75c2..2f8eb46995 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py @@ -1,40 +1,53 @@ +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from docs_src.behind_a_proxy.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "servers": [ - {"url": "https://stag.example.com", "description": "Staging environment"}, - {"url": "https://prod.example.com", "description": "Production environment"}, - ], - "paths": { - "/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"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + { + "url": IsOneOf( + "https://stag.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://stag.example.com", + ), + "description": "Staging environment", + }, + { + "url": IsOneOf( + "https://prod.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://prod.example.com", + ), + "description": "Production environment", + }, + ], + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index cd6d7b5c8b..526e265a61 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -1,467 +1,368 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.bigger_applications.app.main import app - -client = TestClient(app) - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/me": { - "get": { - "tags": ["users"], - "summary": "Read User Me", - "operationId": "read_user_me_users_me_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/{username}": { - "get": { - "tags": ["users"], - "summary": "Read User", - "operationId": "read_user_users__username__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Username", "type": "string"}, - "name": "username", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/": { - "get": { - "tags": ["items"], - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/{item_id}": { - "get": { - "tags": ["items"], - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "put": { - "tags": ["items", "custom"], - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "403": {"description": "Operation forbidden"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/admin/": { - "post": { - "tags": ["admin"], - "summary": "Update Admin", - "operationId": "update_admin_admin__post", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "418": {"description": "I'm a teapot"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/": { - "get": { - "summary": "Root", - "operationId": "root__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} +from fastapi.utils import match_pydantic_error_url -no_jessica = { - "detail": [ +@pytest.fixture(name="client") +def get_client(): + from docs_src.bigger_applications.app.main import app + + client = TestClient(app) + return client + + +def test_users_token_jessica(client: TestClient): + response = client.get("/users?token=jessica") + assert response.status_code == 200 + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +def test_users_with_no_token(client: TestClient): + response = client.get("/users") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -@pytest.mark.parametrize( - "path,expected_status,expected_response,headers", - [ - ( - "/users?token=jessica", - 200, - [{"username": "Rick"}, {"username": "Morty"}], - {}, - ), - ("/users", 422, no_jessica, {}), - ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), - ("/users/foo", 422, no_jessica, {}), - ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), - ("/users/me", 422, no_jessica, {}), - ( - "/users?token=monica", - 400, - {"detail": "No Jessica token provided"}, - {}, - ), - ( - "/items?token=jessica", - 200, - {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items/plumbus?token=jessica", - 200, - {"name": "Plumbus", "item_id": "plumbus"}, - {"X-Token": "fake-super-secret-token"}, - ), - ( - "/items/bar?token=jessica", - 404, - {"detail": "Item not found"}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items/bar?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ( - "/items/plumbus?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), - ("/", 422, no_jessica, {}), - ("/openapi.json", 200, openapi_schema, {}), - ], -) -def test_get_path(path, expected_status, expected_response, headers): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_users_foo_token_jessica(client: TestClient): + response = client.get("/users/foo?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "foo"} -def test_put_no_header(): - response = client.put("/items/foo") - assert response.status_code == 422, response.text +def test_users_foo_with_no_token(client: TestClient): + response = client.get("/users/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_me_token_jessica(client: TestClient): + response = client.get("/users/me?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "fakecurrentuser"} + + +def test_users_me_with_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_token_monica_with_no_jessica(client: TestClient): + response = client.get("/users?token=monica") + assert response.status_code == 400 + assert response.json() == {"detail": "No Jessica token provided"} + + +def test_items_token_jessica(client: TestClient): + response = client.get( + "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 assert response.json() == { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] + "plumbus": {"name": "Plumbus"}, + "gun": {"name": "Portal Gun"}, } -def test_put_invalid_header(): +def test_items_with_no_token_jessica(client: TestClient): + response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_items_plumbus_token_jessica(client: TestClient): + response = client.get( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} + + +def test_items_bar_token_jessica(client: TestClient): + response = client.get( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_items_plumbus_with_no_token(client: TestClient): + response = client.get( + "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_items_with_invalid_token(client: TestClient): + response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_items_bar_with_invalid_token(client: TestClient): + response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_items_with_missing_x_token_header(client: TestClient): + response = client.get("/items?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_items_plumbus_with_missing_x_token_header(client: TestClient): + response = client.get("/items/plumbus?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_root_token_jessica(client: TestClient): + response = client.get("/?token=jessica") + assert response.status_code == 200 + assert response.json() == {"message": "Hello Bigger Applications!"} + + +def test_root_with_no_token(client: TestClient): + response = client.get("/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_put_no_header(client: TestClient): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_put_invalid_header(client: TestClient): response = client.put("/items/foo", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} -def test_put(): +def test_put(client: TestClient): response = client.put( "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -469,7 +370,7 @@ def test_put(): assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} -def test_put_forbidden(): +def test_put_forbidden(client: TestClient): response = client.put( "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -477,7 +378,7 @@ def test_put_forbidden(): assert response.json() == {"detail": "You can only update the item: plumbus"} -def test_admin(): +def test_admin(client: TestClient): response = client.post( "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -485,7 +386,341 @@ def test_admin(): assert response.json() == {"message": "Admin getting schwifty"} -def test_admin_invalid_header(): +def test_admin_invalid_header(client: TestClient): response = client.post("/admin/", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an.py b/tests/test_tutorial/test_bigger_applications/test_main_an.py new file mode 100644 index 0000000000..c0b77d4a72 --- /dev/null +++ b/tests/test_tutorial/test_bigger_applications/test_main_an.py @@ -0,0 +1,726 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.bigger_applications.app_an.main import app + + client = TestClient(app) + return client + + +def test_users_token_jessica(client: TestClient): + response = client.get("/users?token=jessica") + assert response.status_code == 200 + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +def test_users_with_no_token(client: TestClient): + response = client.get("/users") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_foo_token_jessica(client: TestClient): + response = client.get("/users/foo?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "foo"} + + +def test_users_foo_with_no_token(client: TestClient): + response = client.get("/users/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_me_token_jessica(client: TestClient): + response = client.get("/users/me?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "fakecurrentuser"} + + +def test_users_me_with_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_token_monica_with_no_jessica(client: TestClient): + response = client.get("/users?token=monica") + assert response.status_code == 400 + assert response.json() == {"detail": "No Jessica token provided"} + + +def test_items_token_jessica(client: TestClient): + response = client.get( + "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == { + "plumbus": {"name": "Plumbus"}, + "gun": {"name": "Portal Gun"}, + } + + +def test_items_with_no_token_jessica(client: TestClient): + response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_items_plumbus_token_jessica(client: TestClient): + response = client.get( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} + + +def test_items_bar_token_jessica(client: TestClient): + response = client.get( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_items_plumbus_with_no_token(client: TestClient): + response = client.get( + "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_items_with_invalid_token(client: TestClient): + response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_items_bar_with_invalid_token(client: TestClient): + response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_items_with_missing_x_token_header(client: TestClient): + response = client.get("/items?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_items_plumbus_with_missing_x_token_header(client: TestClient): + response = client.get("/items/plumbus?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_root_token_jessica(client: TestClient): + response = client.get("/?token=jessica") + assert response.status_code == 200 + assert response.json() == {"message": "Hello Bigger Applications!"} + + +def test_root_with_no_token(client: TestClient): + response = client.get("/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_put_no_header(client: TestClient): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_put_invalid_header(client: TestClient): + response = client.put("/items/foo", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_put(client: TestClient): + response = client.put( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} + + +def test_put_forbidden(client: TestClient): + response = client.put( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 403, response.text + assert response.json() == {"detail": "You can only update the item: plumbus"} + + +def test_admin(client: TestClient): + response = client.post( + "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin getting schwifty"} + + +def test_admin_invalid_header(client: TestClient): + response = client.post("/admin/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py new file mode 100644 index 0000000000..948331b5dd --- /dev/null +++ b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py @@ -0,0 +1,753 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.bigger_applications.app_an_py39.main import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_users_token_jessica(client: TestClient): + response = client.get("/users?token=jessica") + assert response.status_code == 200 + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +@needs_py39 +def test_users_with_no_token(client: TestClient): + response = client.get("/users") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_users_foo_token_jessica(client: TestClient): + response = client.get("/users/foo?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "foo"} + + +@needs_py39 +def test_users_foo_with_no_token(client: TestClient): + response = client.get("/users/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_users_me_token_jessica(client: TestClient): + response = client.get("/users/me?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "fakecurrentuser"} + + +@needs_py39 +def test_users_me_with_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_users_token_monica_with_no_jessica(client: TestClient): + response = client.get("/users?token=monica") + assert response.status_code == 400 + assert response.json() == {"detail": "No Jessica token provided"} + + +@needs_py39 +def test_items_token_jessica(client: TestClient): + response = client.get( + "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == { + "plumbus": {"name": "Plumbus"}, + "gun": {"name": "Portal Gun"}, + } + + +@needs_py39 +def test_items_with_no_token_jessica(client: TestClient): + response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_items_plumbus_token_jessica(client: TestClient): + response = client.get( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} + + +@needs_py39 +def test_items_bar_token_jessica(client: TestClient): + response = client.get( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +@needs_py39 +def test_items_plumbus_with_no_token(client: TestClient): + response = client.get( + "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_items_with_invalid_token(client: TestClient): + response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_items_bar_with_invalid_token(client: TestClient): + response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_items_with_missing_x_token_header(client: TestClient): + response = client.get("/items?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_items_plumbus_with_missing_x_token_header(client: TestClient): + response = client.get("/items/plumbus?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_root_token_jessica(client: TestClient): + response = client.get("/?token=jessica") + assert response.status_code == 200 + assert response.json() == {"message": "Hello Bigger Applications!"} + + +@needs_py39 +def test_root_with_no_token(client: TestClient): + response = client.get("/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_put_no_header(client: TestClient): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_put_invalid_header(client: TestClient): + response = client.put("/items/foo", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_put(client: TestClient): + response = client.put( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} + + +@needs_py39 +def test_put_forbidden(client: TestClient): + response = client.put( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 403, response.text + assert response.json() == {"detail": "You can only update the item: plumbus"} + + +@needs_py39 +def test_admin(client: TestClient): + response = client.post( + "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin getting schwifty"} + + +@needs_py39 +def test_admin_invalid_header(client: TestClient): + response = client.post("/admin/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index 65cdc758ad..2476b773f4 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -1,217 +1,268 @@ from unittest.mock import patch import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.body.tutorial001 import app - -client = TestClient(app) - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from fastapi.utils import match_pydantic_error_url -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema +@pytest.fixture +def client(): + from docs_src.body.tutorial001 import app + + client = TestClient(app) + return client -price_missing = { - "detail": [ +def test_body_float(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + } + + +def test_post_with_str_float(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": "50.5"}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + } + + +def test_post_with_str_float_description(client: TestClient): + response = client.post( + "/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"} + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": None, + } + + +def test_post_with_str_float_description_tax(client: TestClient): + response = client.post( + "/items/", + json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.3, + } + + +def test_post_with_only_name(client: TestClient): + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {"name": "Foo"}, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} - -price_not_float = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", + "detail": [ + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + } + ] } - ] -} + ) -name_price_missing = { - "detail": [ + +def test_post_with_only_name_price(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": "twenty"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - }, + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "twenty", + "url": match_pydantic_error_url("float_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -body_missing = { - "detail": [ - {"loc": ["body"], "msg": "field required", "type": "value_error.missing"} - ] -} + "detail": [ + { + "loc": ["body", "price"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] + } + ) -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/", - {"name": "Foo", "price": 50.5}, - 200, - {"name": "Foo", "price": 50.5, "description": None, "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5"}, - 200, - {"name": "Foo", "price": 50.5, "description": None, "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5", "description": "Some Foo"}, - 200, - {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, - 200, - {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3}, - ), - ("/items/", {"name": "Foo"}, 422, price_missing), - ("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float), - ("/items/", {}, 422, name_price_missing), - ("/items/", None, 422, body_missing), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.post(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_with_no_data(client: TestClient): + response = client.post("/items/", json={}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_broken_body(): +def test_post_with_none(client: TestClient): + response = client.post("/items/", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_broken_body(client: TestClient): response = client.post( "/items/", headers={"content-type": "application/json"}, content="{some broken json}", ) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", 1], - "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", - "type": "value_error.jsondecode", - "ctx": { - "msg": "Expecting property name enclosed in double quotes", - "doc": "{some broken json}", - "pos": 1, - "lineno": 1, - "colno": 2, - }, - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "json_invalid", + "loc": ["body", 1], + "msg": "JSON decode error", + "input": {}, + "ctx": { + "error": "Expecting property name enclosed in double quotes" + }, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", 1], + "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", + "type": "value_error.jsondecode", + "ctx": { + "msg": "Expecting property name enclosed in double quotes", + "doc": "{some broken json}", + "pos": 1, + "lineno": 1, + "colno": 2, + }, + } + ] + } + ) -def test_post_form_for_json(): +def test_post_form_for_json(client: TestClient): response = client.post("/items/", data={"name": "Foo", "price": 50.5}) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": "name=Foo&price=50.5", + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) -def test_explicit_content_type(): +def test_explicit_content_type(client: TestClient): response = client.post( "/items/", content='{"name": "Foo", "price": 50.5}', @@ -220,7 +271,7 @@ def test_explicit_content_type(): assert response.status_code == 200, response.text -def test_geo_json(): +def test_geo_json(client: TestClient): response = client.post( "/items/", content='{"name": "Foo", "price": 50.5}', @@ -229,7 +280,7 @@ def test_geo_json(): assert response.status_code == 200, response.text -def test_no_content_type_is_json(): +def test_no_content_type_is_json(client: TestClient): response = client.post( "/items/", content='{"name": "Foo", "price": 50.5}', @@ -243,37 +294,199 @@ def test_no_content_type_is_json(): } -def test_wrong_headers(): +def test_wrong_headers(client: TestClient): data = '{"name": "Foo", "price": 50.5}' - invalid_dict = { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - response = client.post( "/items/", content=data, headers={"Content-Type": "text/plain"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url( + "model_attributes_type" + ), # "https://errors.pydantic.dev/0.38.0/v/dict_attributes_type", + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) response = client.post( "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) response = client.post( "/items/", content=data, headers={"Content-Type": "application/not-really-json"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) -def test_other_exceptions(): +def test_other_exceptions(client: TestClient): with patch("json.loads", side_effect=Exception): response = client.post("/items/", json={"test": "test2"}) assert response.status_code == 400, response.text + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body/test_tutorial001_py310.py b/tests/test_tutorial/test_body/test_tutorial001_py310.py index 83bcb68f30..b64d860053 100644 --- a/tests/test_tutorial/test_body/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body/test_tutorial001_py310.py @@ -1,87 +1,12 @@ from unittest.mock import patch import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture def client(): @@ -92,92 +17,188 @@ def client(): @needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -price_missing = { - "detail": [ - { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - -price_not_float = { - "detail": [ - { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] -} - -name_price_missing = { - "detail": [ - { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -body_missing = { - "detail": [ - {"loc": ["body"], "msg": "field required", "type": "value_error.missing"} - ] -} +def test_body_float(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + } @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/", - {"name": "Foo", "price": 50.5}, - 200, - {"name": "Foo", "price": 50.5, "description": None, "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5"}, - 200, - {"name": "Foo", "price": 50.5, "description": None, "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5", "description": "Some Foo"}, - 200, - {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, - 200, - {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3}, - ), - ("/items/", {"name": "Foo"}, 422, price_missing), - ("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float), - ("/items/", {}, 422, name_price_missing), - ("/items/", None, 422, body_missing), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.post(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_with_str_float(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": "50.5"}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + } + + +@needs_py310 +def test_post_with_str_float_description(client: TestClient): + response = client.post( + "/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"} + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": None, + } + + +@needs_py310 +def test_post_with_str_float_description_tax(client: TestClient): + response = client.post( + "/items/", + json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.3, + } + + +@needs_py310 +def test_post_with_only_name(client: TestClient): + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {"name": "Foo"}, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py310 +def test_post_with_only_name_price(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": "twenty"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "twenty", + "url": match_pydantic_error_url("float_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "price"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] + } + ) + + +@needs_py310 +def test_post_with_no_data(client: TestClient): + response = client.post("/items/", json={}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py310 +def test_post_with_none(client: TestClient): + response = client.post("/items/", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py310 @@ -188,37 +209,69 @@ def test_post_broken_body(client: TestClient): content="{some broken json}", ) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", 1], - "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", - "type": "value_error.jsondecode", - "ctx": { - "msg": "Expecting property name enclosed in double quotes", - "doc": "{some broken json}", - "pos": 1, - "lineno": 1, - "colno": 2, - }, - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "json_invalid", + "loc": ["body", 1], + "msg": "JSON decode error", + "input": {}, + "ctx": { + "error": "Expecting property name enclosed in double quotes" + }, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", 1], + "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", + "type": "value_error.jsondecode", + "ctx": { + "msg": "Expecting property name enclosed in double quotes", + "doc": "{some broken json}", + "pos": 1, + "lineno": 1, + "colno": 2, + }, + } + ] + } + ) @needs_py310 def test_post_form_for_json(client: TestClient): response = client.post("/items/", data={"name": "Foo", "price": 50.5}) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": "name=Foo&price=50.5", + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) @needs_py310 @@ -259,32 +312,91 @@ def test_no_content_type_is_json(client: TestClient): @needs_py310 def test_wrong_headers(client: TestClient): data = '{"name": "Foo", "price": 50.5}' - invalid_dict = { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - response = client.post( "/items/", content=data, headers={"Content-Type": "text/plain"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) response = client.post( "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) response = client.post( "/items/", content=data, headers={"Content-Type": "application/not-really-json"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) @needs_py310 @@ -292,3 +404,105 @@ def test_other_exceptions(client: TestClient): with patch("json.loads", side_effect=Exception): response = client.post("/items/", json={"test": "test2"}) assert response.status_code == 400, response.text + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001.py b/tests/test_tutorial/test_body_fields/test_tutorial001.py index fe5a270f36..1ff2d95760 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -1,169 +1,205 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.body_fields.tutorial001 import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001 import app + + client = TestClient(app) + return client + + +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } + + +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - "422": { - "description": "Validation Error", + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" } } }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, - "price": { - "title": "Price", - "exclusiveMinimum": 0.0, - "type": "number", - "description": "The price must be greater than zero", - }, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item"], - "type": "object", - "properties": {"item": {"$ref": "#/components/schemas/Item"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -price_not_greater = { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} - - -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - {"item": {"name": "Foo", "price": 3.0}}, - 200, - { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - }, - ), - ( - "/items/6", - { - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", - } - }, - 200, - { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - }, - ), - ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), - ], -) -def test(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + }, + } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py new file mode 100644 index 0000000000..907d6842a4 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py @@ -0,0 +1,205 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_an import app + + client = TestClient(app) + return client + + +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } + + +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py new file mode 100644 index 0000000000..431d2d1819 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py @@ -0,0 +1,211 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } + + +@needs_py310 +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +@needs_py310 +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py new file mode 100644 index 0000000000..8cef6c154c --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py @@ -0,0 +1,211 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } + + +@needs_py39 +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +@needs_py39 +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py index 993e2a91d2..b48cd9ec26 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py @@ -1,110 +1,10 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, - "price": { - "title": "Price", - "exclusiveMinimum": 0.0, - "type": "number", - "description": "The price must be greater than zero", - }, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item"], - "type": "object", - "properties": {"item": {"$ref": "#/components/schemas/Item"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -115,62 +15,197 @@ def get_client(): @needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -price_not_greater = { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - {"item": {"name": "Foo", "price": 3.0}}, - 200, - { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - }, - ), - ( - "/items/6", - { - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +@needs_py310 +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), } - }, - 200, - { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, }, - }, - ), - ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), - ], -) -def test(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index 8dc710d755..e5dc13b268 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -1,147 +1,208 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.body_multiple_params.tutorial001 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001 import app -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "maximum": 1000.0, - "minimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - } - }, - } + client = TestClient(app) + return client + + +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } + + +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] } - }, -} + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -item_id_not_int = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] -} - - -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5?q=bar", - {"name": "Foo", "price": 50.5}, - 200, - { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, }, - "q": "bar", - }, - ), - ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), - ("/items/5", None, 200, {"item_id": 5}), - ("/items/foo", None, 422, item_id_not_int), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py new file mode 100644 index 0000000000..51e8e3a4e9 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py @@ -0,0 +1,208 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001_an import app + + client = TestClient(app) + return client + + +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } + + +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py new file mode 100644 index 0000000000..8ac1f72618 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py @@ -0,0 +1,215 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } + + +@needs_py310 +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +@needs_py310 +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +@needs_py310 +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py new file mode 100644 index 0000000000..7ada42c528 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py @@ -0,0 +1,215 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } + + +@needs_py39 +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +@needs_py39 +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +@needs_py39 +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py index 5114ccea29..0a832eaf6f 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py @@ -1,103 +1,10 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "maximum": 1000.0, - "minimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - } - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -108,48 +15,201 @@ def get_client(): @needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -item_id_not_int = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] -} +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5?q=bar", - {"name": "Foo", "price": 50.5}, - 200, - { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +@needs_py310 +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +@needs_py310 +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, }, - "q": "bar", - }, - ), - ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), - ("/items/5", None, 200, {"item_id": 5}), - ("/items/foo", None, 422, item_id_not_int), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py index 64aa9c43bf..2046579a94 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -1,198 +1,280 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.body_multiple_params.tutorial003 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003 import app -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + client = TestClient(app) + return client + + +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - "422": { - "description": "Validation Error", + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" } } }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -# Test required and embedded body parameters with no bodies sent -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - { - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - 200, - { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - }, - ), - ( - "/items/5", - None, - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ( - "/items/5", - [], - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py new file mode 100644 index 0000000000..1282483e07 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py @@ -0,0 +1,280 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_an import app + + client = TestClient(app) + return client + + +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py new file mode 100644 index 0000000000..577c079d00 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py @@ -0,0 +1,286 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +@needs_py310 +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py310 +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py new file mode 100644 index 0000000000..0ec04151cc --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py @@ -0,0 +1,286 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +@needs_py39 +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py index fc019d8bb4..9caf5fe6cb 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py @@ -1,114 +1,10 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -118,89 +14,273 @@ def get_client(): return client +@needs_py310 +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +@needs_py310 +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py310 +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + @needs_py310 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -# Test required and embedded body parameters with no bodies sent -@needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - { - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - 200, - { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, }, - "user": {"username": "Dave", "full_name": None}, - }, - ), - ( - "/items/5", - None, - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - ] - }, - ), - ( - "/items/5", - [], - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py index c56d41b5bf..f4a76be449 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -1,103 +1,125 @@ +import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.body_nested_models.tutorial009 import app - -client = TestClient(app) - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/index-weights/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Index Weights", - "operationId": "create_index_weights_index_weights__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Weights", - "type": "object", - "additionalProperties": {"type": "number"}, - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from fastapi.utils import match_pydantic_error_url -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_nested_models.tutorial009 import app + + client = TestClient(app) + return client -def test_post_body(): +def test_post_body(client: TestClient): data = {"2": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) assert response.status_code == 200, response.text assert response.json() == data -def test_post_invalid_body(): +def test_post_invalid_body(client: TestClient): data = {"foo": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "foo", "[key]"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "__key__"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text assert response.json() == { - "detail": [ - { - "loc": ["body", "__key__"], - "msg": "value is not a valid integer", - "type": "type_error.integer", + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/index-weights/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Index Weights", + "operationId": "create_index_weights_index_weights__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Weights", + "type": "object", + "additionalProperties": {"type": "number"}, + } + } + }, + "required": True, + }, + } } - ] + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py index 5b8d828612..8ab9bcac83 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py @@ -1,78 +1,10 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/index-weights/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Index Weights", - "operationId": "create_index_weights_index_weights__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Weights", - "type": "object", - "additionalProperties": {"type": "number"}, - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,13 +14,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_post_body(client: TestClient): data = {"2": 2.2, "3": 3.3} @@ -102,12 +27,104 @@ def test_post_invalid_body(client: TestClient): data = {"foo": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "foo", "[key]"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "__key__"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text assert response.json() == { - "detail": [ - { - "loc": ["body", "__key__"], - "msg": "value is not a valid integer", - "type": "type_error.integer", + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/index-weights/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Index Weights", + "operationId": "create_index_weights_index_weights__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Weights", + "type": "object", + "additionalProperties": {"type": "number"}, + } + } + }, + "required": True, + }, + } } - ] + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index efd0e46765..e586534a07 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -1,143 +1,18 @@ +import pytest from fastapi.testclient import TestClient -from docs_src.body_updates.tutorial001 import app - -client = TestClient(app) - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from ...utils import needs_pydanticv1, needs_pydanticv2 -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_updates.tutorial001 import app + + client = TestClient(app) + return client -def test_get(): +def test_get(client: TestClient): response = client.get("/items/baz") assert response.status_code == 200, response.text assert response.json() == { @@ -149,7 +24,7 @@ def test_get(): } -def test_put(): +def test_put(client: TestClient): response = client.put( "/items/bar", json={"name": "Barz", "price": 3, "description": None} ) @@ -160,3 +35,279 @@ def test_put(): "tax": 10.5, "tags": [], } + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "type": "object", + "title": "Item", + "properties": { + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Price", + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py index 49279b3206..6bc969d43a 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py @@ -1,133 +1,7 @@ import pytest from fastapi.testclient import TestClient -from ...utils import needs_py310 - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 @pytest.fixture(name="client") @@ -138,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_get(client: TestClient): response = client.get("/items/baz") @@ -170,3 +37,281 @@ def test_put(client: TestClient): "tax": 10.5, "tags": [], } + + +@needs_py310 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "type": "object", + "title": "Item", + "properties": { + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Price", + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_py310 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py index 872530bcf9..a1edb33707 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py @@ -1,133 +1,7 @@ import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from ...utils import needs_py39, needs_pydanticv1, needs_pydanticv2 @pytest.fixture(name="client") @@ -138,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_get(client: TestClient): response = client.get("/items/baz") @@ -170,3 +37,281 @@ def test_put(client: TestClient): "tax": 10.5, "tags": [], } + + +@needs_py39 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "type": "object", + "title": "Item", + "properties": { + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Price", + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_py39 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py index 93c8775ce5..b098f259c2 100644 --- a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py @@ -2,42 +2,23 @@ import importlib from fastapi.testclient import TestClient -from docs_src.conditional_openapi import tutorial001 - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "summary": "Root", - "operationId": "root__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} +from ...utils import needs_pydanticv2 -def test_default_openapi(): +def get_client() -> TestClient: + from docs_src.conditional_openapi import tutorial001 + + importlib.reload(tutorial001) + client = TestClient(tutorial001.app) - response = client.get("/openapi.json") - assert response.json() == openapi_schema - response = client.get("/docs") - assert response.status_code == 200, response.text - response = client.get("/redoc") - assert response.status_code == 200, response.text + return client +@needs_pydanticv2 def test_disable_openapi(monkeypatch): monkeypatch.setenv("OPENAPI_URL", "") - importlib.reload(tutorial001) - client = TestClient(tutorial001.app) + # Load the client after setting the env var + client = get_client() response = client.get("/openapi.json") assert response.status_code == 404, response.text response = client.get("/docs") @@ -46,8 +27,37 @@ def test_disable_openapi(monkeypatch): assert response.status_code == 404, response.text +@needs_pydanticv2 def test_root(): - client = TestClient(tutorial001.app) + client = get_client() response = client.get("/") assert response.status_code == 200 assert response.json() == {"message": "Hello World"} + + +@needs_pydanticv2 +def test_default_openapi(): + client = get_client() + response = client.get("/docs") + assert response.status_code == 200, response.text + response = client.get("/redoc") + assert response.status_code == 200, response.text + response = client.get("/openapi.json") + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/docs/ru/overrides/.gitignore b/tests/test_tutorial/test_configure_swagger_ui/__init__.py similarity index 100% rename from docs/ru/overrides/.gitignore rename to tests/test_tutorial/test_configure_swagger_ui/__init__.py diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial003.py b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial001.py similarity index 95% rename from tests/test_tutorial/test_extending_openapi/test_tutorial003.py rename to tests/test_tutorial/test_configure_swagger_ui/test_tutorial001.py index 0184dd9f83..72db54bd20 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial003.py +++ b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.extending_openapi.tutorial003 import app +from docs_src.configure_swagger_ui.tutorial001 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial004.py b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py similarity index 96% rename from tests/test_tutorial/test_extending_openapi/test_tutorial004.py rename to tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py index 4f7615126a..1669011885 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial004.py +++ b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.extending_openapi.tutorial004 import app +from docs_src.configure_swagger_ui.tutorial002 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial005.py b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial003.py similarity index 96% rename from tests/test_tutorial/test_extending_openapi/test_tutorial005.py rename to tests/test_tutorial/test_configure_swagger_ui/test_tutorial003.py index 24aeb93db3..187e89ace0 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial005.py +++ b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial003.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.extending_openapi.tutorial005 import app +from docs_src.configure_swagger_ui.tutorial003 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index 38ae211db3..7d0e669aba 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -1,79 +1,13 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.cookie_params.tutorial001 import app -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Ads Id", "type": "string"}, - "name": "ads_id", - "in": "cookie", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.mark.parametrize( "path,cookies,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"ads_id": None}), ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), ( @@ -90,3 +24,85 @@ def test(path, cookies, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py new file mode 100644 index 0000000000..2505876c87 --- /dev/null +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py @@ -0,0 +1,108 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.cookie_params.tutorial001_an import app + + +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), + ], +) +def test(path, cookies, expected_status, expected_response): + client = TestClient(app, cookies=cookies) + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py new file mode 100644 index 0000000000..108f78b9c8 --- /dev/null +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py @@ -0,0 +1,114 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@needs_py310 +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), + ], +) +def test(path, cookies, expected_status, expected_response): + from docs_src.cookie_params.tutorial001_an_py310 import app + + client = TestClient(app, cookies=cookies) + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(): + from docs_src.cookie_params.tutorial001_an_py310 import app + + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py new file mode 100644 index 0000000000..8126a10523 --- /dev/null +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py @@ -0,0 +1,114 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@needs_py39 +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), + ], +) +def test(path, cookies, expected_status, expected_response): + from docs_src.cookie_params.tutorial001_an_py39 import app + + client = TestClient(app, cookies=cookies) + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(): + from docs_src.cookie_params.tutorial001_an_py39 import app + + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py index 5ad52fb5e1..6711fa5818 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py @@ -1,80 +1,14 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Ads Id", "type": "string"}, - "name": "ads_id", - "in": "cookie", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @needs_py310 @pytest.mark.parametrize( "path,cookies,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"ads_id": None}), ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), ( @@ -93,3 +27,88 @@ def test(path, cookies, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(): + from docs_src.cookie_params.tutorial001_py310 import app + + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/docs/sq/overrides/.gitignore b/tests/test_tutorial/test_custom_docs_ui/__init__.py similarity index 100% rename from docs/sq/overrides/.gitignore rename to tests/test_tutorial/test_custom_docs_ui/__init__.py diff --git a/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py b/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py new file mode 100644 index 0000000000..34a18b12ca --- /dev/null +++ b/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py @@ -0,0 +1,44 @@ +import os +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture(scope="module") +def client(): + static_dir: Path = Path(os.getcwd()) / "static" + print(static_dir) + static_dir.mkdir(exist_ok=True) + from docs_src.custom_docs_ui.tutorial001 import app + + with TestClient(app) as client: + yield client + static_dir.rmdir() + + +def test_swagger_ui_html(client: TestClient): + response = client.get("/docs") + assert response.status_code == 200, response.text + assert ( + "https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js" in response.text + ) + assert "https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" in response.text + + +def test_swagger_ui_oauth2_redirect_html(client: TestClient): + response = client.get("/docs/oauth2-redirect") + assert response.status_code == 200, response.text + assert "window.opener.swaggerUIRedirectOauth2" in response.text + + +def test_redoc_html(client: TestClient): + response = client.get("/redoc") + assert response.status_code == 200, response.text + assert "https://unpkg.com/redoc@next/bundles/redoc.standalone.js" in response.text + + +def test_api(client: TestClient): + response = client.get("/users/john") + assert response.status_code == 200, response.text + assert response.json()["message"] == "Hello john" diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial002.py b/tests/test_tutorial/test_custom_docs_ui/test_tutorial002.py similarity index 95% rename from tests/test_tutorial/test_extending_openapi/test_tutorial002.py rename to tests/test_tutorial/test_custom_docs_ui/test_tutorial002.py index 654db2e4c8..712618807c 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial002.py +++ b/tests/test_tutorial/test_custom_docs_ui/test_tutorial002.py @@ -10,7 +10,7 @@ def client(): static_dir: Path = Path(os.getcwd()) / "static" print(static_dir) static_dir.mkdir(exist_ok=True) - from docs_src.extending_openapi.tutorial002 import app + from docs_src.custom_docs_ui.tutorial002 import app with TestClient(app) as client: yield client diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py index d2d27f8a27..ad142ec887 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.custom_request_and_route.tutorial002 import app @@ -12,16 +14,33 @@ def test_endpoint_works(): def test_exception_handler_body_access(): response = client.post("/", json={"numbers": [1, 2, 3]}) - - assert response.json() == { - "detail": { - "body": '{"numbers": [1, 2, 3]}', - "errors": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ], + assert response.json() == IsDict( + { + "detail": { + "errors": [ + { + "type": "list_type", + "loc": ["body"], + "msg": "Input should be a valid list", + "input": {"numbers": [1, 2, 3]}, + "url": match_pydantic_error_url("list_type"), + } + ], + "body": '{"numbers": [1, 2, 3]}', + } } - } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": { + "body": '{"numbers": [1, 2, 3]}', + "errors": [ + { + "loc": ["body"], + "msg": "value is not a valid list", + "type": "type_error.list", + } + ], + } + } + ) diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index 430076f88f..fc83624673 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -4,33 +4,31 @@ from docs_src.custom_response.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_custom_response(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001b.py b/tests/test_tutorial/test_custom_response/test_tutorial001b.py index 0f15d5f48c..91e5c501e3 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001b.py @@ -4,33 +4,31 @@ from docs_src.custom_response.tutorial001b import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_custom_response(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial004.py b/tests/test_tutorial/test_custom_response/test_tutorial004.py index 5d75cce960..de60574f5e 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial004.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial004.py @@ -4,24 +4,6 @@ from docs_src.custom_response.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"text/html": {"schema": {"type": "string"}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, -} html_contents = """ <html> @@ -35,13 +17,30 @@ html_contents = """ """ -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_get_custom_response(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.text == html_contents + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"text/html": {"schema": {"type": "string"}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial005.py b/tests/test_tutorial/test_custom_response/test_tutorial005.py index ecf6ee2b98..889bf3e929 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial005.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial005.py @@ -4,33 +4,31 @@ from docs_src.custom_response.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "summary": "Main", - "operationId": "main__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"text/plain": {"schema": {"type": "string"}}}, - } - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get(): response = client.get("/") assert response.status_code == 200, response.text assert response.text == "Hello World" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Main", + "operationId": "main__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"text/plain": {"schema": {"type": "string"}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006.py b/tests/test_tutorial/test_custom_response/test_tutorial006.py index 9b10916e58..2d0a2cd3f6 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006.py @@ -5,33 +5,30 @@ from docs_src.custom_response.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/typer": { - "get": { - "summary": "Redirect Typer", - "operationId": "redirect_typer_typer_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} +def test_get(): + response = client.get("/typer", follow_redirects=False) + assert response.status_code == 307, response.text + assert response.headers["location"] == "https://typer.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_get(): - response = client.get("/typer", follow_redirects=False) - assert response.status_code == 307, response.text - assert response.headers["location"] == "https://typer.tiangolo.com" + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/typer": { + "get": { + "summary": "Redirect Typer", + "operationId": "redirect_typer_typer_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006b.py b/tests/test_tutorial/test_custom_response/test_tutorial006b.py index b3e60e86a3..1739fd4570 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006b.py @@ -5,28 +5,25 @@ from docs_src.custom_response.tutorial006b import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/fastapi": { - "get": { - "summary": "Redirect Fastapi", - "operationId": "redirect_fastapi_fastapi_get", - "responses": {"307": {"description": "Successful Response"}}, - } - } - }, -} +def test_redirect_response_class(): + response = client.get("/fastapi", follow_redirects=False) + assert response.status_code == 307 + assert response.headers["location"] == "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_redirect_response_class(): - response = client.get("/fastapi", follow_redirects=False) - assert response.status_code == 307 - assert response.headers["location"] == "https://fastapi.tiangolo.com" + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/fastapi": { + "get": { + "summary": "Redirect Fastapi", + "operationId": "redirect_fastapi_fastapi_get", + "responses": {"307": {"description": "Successful Response"}}, + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006c.py b/tests/test_tutorial/test_custom_response/test_tutorial006c.py index 0cb6ddaa33..51aa1833de 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006c.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006c.py @@ -5,28 +5,25 @@ from docs_src.custom_response.tutorial006c import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/pydantic": { - "get": { - "summary": "Redirect Pydantic", - "operationId": "redirect_pydantic_pydantic_get", - "responses": {"302": {"description": "Successful Response"}}, - } - } - }, -} +def test_redirect_status_code(): + response = client.get("/pydantic", follow_redirects=False) + assert response.status_code == 302 + assert response.headers["location"] == "https://pydantic-docs.helpmanual.io/" def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_redirect_status_code(): - response = client.get("/pydantic", follow_redirects=False) - assert response.status_code == 302 - assert response.headers["location"] == "https://pydantic-docs.helpmanual.io/" + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/pydantic": { + "get": { + "summary": "Redirect Pydantic", + "operationId": "redirect_pydantic_pydantic_get", + "responses": {"302": {"description": "Successful Response"}}, + } + } + }, + } diff --git a/docs/sv/overrides/.gitignore b/tests/test_tutorial/test_dataclasses/__init__.py similarity index 100% rename from docs/sv/overrides/.gitignore rename to tests/test_tutorial/test_dataclasses/__init__.py diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index bf15641949..9f1200f373 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -1,92 +1,11 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dataclasses.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == openapi_schema - def test_post_item(): response = client.post("/items/", json={"name": "Foo", "price": 3}) @@ -102,12 +21,128 @@ def test_post_item(): def test_post_invalid_item(): response = client.post("/items/", json={"name": "Foo", "price": "invalid price"}) assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "invalid price", + "url": match_pydantic_error_url("float_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "price"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] + } + ) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 assert response.json() == { - "detail": [ - { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } } - ] + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, } diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 34aeb0be5f..4146f4cd65 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -1,71 +1,10 @@ -from copy import deepcopy - +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/next": { - "get": { - "summary": "Read Next Item", - "operationId": "read_next_item_items_next_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - } - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - }, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - # TODO: remove this once Pydantic 1.9 is released - # Ref: https://github.com/samuelcolvin/pydantic/pull/2557 - data = response.json() - alternative_data1 = deepcopy(data) - alternative_data2 = deepcopy(data) - alternative_data1["components"]["schemas"]["Item"]["required"] = ["name", "price"] - alternative_data2["components"]["schemas"]["Item"]["required"] = [ - "name", - "price", - "tags", - ] - assert alternative_data1 == openapi_schema or alternative_data2 == openapi_schema - def test_get_item(): response = client.get("/items/next") @@ -77,3 +16,82 @@ def test_get_item(): "tags": ["breater"], "tax": None, } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/next": { + "get": { + "summary": "Read Next Item", + "operationId": "read_next_item_items_next_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": IsOneOf( + ["name", "price", "tags", "description", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tags": IsDict( + { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + } + ), + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index 2d86f7b9ab..dd0e36735e 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -2,138 +2,10 @@ from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial003 import app +from ...utils import needs_pydanticv1, needs_pydanticv2 + client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/authors/{author_id}/items/": { - "post": { - "summary": "Create Author Items", - "operationId": "create_author_items_authors__author_id__items__post", - "parameters": [ - { - "required": True, - "schema": {"title": "Author Id", "type": "string"}, - "name": "author_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Author"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/authors/": { - "get": { - "summary": "Get Authors", - "operationId": "get_authors_authors__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Get Authors Authors Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Author"}, - } - } - }, - } - }, - } - }, - }, - "components": { - "schemas": { - "Author": { - "title": "Author", - "required": ["name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - }, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == openapi_schema - def test_post_authors_item(): response = client.post( @@ -179,3 +51,273 @@ def test_get_authors(): ], }, ] + + +@needs_pydanticv2 +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/authors/{author_id}/items/": { + "post": { + "summary": "Create Author Items", + "operationId": "create_author_items_authors__author_id__items__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Author Id", "type": "string"}, + "name": "author_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Author"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/authors/": { + "get": { + "summary": "Get Authors", + "operationId": "get_authors_authors__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Authors Authors Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Author" + }, + } + } + }, + } + }, + } + }, + }, + "components": { + "schemas": { + "Author": { + "title": "Author", + "required": ["name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + }, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema_pv1(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/authors/{author_id}/items/": { + "post": { + "summary": "Create Author Items", + "operationId": "create_author_items_authors__author_id__items__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Author Id", "type": "string"}, + "name": "author_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Author"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/authors/": { + "get": { + "summary": "Get Authors", + "operationId": "get_authors_authors__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Authors Authors Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Author" + }, + } + } + }, + } + }, + } + }, + }, + "components": { + "schemas": { + "Author": { + "title": "Author", + "required": ["name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + }, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial001.py index c3bca5d5b3..d1324a6411 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001.py @@ -1,136 +1,11 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dependencies.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -140,10 +15,169 @@ def test_openapi_schema(): ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), ("/users", 200, {"q": None, "skip": 0, "limit": 100}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py new file mode 100644 index 0000000000..79c2a1e10e --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py @@ -0,0 +1,183 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial001_an import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ], +) +def test_get(path, expected_status, expected_response): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py new file mode 100644 index 0000000000..7db55a1c55 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py @@ -0,0 +1,191 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py new file mode 100644 index 0000000000..68c2dedb1b --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py @@ -0,0 +1,191 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py index 32a61c8219..381eecb639 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py @@ -1,128 +1,9 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -132,13 +13,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -148,10 +22,170 @@ def test_openapi_schema(client: TestClient): ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), ("/users", 200, {"q": None, "skip": 0, "limit": 100}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial004.py index f2b1878d54..5c5d34cfcc 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004.py @@ -1,94 +1,11 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dependencies.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -142,3 +59,104 @@ def test_get(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py new file mode 100644 index 0000000000..c5c1a1fb88 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py @@ -0,0 +1,162 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial004_an import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ( + "/items", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ] + }, + ), + ( + "/items?q=foo", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ], + "q": "foo", + }, + ), + ( + "/items?q=foo&skip=1", + 200, + {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, + ), + ( + "/items?q=bar&limit=2", + 200, + {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?q=bar&skip=1&limit=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?limit=1&q=bar&skip=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ], +) +def test_get(path, expected_status, expected_response): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py new file mode 100644 index 0000000000..6fd093ddb1 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py @@ -0,0 +1,170 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial004_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ( + "/items", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ] + }, + ), + ( + "/items?q=foo", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ], + "q": "foo", + }, + ), + ( + "/items?q=foo&skip=1", + 200, + {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, + ), + ( + "/items?q=bar&limit=2", + 200, + {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?q=bar&skip=1&limit=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?limit=1&q=bar&skip=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py new file mode 100644 index 0000000000..fbbe84cc94 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py @@ -0,0 +1,170 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial004_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ( + "/items", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ] + }, + ), + ( + "/items?q=foo", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ], + "q": "foo", + }, + ), + ( + "/items?q=foo&skip=1", + 200, + {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, + ), + ( + "/items?q=bar&limit=2", + 200, + {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?q=bar&skip=1&limit=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?limit=1&q=bar&skip=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py index e3ae0c7418..845b098e79 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py @@ -1,86 +1,9 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -90,13 +13,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -150,3 +66,105 @@ def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py index 2916577a2a..704e389a5b 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -1,105 +1,51 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dependencies.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_no_headers(): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_invalid_one_header(): @@ -126,3 +72,81 @@ def test_get_valid_headers(): ) assert response.status_code == 200, response.text assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py new file mode 100644 index 0000000000..5034fceba5 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py @@ -0,0 +1,152 @@ +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from docs_src.dependencies.tutorial006_an import app + +client = TestClient(app) + + +def test_get_no_headers(): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_get_invalid_one_header(): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_get_invalid_second_header(): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +def test_get_valid_headers(): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py new file mode 100644 index 0000000000..3fc22dd3c2 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py @@ -0,0 +1,164 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial006_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_get_no_headers(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_get_invalid_one_header(client: TestClient): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_get_invalid_second_header(client: TestClient): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +@needs_py39 +def test_get_valid_headers(client: TestClient): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b.py b/tests/test_tutorial/test_dependencies/test_tutorial008b.py new file mode 100644 index 0000000000..86acba9e4f --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial008b.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial008b import app + +client = TestClient(app) + + +def test_get_no_item(): + response = client.get("/items/foo") + assert response.status_code == 404, response.text + assert response.json() == {"detail": "Item not found"} + + +def test_owner_error(): + response = client.get("/items/plumbus") + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Owner error: Rick"} + + +def test_get_item(): + response = client.get("/items/portal-gun") + assert response.status_code == 200, response.text + assert response.json() == {"description": "Gun to create portals", "owner": "Rick"} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py b/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py new file mode 100644 index 0000000000..7f51fc52a5 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial008b_an import app + +client = TestClient(app) + + +def test_get_no_item(): + response = client.get("/items/foo") + assert response.status_code == 404, response.text + assert response.json() == {"detail": "Item not found"} + + +def test_owner_error(): + response = client.get("/items/plumbus") + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Owner error: Rick"} + + +def test_get_item(): + response = client.get("/items/portal-gun") + assert response.status_code == 200, response.text + assert response.json() == {"description": "Gun to create portals", "owner": "Rick"} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py new file mode 100644 index 0000000000..7f51fc52a5 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial008b_an import app + +client = TestClient(app) + + +def test_get_no_item(): + response = client.get("/items/foo") + assert response.status_code == 404, response.text + assert response.json() == {"detail": "Item not found"} + + +def test_owner_error(): + response = client.get("/items/plumbus") + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Owner error: Rick"} + + +def test_get_item(): + response = client.get("/items/portal-gun") + assert response.status_code == 200, response.text + assert response.json() == {"description": "Gun to create portals", "owner": "Rick"} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012.py b/tests/test_tutorial/test_dependencies/test_tutorial012.py index e4e07395df..753e62e43e 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012.py @@ -1,160 +1,92 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dependencies.tutorial012 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_no_headers_items(): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_no_headers_users(): response = client.get("/users/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_invalid_one_header_items(): @@ -207,3 +139,117 @@ def test_get_valid_headers_users(): ) assert response.status_code == 200, response.text assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py new file mode 100644 index 0000000000..4157d46128 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py @@ -0,0 +1,255 @@ +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from docs_src.dependencies.tutorial012_an import app + +client = TestClient(app) + + +def test_get_no_headers_items(): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_get_no_headers_users(): + response = client.get("/users/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_get_invalid_one_header_items(): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_get_invalid_one_users(): + response = client.get("/users/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_get_invalid_second_header_items(): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +def test_get_invalid_second_header_users(): + response = client.get( + "/users/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +def test_get_valid_headers_items(): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +def test_get_valid_headers_users(): + response = client.get( + "/users/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py new file mode 100644 index 0000000000..9e46758cbd --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py @@ -0,0 +1,271 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial012_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_get_no_headers_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_get_no_headers_users(client: TestClient): + response = client.get("/users/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_get_invalid_one_header_items(client: TestClient): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_get_invalid_one_users(client: TestClient): + response = client.get("/users/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_get_invalid_second_header_items(client: TestClient): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +@needs_py39 +def test_get_invalid_second_header_users(client: TestClient): + response = client.get( + "/users/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +@needs_py39 +def test_get_valid_headers_items(client: TestClient): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +@needs_py39 +def test_get_valid_headers_users(client: TestClient): + response = client.get( + "/users/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_events/test_tutorial001.py b/tests/test_tutorial/test_events/test_tutorial001.py index d52dd1a047..f65b92d127 100644 --- a/tests/test_tutorial/test_events/test_tutorial001.py +++ b/tests/test_tutorial/test_events/test_tutorial001.py @@ -1,79 +1,92 @@ +import pytest +from fastapi import FastAPI from fastapi.testclient import TestClient -from docs_src.events.tutorial001 import app -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +@pytest.fixture(name="app", scope="module") +def get_app(): + with pytest.warns(DeprecationWarning): + from docs_src.events.tutorial001 import app + yield app -def test_events(): +def test_events(app: FastAPI): with TestClient(app) as client: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema response = client.get("/items/foo") assert response.status_code == 200, response.text assert response.json() == {"name": "Fighters"} + + +def test_openapi_schema(app: FastAPI): + with TestClient(app) as client: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_events/test_tutorial002.py b/tests/test_tutorial/test_events/test_tutorial002.py index f6ac1e07bf..137294d737 100644 --- a/tests/test_tutorial/test_events/test_tutorial002.py +++ b/tests/test_tutorial/test_events/test_tutorial002.py @@ -1,34 +1,43 @@ +import pytest +from fastapi import FastAPI from fastapi.testclient import TestClient -from docs_src.events.tutorial002 import app -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, -} +@pytest.fixture(name="app", scope="module") +def get_app(): + with pytest.warns(DeprecationWarning): + from docs_src.events.tutorial002 import app + yield app -def test_events(): +def test_events(app: FastAPI): with TestClient(app) as client: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"name": "Foo"}] with open("log.txt") as log: assert "Application shutdown" in log.read() + + +def test_openapi_schema(app: FastAPI): + with TestClient(app) as client: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_events/test_tutorial003.py b/tests/test_tutorial/test_events/test_tutorial003.py new file mode 100644 index 0000000000..0ad1a1f8b2 --- /dev/null +++ b/tests/test_tutorial/test_events/test_tutorial003.py @@ -0,0 +1,92 @@ +from fastapi.testclient import TestClient + +from docs_src.events.tutorial003 import ( + app, + fake_answer_to_everything_ml_model, + ml_models, +) + + +def test_events(): + assert not ml_models, "ml_models should be empty" + with TestClient(app) as client: + assert ml_models["answer_to_everything"] == fake_answer_to_everything_ml_model + response = client.get("/predict", params={"x": 2}) + assert response.status_code == 200, response.text + assert response.json() == {"result": 84.0} + assert not ml_models, "ml_models should be empty" + + +def test_openapi_schema(): + with TestClient(app) as client: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/predict": { + "get": { + "summary": "Predict", + "operationId": "predict_predict_get", + "parameters": [ + { + "required": True, + "schema": {"title": "X", "type": "number"}, + "name": "x", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py index ec56e9ca6a..a85a313501 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py @@ -4,41 +4,44 @@ from docs_src.extending_openapi.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": { - "title": "Custom title", - "version": "2.5.0", - "description": "This is a very custom OpenAPI schema", - "x-logo": {"url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"}, - }, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"name": "Foo"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": { + "title": "Custom title", + "summary": "This is a very custom OpenAPI schema", + "description": "Here's a longer description of the custom **OpenAPI** schema", + "version": "2.5.0", + "x-logo": { + "url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" + }, + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } + openapi_schema = response.json() + # Request again to test the custom cache + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py index 8522d7b9d7..7710446ce3 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.extra_data_types.tutorial001 import app @@ -5,118 +6,6 @@ from docs_src.extra_data_types.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - } - } - }, - } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_extra_types(): item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" data = { @@ -136,3 +25,175 @@ def test_extra_types(): response = client.put(f"/items/{item_id}", json=data) assert response.status_code == 200, response.text assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py new file mode 100644 index 0000000000..9951b3b511 --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py @@ -0,0 +1,199 @@ +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.extra_data_types.tutorial001_an import app + +client = TestClient(app) + + +def test_extra_types(): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py new file mode 100644 index 0000000000..7c482b8cb2 --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py @@ -0,0 +1,208 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_data_types.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_extra_types(client: TestClient): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py new file mode 100644 index 0000000000..87473867b0 --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py @@ -0,0 +1,208 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_data_types.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_extra_types(client: TestClient): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py index 4efdecc53a..0b71d91773 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py @@ -1,113 +1,9 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - } - } - }, - } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -117,13 +13,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_extra_types(client: TestClient): item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" @@ -144,3 +33,176 @@ def test_extra_types(client: TestClient): response = client.put(f"/items/{item_id}", json=data) assert response.status_code == 200, response.text assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py index f1433470c1..0ccb99948f 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -1,110 +1,10 @@ +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from docs_src.extra_models.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Item Items Item Id Get", - "anyOf": [ - {"$ref": "#/components/schemas/PlaneItem"}, - {"$ref": "#/components/schemas/CarItem"}, - ], - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "PlaneItem": { - "title": "PlaneItem", - "required": ["description", "size"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "plane"}, - "size": {"title": "Size", "type": "integer"}, - }, - }, - "CarItem": { - "title": "CarItem", - "required": ["description"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "car"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_car(): response = client.get("/items/item1") @@ -123,3 +23,112 @@ def test_get_plane(): "type": "plane", "size": 5, } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Item Items Item Id Get", + "anyOf": [ + {"$ref": "#/components/schemas/PlaneItem"}, + {"$ref": "#/components/schemas/CarItem"}, + ], + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "PlaneItem": { + "title": "PlaneItem", + "required": IsOneOf( + ["description", "type", "size"], + # TODO: remove when deprecating Pydantic v1 + ["description", "size"], + ), + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "plane"}, + "size": {"title": "Size", "type": "integer"}, + }, + }, + "CarItem": { + "title": "CarItem", + "required": IsOneOf( + ["description", "type"], + # TODO: remove when deprecating Pydantic v1 + ["description"], + ), + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "car"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py index 56fd83ad3a..b2fe65fd9f 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py @@ -1,103 +1,9 @@ import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Item Items Item Id Get", - "anyOf": [ - {"$ref": "#/components/schemas/PlaneItem"}, - {"$ref": "#/components/schemas/CarItem"}, - ], - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "PlaneItem": { - "title": "PlaneItem", - "required": ["description", "size"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "plane"}, - "size": {"title": "Size", "type": "integer"}, - }, - }, - "CarItem": { - "title": "CarItem", - "required": ["description"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "car"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -107,13 +13,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_get_car(client: TestClient): response = client.get("/items/item1") @@ -133,3 +32,113 @@ def test_get_plane(client: TestClient): "type": "plane", "size": 5, } + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Item Items Item Id Get", + "anyOf": [ + {"$ref": "#/components/schemas/PlaneItem"}, + {"$ref": "#/components/schemas/CarItem"}, + ], + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "PlaneItem": { + "title": "PlaneItem", + "required": IsOneOf( + ["description", "type", "size"], + # TODO: remove when deprecating Pydantic v1 + ["description", "size"], + ), + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "plane"}, + "size": {"title": "Size", "type": "integer"}, + }, + }, + "CarItem": { + "title": "CarItem", + "required": IsOneOf( + ["description", "type"], + # TODO: remove when deprecating Pydantic v1 + ["description"], + ), + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "car"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004.py b/tests/test_tutorial/test_extra_models/test_tutorial004.py index 548fb88340..71f6a8c700 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial004.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial004.py @@ -4,52 +4,6 @@ from docs_src.extra_models.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "description"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_items(): response = client.get("/items/") @@ -58,3 +12,47 @@ def test_get_items(): {"name": "Foo", "description": "There comes my hero"}, {"name": "Red", "description": "It's my aeroplane"}, ] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "description"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py index 7f4f5b9beb..5475b92e10 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py @@ -3,46 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "description"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - } - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -52,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_get_items(client: TestClient): response = client.get("/items/") @@ -67,3 +20,48 @@ def test_get_items(client: TestClient): {"name": "Foo", "description": "There comes my hero"}, {"name": "Red", "description": "It's my aeroplane"}, ] + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "description"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005.py b/tests/test_tutorial/test_extra_models/test_tutorial005.py index c3dfaa42f7..b0861c37f7 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial005.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial005.py @@ -4,41 +4,39 @@ from docs_src.extra_models.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/keyword-weights/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Keyword Weights Keyword Weights Get", - "type": "object", - "additionalProperties": {"type": "number"}, - } - } - }, - } - }, - "summary": "Read Keyword Weights", - "operationId": "read_keyword_weights_keyword_weights__get", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_items(): response = client.get("/keyword-weights/") assert response.status_code == 200, response.text assert response.json() == {"foo": 2.3, "bar": 3.4} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/keyword-weights/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Keyword Weights Keyword Weights Get", + "type": "object", + "additionalProperties": {"type": "number"}, + } + } + }, + } + }, + "summary": "Read Keyword Weights", + "operationId": "read_keyword_weights_keyword_weights__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py index 3bb5a99f10..7278e93c36 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py @@ -3,33 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/keyword-weights/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Keyword Weights Keyword Weights Get", - "type": "object", - "additionalProperties": {"type": "number"}, - } - } - }, - } - }, - "summary": "Read Keyword Weights", - "operationId": "read_keyword_weights_keyword_weights__get", - } - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -39,15 +12,40 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_get_items(client: TestClient): response = client.get("/keyword-weights/") assert response.status_code == 200, response.text assert response.json() == {"foo": 2.3, "bar": 3.4} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/keyword-weights/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Keyword Weights Keyword Weights Get", + "type": "object", + "additionalProperties": {"type": "number"}, + } + } + }, + } + }, + "summary": "Read Keyword Weights", + "operationId": "read_keyword_weights_keyword_weights__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_first_steps/test_tutorial001.py b/tests/test_tutorial/test_first_steps/test_tutorial001.py index 48d42285c8..6cc9fc2282 100644 --- a/tests/test_tutorial/test_first_steps/test_tutorial001.py +++ b/tests/test_tutorial/test_first_steps/test_tutorial001.py @@ -5,35 +5,38 @@ from docs_src.first_steps.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Root", - "operationId": "root__get", - } - } - }, -} - @pytest.mark.parametrize( "path,expected_status,expected_response", [ ("/", 200, {"message": "Hello World"}), ("/nonexistent", 404, {"detail": "Not Found"}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get_path(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Root", + "operationId": "root__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial003.py b/tests/test_tutorial/test_generate_clients/test_tutorial003.py index 128fcea309..1cd9678a1c 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial003.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial003.py @@ -4,166 +4,6 @@ from docs_src.generate_clients.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "tags": ["items"], - "summary": "Get Items", - "operationId": "items-get_items", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Items-Get Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - } - }, - }, - "post": { - "tags": ["items"], - "summary": "Create Item", - "operationId": "items-create_item", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseMessage" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/users/": { - "post": { - "tags": ["users"], - "summary": "Create User", - "operationId": "users-create_user", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseMessage" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "ResponseMessage": { - "title": "ResponseMessage", - "required": ["message"], - "type": "object", - "properties": {"message": {"title": "Message", "type": "string"}}, - }, - "User": { - "title": "User", - "required": ["username", "email"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi(): - with client: - response = client.get("/openapi.json") - - assert response.json() == openapi_schema - def test_post_items(): response = client.post("/items/", json={"name": "Foo", "price": 5}) @@ -186,3 +26,162 @@ def test_get_items(): {"name": "Plumbus", "price": 3}, {"name": "Portal Gun", "price": 9001}, ] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "items-get_items", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Items-Get Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + }, + "post": { + "tags": ["items"], + "summary": "Create Item", + "operationId": "items-create_item", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/users/": { + "post": { + "tags": ["users"], + "summary": "Create User", + "operationId": "users-create_user", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "ResponseMessage": { + "title": "ResponseMessage", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + }, + "User": { + "title": "User", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial001.py b/tests/test_tutorial/test_handling_errors/test_tutorial001.py index ffd79ccff3..8809c135bd 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial001.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial001.py @@ -4,78 +4,6 @@ from docs_src.handling_errors.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_item(): response = client.get("/items/foo") @@ -88,3 +16,75 @@ def test_get_item_not_found(): assert response.status_code == 404, response.text assert response.headers.get("x-error") is None assert response.json() == {"detail": "Item not found"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial002.py b/tests/test_tutorial/test_handling_errors/test_tutorial002.py index e678499c63..efd86ebdec 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial002.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial002.py @@ -4,78 +4,6 @@ from docs_src.handling_errors.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items-header/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Header", - "operationId": "read_item_header_items_header__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_item_header(): response = client.get("/items-header/foo") @@ -88,3 +16,75 @@ def test_get_item_not_found_header(): assert response.status_code == 404, response.text assert response.headers.get("x-error") == "There goes my error" assert response.json() == {"detail": "Item not found"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items-header/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Header", + "operationId": "read_item_header_items_header__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial003.py b/tests/test_tutorial/test_handling_errors/test_tutorial003.py index a01726dc2d..4763f68f3f 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial003.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial003.py @@ -4,78 +4,6 @@ from docs_src.handling_errors.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/unicorns/{name}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Unicorn", - "operationId": "read_unicorn_unicorns__name__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Name", "type": "string"}, - "name": "name", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get(): response = client.get("/unicorns/shinny") @@ -89,3 +17,75 @@ def test_get_exception(): assert response.json() == { "message": "Oops! yolo did something. There goes a rainbow..." } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/unicorns/{name}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Unicorn", + "operationId": "read_unicorn_unicorns__name__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Name", "type": "string"}, + "name": "name", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index 0b5f747986..217159a596 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -4,88 +4,22 @@ from docs_src.handling_errors.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 400, response.text - validation_error_str_lines = [ - b"1 validation error for Request", - b"path -> item_id", - b" value is not a valid integer (type=type_error.integer)", - ] - assert response.content == b"\n".join(validation_error_str_lines) + # TODO: remove when deprecating Pydantic v1 + assert ( + # TODO: remove when deprecating Pydantic v1 + "path -> item_id" in response.text + or "'loc': ('path', 'item_id')" in response.text + ) + assert ( + # TODO: remove when deprecating Pydantic v1 + "value is not a valid integer" in response.text + or "Input should be a valid integer, unable to parse string as an integer" + in response.text + ) def test_get_http_error(): @@ -98,3 +32,75 @@ def test_get(): response = client.get("/items/2") assert response.status_code == 200, response.text assert response.json() == {"item_id": 2} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py index 253f3d006a..494c317cab 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py @@ -1,104 +1,41 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.handling_errors.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["title", "size"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "size": {"title": "Size", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_validation_error(): response = client.post("/items/", json={"title": "towel", "size": "XL"}) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ], - "body": {"title": "towel", "size": "XL"}, - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "size"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "XL", + "url": match_pydantic_error_url("int_parsing"), + } + ], + "body": {"title": "towel", "size": "XL"}, + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "size"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ], + "body": {"title": "towel", "size": "XL"}, + } + ) def test_post(): @@ -106,3 +43,84 @@ def test_post(): response = client.post("/items/", json=data) assert response.status_code == 200, response.text assert response.json() == data + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["title", "size"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "size": {"title": "Size", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py index 21233d7bbf..cc2b496a83 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial006.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial006.py @@ -1,94 +1,39 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.handling_errors.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) def test_get_http_error(): @@ -101,3 +46,75 @@ def test_get(): response = client.get("/items/2") assert response.status_code == 200, response.text assert response.json() == {"item_id": 2} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001.py b/tests/test_tutorial/test_header_params/test_tutorial001.py index 273cf3249e..746fc05024 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial001 import app @@ -6,77 +7,9 @@ from docs_src.header_params.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "User-Agent", "type": "string"}, - "name": "user-agent", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"User-Agent": "testclient"}), ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), @@ -86,3 +19,84 @@ def test(path, headers, expected_status, expected_response): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User-Agent", "type": "string"} + ), + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an.py b/tests/test_tutorial/test_header_params/test_tutorial001_an.py new file mode 100644 index 0000000000..a715228aa2 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an.py @@ -0,0 +1,102 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial001_an import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"User-Agent": "testclient"}), + ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), + ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User-Agent", "type": "string"} + ), + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py new file mode 100644 index 0000000000..caf85bc6ca --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py @@ -0,0 +1,110 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"User-Agent": "testclient"}), + ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), + ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User-Agent", "type": "string"} + ), + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py index 77a60eb9db..57e0a296af 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py @@ -1,74 +1,9 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "User-Agent", "type": "string"}, - "name": "user-agent", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,7 +17,6 @@ def get_client(): @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"User-Agent": "testclient"}), ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), @@ -92,3 +26,85 @@ def test(path, headers, expected_status, expected_response, client: TestClient): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User-Agent", "type": "string"} + ), + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py new file mode 100644 index 0000000000..78bac838cf --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -0,0 +1,113 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial002 import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an.py b/tests/test_tutorial/test_header_params/test_tutorial002_an.py new file mode 100644 index 0000000000..ffda8158fc --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an.py @@ -0,0 +1,113 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial002_an import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py new file mode 100644 index 0000000000..6f332f3bac --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py @@ -0,0 +1,121 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial002_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py new file mode 100644 index 0000000000..8202bc671e --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py @@ -0,0 +1,124 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial002_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(): + from docs_src.header_params.tutorial002_an_py39 import app + + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py new file mode 100644 index 0000000000..c113ed23e1 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py @@ -0,0 +1,124 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial002_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(): + from docs_src.header_params.tutorial002_py310 import app + + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py new file mode 100644 index 0000000000..6f7de8ed41 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -0,0 +1,114 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial003 import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + ( + "/items", + [("x-token", "foo"), ("x-token", "bar")], + 200, + {"X-Token values": ["foo", "bar"]}, + ), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an.py b/tests/test_tutorial/test_header_params/test_tutorial003_an.py new file mode 100644 index 0000000000..742ed41f48 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an.py @@ -0,0 +1,110 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial003_an import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py new file mode 100644 index 0000000000..fdac4a416c --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py @@ -0,0 +1,118 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial003_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py new file mode 100644 index 0000000000..c50543cc88 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py @@ -0,0 +1,118 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial003_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py new file mode 100644 index 0000000000..3afb355e94 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py @@ -0,0 +1,118 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial003_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_metadata/test_tutorial001.py b/tests/test_tutorial/test_metadata/test_tutorial001.py index b7281e2935..04e8ff82b0 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial001.py +++ b/tests/test_tutorial/test_metadata/test_tutorial001.py @@ -4,47 +4,46 @@ from docs_src.metadata.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": { - "title": "ChimichangApp", - "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", - "termsOfService": "http://example.com/terms/", - "contact": { - "name": "Deadpoolio the Amazing", - "url": "http://x-force.example.com/contact/", - "email": "dp@x-force.example.com", - }, - "license": { - "name": "Apache 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0.html", - }, - "version": "0.0.1", - }, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_items(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"name": "Katana"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": { + "title": "ChimichangApp", + "summary": "Deadpool's favorite app. Nuff said.", + "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", + "termsOfService": "http://example.com/terms/", + "contact": { + "name": "Deadpoolio the Amazing", + "url": "http://x-force.example.com/contact/", + "email": "dp@x-force.example.com", + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + }, + "version": "0.0.1", + }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_metadata/test_tutorial001_1.py b/tests/test_tutorial/test_metadata/test_tutorial001_1.py new file mode 100644 index 0000000000..3efb1c4329 --- /dev/null +++ b/tests/test_tutorial/test_metadata/test_tutorial001_1.py @@ -0,0 +1,49 @@ +from fastapi.testclient import TestClient + +from docs_src.metadata.tutorial001_1 import app + +client = TestClient(app) + + +def test_items(): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [{"name": "Katana"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": { + "title": "ChimichangApp", + "summary": "Deadpool's favorite app. Nuff said.", + "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", + "termsOfService": "http://example.com/terms/", + "contact": { + "name": "Deadpoolio the Amazing", + "url": "http://x-force.example.com/contact/", + "email": "dp@x-force.example.com", + }, + "license": { + "name": "Apache 2.0", + "identifier": "MIT", + }, + "version": "0.0.1", + }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_metadata/test_tutorial004.py b/tests/test_tutorial/test_metadata/test_tutorial004.py index 2d255b8b0d..5072203712 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial004.py +++ b/tests/test_tutorial/test_metadata/test_tutorial004.py @@ -4,62 +4,60 @@ from docs_src.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 + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/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/", + }, + }, + ], + } diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index e773e7f8f5..73af420ae1 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -1,165 +1,10 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.openapi_callbacks.tutorial001 import app, invoice_notification client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/invoices/": { - "post": { - "summary": "Create Invoice", - "description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").', - "operationId": "create_invoice_invoices__post", - "parameters": [ - { - "required": False, - "schema": { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - }, - "name": "callback_url", - "in": "query", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Invoice"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "callbacks": { - "invoice_notification": { - "{$callback_url}/invoices/{$request.body.id}": { - "post": { - "summary": "Invoice Notification", - "operationId": "invoice_notification__callback_url__invoices___request_body_id__post", - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceEvent" - } - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceEventReceived" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Invoice": { - "title": "Invoice", - "required": ["id", "customer", "total"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - "customer": {"title": "Customer", "type": "string"}, - "total": {"title": "Total", "type": "number"}, - }, - }, - "InvoiceEvent": { - "title": "InvoiceEvent", - "required": ["description", "paid"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "paid": {"title": "Paid", "type": "boolean"}, - }, - }, - "InvoiceEventReceived": { - "title": "InvoiceEventReceived", - "required": ["ok"], - "type": "object", - "properties": {"ok": {"title": "Ok", "type": "boolean"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi(): - with client: - response = client.get("/openapi.json") - - assert response.json() == openapi_schema - def test_get(): response = client.post( @@ -172,3 +17,184 @@ def test_get(): def test_dummy_callback(): # Just for coverage invoice_notification({}) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/invoices/": { + "post": { + "summary": "Create Invoice", + "description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").', + "operationId": "create_invoice_invoices__post", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + "title": "Callback Url", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Callback Url", + "maxLength": 2083, + "minLength": 1, + "type": "string", + "format": "uri", + } + ), + "name": "callback_url", + "in": "query", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Invoice"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "callbacks": { + "invoice_notification": { + "{$callback_url}/invoices/{$request.body.id}": { + "post": { + "summary": "Invoice Notification", + "operationId": "invoice_notification__callback_url__invoices___request_body_id__post", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceEvent" + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceEventReceived" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Invoice": { + "title": "Invoice", + "required": ["id", "customer", "total"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "title": IsDict( + { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Title", "type": "string"} + ), + "customer": {"title": "Customer", "type": "string"}, + "total": {"title": "Total", "type": "number"}, + }, + }, + "InvoiceEvent": { + "title": "InvoiceEvent", + "required": ["description", "paid"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "paid": {"title": "Paid", "type": "boolean"}, + }, + }, + "InvoiceEventReceived": { + "title": "InvoiceEventReceived", + "required": ["ok"], + "type": "object", + "properties": {"ok": {"title": "Ok", "type": "boolean"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/docs/tr/overrides/.gitignore b/tests/test_tutorial/test_openapi_webhooks/__init__.py similarity index 100% rename from docs/tr/overrides/.gitignore rename to tests/test_tutorial/test_openapi_webhooks/__init__.py diff --git a/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py new file mode 100644 index 0000000000..dc67ec401d --- /dev/null +++ b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py @@ -0,0 +1,117 @@ +from fastapi.testclient import TestClient + +from docs_src.openapi_webhooks.tutorial001 import app + +client = TestClient(app) + + +def test_get(): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == ["Rick", "Morty"] + + +def test_dummy_webhook(): + # Just for coverage + app.webhooks.routes[0].endpoint({}) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + "webhooks": { + "new-subscription": { + "post": { + "summary": "New Subscription", + "description": "When a new user subscribes to your service we'll send you a POST request with this\ndata to the URL that you register for the event `new-subscription` in the dashboard.", + "operationId": "new_subscriptionnew_subscription_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Subscription"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Subscription": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "monthly_fee": {"type": "number", "title": "Monthly Fee"}, + "start_date": { + "type": "string", + "format": "date-time", + "title": "Start Date", + }, + }, + "type": "object", + "required": ["username", "monthly_fee", "start_date"], + "title": "Subscription", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py index 3b5301348f..95542398e4 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py @@ -4,33 +4,31 @@ from docs_src.path_operation_advanced_configuration.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "some_specific_id_you_define", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "some_specific_id_you_define", + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py index 01acb664c7..d1388c3670 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py @@ -4,33 +4,31 @@ from docs_src.path_operation_advanced_configuration.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items", + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py index 4a23db7bc1..313bb2a04a 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py @@ -4,20 +4,18 @@ from docs_src.path_operation_advanced_configuration.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": {}, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": {}, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index 3de19833be..4f69e4646c 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -2,103 +2,10 @@ from fastapi.testclient import TestClient from docs_src.path_operation_advanced_configuration.tutorial004 import app +from ...utils import needs_pydanticv1, needs_pydanticv2 + client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_query_params_str_validations(): response = client.post("/items/", json={"name": "Foo", "price": 42}) @@ -110,3 +17,202 @@ def test_query_params_str_validations(): "tax": None, "tags": [], } + + +@needs_pydanticv2 +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema_pv1(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py index 5042d18375..07e2d7d202 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py @@ -4,33 +4,31 @@ from docs_src.path_operation_advanced_configuration.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "x-aperture-labs-portal": "blue", - } - } - }, -} + +def test_get(): + response = client.get("/items/") + assert response.status_code == 200, response.text def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_get(): - response = client.get("/items/") - assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "x-aperture-labs-portal": "blue", + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py index 330b4e2c79..f92c59015e 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py @@ -4,47 +4,6 @@ from docs_src.path_operation_advanced_configuration.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"type": "string"}, - "price": {"type": "number"}, - "description": {"type": "string"}, - }, - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post(): response = client.post("/items/", content=b"this is actually not validated") @@ -57,3 +16,42 @@ def test_post(): "description": "Just kiddin', no magic here. ✨", }, } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"type": "string"}, + "price": {"type": "number"}, + "description": {"type": "string"}, + }, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py index 076f60b2f0..2d28022691 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py @@ -1,56 +1,20 @@ +import pytest from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.path_operation_advanced_configuration.tutorial007 import app - -client = TestClient(app) - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/x-yaml": { - "schema": { - "title": "Item", - "required": ["name", "tags"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - }, - }, - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} +from ...utils import needs_pydanticv2 -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema +@pytest.fixture(name="client") +def get_client(): + from docs_src.path_operation_advanced_configuration.tutorial007 import app + + client = TestClient(app) + return client -def test_post(): +@needs_pydanticv2 +def test_post(client: TestClient): yaml_data = """ name: Deadpoolio tags: @@ -66,7 +30,8 @@ def test_post(): } -def test_post_broken_yaml(): +@needs_pydanticv2 +def test_post_broken_yaml(client: TestClient): yaml_data = """ name: Deadpoolio tags: @@ -79,7 +44,8 @@ def test_post_broken_yaml(): assert response.json() == {"detail": "Invalid YAML"} -def test_post_invalid(): +@needs_pydanticv2 +def test_post_invalid(client: TestClient): yaml_data = """ name: Deadpoolio tags: @@ -90,8 +56,59 @@ def test_post_invalid(): """ response = client.post("/items/", content=yaml_data) assert response.status_code == 422, response.text + # insert_assert(response.json()) assert response.json() == { "detail": [ - {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"} + { + "type": "string_type", + "loc": ["tags", 3], + "msg": "Input should be a valid string", + "input": {"sneaky": "object"}, + "url": match_pydantic_error_url("string_type"), + } ] } + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/x-yaml": { + "schema": { + "title": "Item", + "required": ["name", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + }, + }, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py new file mode 100644 index 0000000000..ef012f8a6c --- /dev/null +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.path_operation_advanced_configuration.tutorial007_pv1 import app + + client = TestClient(app) + return client + + +@needs_pydanticv1 +def test_post(client: TestClient): + yaml_data = """ + name: Deadpoolio + tags: + - x-force + - x-men + - x-avengers + """ + response = client.post("/items/", content=yaml_data) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Deadpoolio", + "tags": ["x-force", "x-men", "x-avengers"], + } + + +@needs_pydanticv1 +def test_post_broken_yaml(client: TestClient): + yaml_data = """ + name: Deadpoolio + tags: + x - x-force + x - x-men + x - x-avengers + """ + response = client.post("/items/", content=yaml_data) + assert response.status_code == 422, response.text + assert response.json() == {"detail": "Invalid YAML"} + + +@needs_pydanticv1 +def test_post_invalid(client: TestClient): + yaml_data = """ + name: Deadpoolio + tags: + - x-force + - x-men + - x-avengers + - sneaky: object + """ + response = client.post("/items/", content=yaml_data) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"} + ] + } + + +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/x-yaml": { + "schema": { + "title": "Item", + "required": ["name", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + }, + }, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py index be9f2afecf..58dec5769d 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py @@ -4,45 +4,6 @@ from docs_src.path_operation_configuration.tutorial002b import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "tags": ["items"], - "summary": "Get Items", - "operationId": "get_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/users/": { - "get": { - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_items(): response = client.get("/items/") @@ -54,3 +15,40 @@ def test_get_users(): response = client.get("/users/") assert response.status_code == 200, response.text assert response.json() == ["Rick", "Morty"] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index e587519a00..d3792e701f 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -2,103 +2,10 @@ from fastapi.testclient import TestClient from docs_src.path_operation_configuration.tutorial005 import app +from ...utils import needs_pydanticv1, needs_pydanticv2 + client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "The created item", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_query_params_str_validations(): response = client.post("/items/", json={"name": "Foo", "price": 42}) @@ -110,3 +17,202 @@ def test_query_params_str_validations(): "tax": None, "tags": [], } + + +@needs_pydanticv2 +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema_pv1(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py index 43a7a610de..a68deb3df5 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py @@ -1,96 +1,7 @@ import pytest from fastapi.testclient import TestClient -from ...utils import needs_py310 - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "The created item", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 @pytest.fixture(name="client") @@ -101,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_query_params_str_validations(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": 42}) @@ -119,3 +23,204 @@ def test_query_params_str_validations(client: TestClient): "tax": None, "tags": [], } + + +@needs_py310 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_py310 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py index 62aa73ac52..e17f2592dc 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py @@ -1,96 +1,7 @@ import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "The created item", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from ...utils import needs_py39, needs_pydanticv1, needs_pydanticv2 @pytest.fixture(name="client") @@ -101,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_query_params_str_validations(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": 42}) @@ -119,3 +23,204 @@ def test_query_params_str_validations(client: TestClient): "tax": None, "tags": [], } + + +@needs_py39 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_py39 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py index 582caed44a..91180d1097 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py @@ -5,59 +5,6 @@ from docs_src.path_operation_configuration.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "tags": ["items"], - "summary": "Read Items", - "operationId": "read_items_items__get", - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - } - }, - "/elements/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "tags": ["items"], - "summary": "Read Elements", - "operationId": "read_elements_elements__get", - "deprecated": True, - } - }, - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -71,3 +18,54 @@ def test_query_params_str_validations(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + } + }, + "/elements/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "tags": ["items"], + "summary": "Read Elements", + "operationId": "read_elements_elements__get", + "deprecated": True, + } + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial004.py b/tests/test_tutorial/test_path_params/test_tutorial004.py index 7f0227ecfb..acbeaca769 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial004.py +++ b/tests/test_tutorial/test_path_params/test_tutorial004.py @@ -4,78 +4,6 @@ from docs_src.path_params.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/{file_path}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read File", - "operationId": "read_file_files__file_path__get", - "parameters": [ - { - "required": True, - "schema": {"title": "File Path", "type": "string"}, - "name": "file_path", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_file_path(): response = client.get("/files/home/johndoe/myfile.txt") @@ -89,3 +17,75 @@ def test_root_file_path(): print(response.content) assert response.status_code == 200, response.text assert response.json() == {"file_path": "/home/johndoe/myfile.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/{file_path}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read File", + "operationId": "read_file_files__file_path__get", + "parameters": [ + { + "required": True, + "schema": {"title": "File Path", "type": "string"}, + "name": "file_path", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index eae3637bed..2e4b0146be 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -1,196 +1,143 @@ -import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_params.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/models/{model_name}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Model", - "operationId": "get_model_models__model_name__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Model Name", - "enum": ["alexnet", "resnet", "lenet"], - "type": "string", - }, - "name": "model_name", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} + +def test_get_enums_alexnet(): + response = client.get("/models/alexnet") + assert response.status_code == 200 + assert response.json() == {"model_name": "alexnet", "message": "Deep Learning FTW!"} -openapi_schema2 = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/models/{model_name}": { - "get": { - "summary": "Get Model", - "operationId": "get_model_models__model_name__get", - "parameters": [ - { - "required": True, - "schema": {"$ref": "#/components/schemas/ModelName"}, - "name": "model_name", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ModelName": { - "title": "ModelName", - "enum": ["alexnet", "resnet", "lenet"], - "type": "string", - "description": "An enumeration.", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} +def test_get_enums_lenet(): + response = client.get("/models/lenet") + assert response.status_code == 200 + assert response.json() == {"model_name": "lenet", "message": "LeCNN all the images"} -def test_openapi(): +def test_get_enums_resnet(): + response = client.get("/models/resnet") + assert response.status_code == 200 + assert response.json() == {"model_name": "resnet", "message": "Have some residuals"} + + +def test_get_enums_invalid(): + response = client.get("/models/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "enum", + "loc": ["path", "model_name"], + "msg": "Input should be 'alexnet', 'resnet' or 'lenet'", + "input": "foo", + "ctx": {"expected": "'alexnet', 'resnet' or 'lenet'"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]}, + "loc": ["path", "model_name"], + "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'", + "type": "type_error.enum", + } + ] + } + ) + + +def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text data = response.json() - assert data == openapi_schema or data == openapi_schema2 - - -@pytest.mark.parametrize( - "url,status_code,expected", - [ - ( - "/models/alexnet", - 200, - {"model_name": "alexnet", "message": "Deep Learning FTW!"}, - ), - ( - "/models/lenet", - 200, - {"model_name": "lenet", "message": "LeCNN all the images"}, - ), - ( - "/models/resnet", - 200, - {"model_name": "resnet", "message": "Have some residuals"}, - ), - ( - "/models/foo", - 422, - { - "detail": [ + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/models/{model_name}": { + "get": { + "summary": "Get Model", + "operationId": "get_model_models__model_name__get", + "parameters": [ + { + "required": True, + "schema": {"$ref": "#/components/schemas/ModelName"}, + "name": "model_name", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ModelName": IsDict( { - "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]}, - "loc": ["path", "model_name"], - "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'", - "type": "type_error.enum", + "title": "ModelName", + "enum": ["alexnet", "resnet", "lenet"], + "type": "string", } - ] - }, - ), - ], -) -def test_get_enums(url, status_code, expected): - response = client.get(url) - assert response.status_code == status_code - assert response.json() == expected + ) + | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "title": "ModelName", + "enum": ["alexnet", "resnet", "lenet"], + "type": "string", + "description": "An enumeration.", + } + ), + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial005.py b/tests/test_tutorial/test_query_params/test_tutorial005.py index 07178f8a6e..9215863576 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params/test_tutorial005.py @@ -1,104 +1,120 @@ -import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.query_params.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + +def test_foo_needy_very(): + response = client.get("/items/foo?needy=very") + assert response.status_code == 200 + assert response.json() == {"item_id": "foo", "needy": "very"} + + +def test_foo_no_needy(): + response = client.get("/items/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "needy"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read User Item", - "operationId": "read_user_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Needy", "type": "string"}, - "name": "needy", - "in": "query", - }, - ], + "summary": "Read User Item", + "operationId": "read_user_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Needy", "type": "string"}, + "name": "needy", + "in": "query", + }, + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, }, - }, - } - }, -} - - -query_required = { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - - -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/openapi.json", 200, openapi_schema), - ("/items/foo?needy=very", 200, {"item_id": "foo", "needy": "very"}), - ("/items/foo", 422, query_required), - ("/items/foo", 422, query_required), - ], -) -def test(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial006.py b/tests/test_tutorial/test_query_params/test_tutorial006.py index 73c5302e7c..e07803d6c9 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006.py @@ -1,141 +1,179 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.query_params.tutorial006 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params.tutorial006 import app -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + c = TestClient(app) + return c + + +def test_foo_needy_very(client: TestClient): + response = client.get("/items/foo?needy=very") + assert response.status_code == 200 + assert response.json() == { + "item_id": "foo", + "needy": "very", + "skip": 0, + "limit": None, + } + + +def test_foo_no_needy(client: TestClient): + response = client.get("/items/foo?skip=a&limit=b") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "int_parsing", + "loc": ["query", "skip"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "a", + "url": match_pydantic_error_url("int_parsing"), + }, + { + "type": "int_parsing", + "loc": ["query", "limit"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "b", + "url": match_pydantic_error_url("int_parsing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "needy"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["query", "skip"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "limit"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } - } + }, }, }, - }, - "summary": "Read User Item", - "operationId": "read_user_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Needy", "type": "string"}, - "name": "needy", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer"}, - "name": "limit", - "in": "query", - }, - ], + "summary": "Read User Item", + "operationId": "read_user_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Needy", "type": "string"}, + "name": "needy", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Limit", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Limit", "type": "integer"} + ), + "name": "limit", + "in": "query", + }, + ], + } } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, }, - }, - } - }, -} - - -query_required = { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - - -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/openapi.json", 200, openapi_schema), - ( - "/items/foo?needy=very", - 200, - {"item_id": "foo", "needy": "very", "skip": 0, "limit": None}, - ), - ( - "/items/foo?skip=a&limit=b", - 422, - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["query", "skip"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "limit"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] - }, - ), - ], -) -def test(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py index 141525f15f..6c4c0b4dc8 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py @@ -1,103 +1,10 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read User Item", - "operationId": "read_user_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Needy", "type": "string"}, - "name": "needy", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer"}, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -query_required = { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - @pytest.fixture(name="client") def get_client(): @@ -108,41 +15,170 @@ def get_client(): @needs_py310 -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/openapi.json", 200, openapi_schema), - ( - "/items/foo?needy=very", - 200, - {"item_id": "foo", "needy": "very", "skip": 0, "limit": None}, - ), - ( - "/items/foo?skip=a&limit=b", - 422, - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", +def test_foo_needy_very(client: TestClient): + response = client.get("/items/foo?needy=very") + assert response.status_code == 200 + assert response.json() == { + "item_id": "foo", + "needy": "very", + "skip": 0, + "limit": None, + } + + +@needs_py310 +def test_foo_no_needy(client: TestClient): + response = client.get("/items/foo?skip=a&limit=b") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "int_parsing", + "loc": ["query", "skip"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "a", + "url": match_pydantic_error_url("int_parsing"), + }, + { + "type": "int_parsing", + "loc": ["query", "limit"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "b", + "url": match_pydantic_error_url("int_parsing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "needy"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["query", "skip"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "limit"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + ] + } + ) + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - { - "loc": ["query", "skip"], - "msg": "value is not a valid integer", - "type": "type_error.integer", + "summary": "Read User Item", + "operationId": "read_user_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Needy", "type": "string"}, + "name": "needy", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Limit", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Limit", "type": "integer"} + ), + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, }, - { - "loc": ["query", "limit"], - "msg": "value is not a valid integer", - "type": "type_error.integer", + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } }, - ] - }, - ), - ], -) -def test(path, expected_status, expected_response, client: TestClient): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py deleted file mode 100644 index f8d7f85c8e..0000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py +++ /dev/null @@ -1,122 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from docs_src.query_params_str_validations.tutorial010 import app - -client = TestClient(app) - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "description": "Query string for the items to search in the database that have a good match", - "required": False, - "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, - "name": "item-query", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -regex_error = { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] -} - - -@pytest.mark.parametrize( - "q_name,q,expected_status,expected_response", - [ - (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ( - "item-query", - "fixedquery", - 200, - {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, - ), - ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ("item-query", "nonregexquery", 422, regex_error), - ], -) -def test_query_params_str_validations(q_name, q, expected_status, expected_response): - url = "/items/" - if q_name and q: - url = f"{url}?{q_name}={q}" - response = client.get(url) - assert response.status_code == expected_status - assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py deleted file mode 100644 index 298b5d616c..0000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py +++ /dev/null @@ -1,132 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "description": "Query string for the items to search in the database that have a good match", - "required": False, - "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, - "name": "item-query", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params_str_validations.tutorial010_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -regex_error = { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] -} - - -@needs_py310 -@pytest.mark.parametrize( - "q_name,q,expected_status,expected_response", - [ - (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ( - "item-query", - "fixedquery", - 200, - {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, - ), - ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ("item-query", "nonregexquery", 422, regex_error), - ], -) -def test_query_params_str_validations( - q_name, q, expected_status, expected_response, client: TestClient -): - url = "/items/" - if q_name and q: - url = f"{url}?{q_name}={q}" - response = client.get(url) - assert response.status_code == expected_status - assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py new file mode 100644 index 0000000000..287c2e8f8e --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py @@ -0,0 +1,163 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010 import app + + client = TestClient(app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py new file mode 100644 index 0000000000..5b0515070a --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py @@ -0,0 +1,163 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_an import app + + client = TestClient(app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py new file mode 100644 index 0000000000..d22b1ce204 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py @@ -0,0 +1,170 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +@needs_py310 +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +@needs_py310 +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +@needs_py310 +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py new file mode 100644 index 0000000000..3e7d5d3adb --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py @@ -0,0 +1,170 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +@needs_py39 +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +@needs_py39 +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +@needs_py39 +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py new file mode 100644 index 0000000000..1c3a09d399 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py @@ -0,0 +1,170 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +@needs_py310 +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +@needs_py310 +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +@needs_py310 +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py index ad3645f314..5ba39b05d6 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py @@ -1,85 +1,10 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.query_params_str_validations.tutorial011 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_multi_query_values(): url = "/items/?q=foo&q=bar" @@ -93,3 +18,91 @@ def test_query_no_values(): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": None} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py new file mode 100644 index 0000000000..3942ea77a9 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py @@ -0,0 +1,108 @@ +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial011_an import app + +client = TestClient(app) + + +def test_multi_query_values(): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +def test_query_no_values(): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py new file mode 100644 index 0000000000..f2ec38c950 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py @@ -0,0 +1,118 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial011_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py310 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py new file mode 100644 index 0000000000..cd7b156798 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py @@ -0,0 +1,118 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial011_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py index 9330037edd..bdc7295162 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py @@ -1,78 +1,9 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,13 +13,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_multi_query_values(client: TestClient): url = "/items/?q=foo&q=bar" @@ -103,3 +27,92 @@ def test_query_no_values(client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": None} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py index 11f23be27c..26ac56b2f1 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py @@ -1,78 +1,9 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,13 +13,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_multi_query_values(client: TestClient): url = "/items/?q=foo&q=bar" @@ -103,3 +27,92 @@ def test_query_no_values(client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": None} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py index d69139dda5..1436db384c 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py @@ -4,83 +4,6 @@ from docs_src.query_params_str_validations.tutorial012 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - "default": ["foo", "bar"], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_default_query_values(): url = "/items/" @@ -94,3 +17,80 @@ def test_multi_query_values(): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": ["baz", "foobar"]} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py new file mode 100644 index 0000000000..270763f1d1 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py @@ -0,0 +1,96 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial012_an import app + +client = TestClient(app) + + +def test_default_query_values(): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +def test_multi_query_values(): + url = "/items/?q=baz&q=foobar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["baz", "foobar"]} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py new file mode 100644 index 0000000000..5483916839 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial012_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_default_query_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=baz&q=foobar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["baz", "foobar"]} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py index b25bb2847f..e7d7451548 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py @@ -3,77 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - "default": ["foo", "bar"], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -83,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_default_query_values(client: TestClient): url = "/items/" @@ -104,3 +26,81 @@ def test_multi_query_values(client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": ["baz", "foobar"]} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py index 1b2e363540..1ba1fdf618 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py @@ -4,83 +4,6 @@ from docs_src.query_params_str_validations.tutorial013 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {}, - "default": [], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_multi_query_values(): url = "/items/?q=foo&q=bar" @@ -94,3 +17,80 @@ def test_query_no_values(): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": []} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py new file mode 100644 index 0000000000..3432617481 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py @@ -0,0 +1,96 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial013_an import app + +client = TestClient(app) + + +def test_multi_query_values(): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +def test_query_no_values(): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": []} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py new file mode 100644 index 0000000000..537d6325b3 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial013_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": []} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py index 57b8b9d946..7bce7590c2 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py @@ -5,71 +5,6 @@ from docs_src.query_params_str_validations.tutorial014 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_hidden_query(): response = client.get("/items?hidden_query=somevalue") assert response.status_code == 200, response.text @@ -80,3 +15,67 @@ def test_no_hidden_query(): response = client.get("/items") assert response.status_code == 200, response.text assert response.json() == {"hidden_query": "Not found"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py new file mode 100644 index 0000000000..2182e87b75 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py @@ -0,0 +1,81 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial014_an import app + +client = TestClient(app) + + +def test_hidden_query(): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +def test_no_hidden_query(): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py new file mode 100644 index 0000000000..344004d01b --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py @@ -0,0 +1,91 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial014_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_hidden_query(client: TestClient): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +@needs_py310 +def test_no_hidden_query(client: TestClient): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py new file mode 100644 index 0000000000..5d4f6df3d0 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py @@ -0,0 +1,91 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial014_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_hidden_query(client: TestClient): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +@needs_py310 +def test_no_hidden_query(client: TestClient): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py index fe54fc080b..dad49fb12d 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py @@ -3,64 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -70,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_hidden_query(client: TestClient): response = client.get("/items?hidden_query=somevalue") @@ -89,3 +24,68 @@ def test_no_hidden_query(client: TestClient): response = client.get("/items") assert response.status_code == 200, response.text assert response.json() == {"hidden_query": "Not found"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index 166014c71f..91cc2b6365 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -1,131 +1,11 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.request_files.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - } - }, - "/uploadfile/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - file_required = { "detail": [ @@ -141,13 +21,59 @@ file_required = { def test_post_form_no_body(): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_body_json(): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_file(tmp_path): @@ -182,3 +108,125 @@ def test_post_upload_file(tmp_path): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index a254bf3e8f..42f75442a5 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -1,127 +1,10 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.request_files.tutorial001_02 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_form_no_body(): response = client.post("/files/") @@ -155,3 +38,171 @@ def test_post_upload_file(tmp_path): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py new file mode 100644 index 0000000000..f63eb339c4 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py @@ -0,0 +1,208 @@ +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_02_an import app + +client = TestClient(app) + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +def test_post_uploadfile_no_body(): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py new file mode 100644 index 0000000000..94b6ac67ee --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py @@ -0,0 +1,220 @@ +from pathlib import Path + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_02_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +@needs_py310 +def test_post_uploadfile_no_body(client: TestClient): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +@needs_py310 +def test_post_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py310 +def test_post_upload_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py new file mode 100644 index 0000000000..fcb39f8f18 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py @@ -0,0 +1,220 @@ +from pathlib import Path + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_02_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +@needs_py39 +def test_post_uploadfile_no_body(client: TestClient): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +@needs_py39 +def test_post_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py39 +def test_post_upload_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py index 15b6a8d538..a700752a30 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py @@ -1,122 +1,11 @@ from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -126,13 +15,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_post_form_no_body(client: TestClient): response = client.post("/files/") @@ -167,3 +49,172 @@ def test_post_upload_file(tmp_path: Path, client: TestClient): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03.py b/tests/test_tutorial/test_request_files/test_tutorial001_03.py index c34165f18e..f02170814c 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -4,138 +4,6 @@ from docs_src.request_files.tutorial001_03 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as bytes", - "format": "binary", - } - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as UploadFile", - "format": "binary", - } - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_file(tmp_path): path = tmp_path / "test.txt" @@ -157,3 +25,135 @@ def test_post_upload_file(tmp_path): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py new file mode 100644 index 0000000000..acfb749ce2 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py @@ -0,0 +1,159 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_03_an import app + +client = TestClient(app) + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py new file mode 100644 index 0000000000..36e5faac18 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py @@ -0,0 +1,167 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_03_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_post_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py39 +def test_post_upload_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_an.py new file mode 100644 index 0000000000..3021eb3c3c --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an.py @@ -0,0 +1,221 @@ +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from docs_src.request_files.tutorial001_an import app + +client = TestClient(app) + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_json(): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_large_file(tmp_path): + default_pydantic_max_size = 2**16 + path = tmp_path / "test.txt" + path.write_bytes(b"x" * (default_pydantic_max_size + 1)) + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": default_pydantic_max_size + 1} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py new file mode 100644 index 0000000000..04f3a4693b --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py @@ -0,0 +1,231 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py39 +def test_post_large_file(tmp_path, client: TestClient): + default_pydantic_max_size = 2**16 + path = tmp_path / "test.txt" + path.write_bytes(b"x" * (default_pydantic_max_size + 1)) + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": default_pydantic_max_size + 1} + + +@needs_py39 +def test_post_upload_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index 73d1179a1c..ed9680b62b 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -1,173 +1,68 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.request_files.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Files", - "operationId": "create_files_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_files_files__post" - } - } - }, - "required": True, - }, - } - }, - "/uploadfiles/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Upload Files", - "operationId": "create_upload_files_uploadfiles__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" - } - } - }, - "required": True, - }, - } - }, - "/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Main", - "operationId": "main__get", - } - }, - }, - "components": { - "schemas": { - "Body_create_upload_files_uploadfiles__post": { - "title": "Body_create_upload_files_uploadfiles__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - } - }, - }, - "Body_create_files_files__post": { - "title": "Body_create_files_files__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -file_required = { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - def test_post_form_no_body(): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_body_json(): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_files(tmp_path): @@ -213,3 +108,145 @@ def test_get_root(): response = client.get("/") assert response.status_code == 200, response.text assert b"<form" in response.content + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfiles/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + } + }, + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Main", + "operationId": "main__get", + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an.py b/tests/test_tutorial/test_request_files/test_tutorial002_an.py new file mode 100644 index 0000000000..ea8c1216c0 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an.py @@ -0,0 +1,252 @@ +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from docs_src.request_files.tutorial002_an import app + +client = TestClient(app) + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_json(): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_files(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"<file content2>") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"<file content2>") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +def test_get_root(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"<form" in response.content + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfiles/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + } + }, + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Main", + "operationId": "main__get", + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py new file mode 100644 index 0000000000..6d58778367 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py @@ -0,0 +1,271 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py39 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_files.tutorial002_an_py39 import app + + return app + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_files(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"<file content2>") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +@needs_py39 +def test_post_upload_file(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"<file content2>") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +@needs_py39 +def test_get_root(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"<form" in response.content + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfiles/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + } + }, + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Main", + "operationId": "main__get", + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py index de4127057d..2d0445421b 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py @@ -1,145 +1,11 @@ import pytest +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Files", - "operationId": "create_files_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_files_files__post" - } - } - }, - "required": True, - }, - } - }, - "/uploadfiles/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Upload Files", - "operationId": "create_upload_files_uploadfiles__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" - } - } - }, - "required": True, - }, - } - }, - "/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Main", - "operationId": "main__get", - } - }, - }, - "components": { - "schemas": { - "Body_create_upload_files_uploadfiles__post": { - "title": "Body_create_upload_files_uploadfiles__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - } - }, - }, - "Body_create_files_files__post": { - "title": "Body_create_files_files__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="app") def get_app(): @@ -150,18 +16,10 @@ def get_app(): @pytest.fixture(name="client") def get_client(app: FastAPI): - client = TestClient(app) return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - file_required = { "detail": [ { @@ -177,14 +35,60 @@ file_required = { def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 @@ -233,3 +137,146 @@ def test_get_root(app: FastAPI): response = client.get("/") assert response.status_code == 200, response.text assert b"<form" in response.content + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfiles/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + } + }, + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Main", + "operationId": "main__get", + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial003.py b/tests/test_tutorial/test_request_files/test_tutorial003.py index 83aea66cdd..85cd03a590 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003.py @@ -4,150 +4,6 @@ from docs_src.request_files.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create Files", - "operationId": "create_files_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_files_files__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfiles/": { - "post": { - "summary": "Create Upload Files", - "operationId": "create_upload_files_uploadfiles__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/": { - "get": { - "summary": "Main", - "operationId": "main__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_files_files__post": { - "title": "Body_create_files_files__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - "description": "Multiple files as bytes", - } - }, - }, - "Body_create_upload_files_uploadfiles__post": { - "title": "Body_create_upload_files_uploadfiles__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - "description": "Multiple files as UploadFile", - } - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_files(tmp_path): path = tmp_path / "test.txt" @@ -192,3 +48,147 @@ def test_get_root(): response = client.get("/") assert response.status_code == 200, response.text assert b"<form" in response.content + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfiles/": { + "post": { + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Main", + "operationId": "main__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as bytes", + } + }, + }, + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as UploadFile", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial003_an.py b/tests/test_tutorial/test_request_files/test_tutorial003_an.py new file mode 100644 index 0000000000..0327a2db6c --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial003_an.py @@ -0,0 +1,194 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial003_an import app + +client = TestClient(app) + + +def test_post_files(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"<file content2>") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"<file content2>") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +def test_get_root(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"<form" in response.content + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfiles/": { + "post": { + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Main", + "operationId": "main__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as bytes", + } + }, + }, + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as UploadFile", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py new file mode 100644 index 0000000000..3aa68c6215 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py @@ -0,0 +1,222 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_files.tutorial003_an_py39 import app + + return app + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client + + +file_required = { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +@needs_py39 +def test_post_files(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"<file content2>") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +@needs_py39 +def test_post_upload_file(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"<file content2>") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +@needs_py39 +def test_get_root(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"<form" in response.content + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfiles/": { + "post": { + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Main", + "operationId": "main__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as bytes", + } + }, + }, + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as UploadFile", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial003_py39.py b/tests/test_tutorial/test_request_files/test_tutorial003_py39.py index 56aeb54cd1..238bb39cd0 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003_py39.py @@ -4,144 +4,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create Files", - "operationId": "create_files_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_files_files__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfiles/": { - "post": { - "summary": "Create Upload Files", - "operationId": "create_upload_files_uploadfiles__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/": { - "get": { - "summary": "Main", - "operationId": "main__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_files_files__post": { - "title": "Body_create_files_files__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - "description": "Multiple files as bytes", - } - }, - }, - "Body_create_upload_files_uploadfiles__post": { - "title": "Body_create_upload_files_uploadfiles__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - "description": "Multiple files as UploadFile", - } - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - @pytest.fixture(name="app") def get_app(): @@ -152,18 +14,10 @@ def get_app(): @pytest.fixture(name="client") def get_client(app: FastAPI): - client = TestClient(app) return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - file_required = { "detail": [ { @@ -221,3 +75,148 @@ def test_get_root(app: FastAPI): response = client.get("/") assert response.status_code == 200, response.text assert b"<form" in response.content + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfiles/": { + "post": { + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Main", + "operationId": "main__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as bytes", + } + }, + }, + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as UploadFile", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001.py b/tests/test_tutorial/test_request_forms/test_tutorial001.py index 215260ffa9..805daeb10f 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001.py @@ -1,149 +1,241 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.request_forms.tutorial001 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_forms.tutorial001 import app -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + client = TestClient(app) + return client + + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, }, - "422": { - "description": "Validation Error", + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { "content": { - "application/json": { + "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Body_login_login__post" } } }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Body_login_login__post": { + "title": "Body_login_login__post", + "required": ["username", "password"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, }, }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login__post" - } + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, } }, - "required": True, }, } - } - }, - "components": { - "schemas": { - "Body_login_login__post": { - "title": "Body_login_login__post", - "required": ["username", "password"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -password_required = { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} -username_required = { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} -username_and_password_required = { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - - -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/login/", - {"username": "Foo", "password": "secret"}, - 200, - {"username": "Foo"}, - ), - ("/login/", {"username": "Foo"}, 422, password_required), - ("/login/", {"password": "secret"}, 422, username_required), - ("/login/", None, 422, username_and_password_required), - ], -) -def test_post_body_form(path, body, expected_status, expected_response): - response = client.post(path, data=body) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_post_body_json(): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == username_and_password_required + } diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001_an.py b/tests/test_tutorial/test_request_forms/test_tutorial001_an.py new file mode 100644 index 0000000000..c43a0b6955 --- /dev/null +++ b/tests/test_tutorial/test_request_forms/test_tutorial001_an.py @@ -0,0 +1,241 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_forms.tutorial001_an import app + + client = TestClient(app) + return client + + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_login__post" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Body_login_login__post": { + "title": "Body_login_login__post", + "required": ["username", "password"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py new file mode 100644 index 0000000000..078b812aa5 --- /dev/null +++ b/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py @@ -0,0 +1,249 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_forms.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo"} + + +@needs_py39 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_login__post" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Body_login_login__post": { + "title": "Body_login_login__post", + "required": ["username", "password"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index 09e232b8e5..cac58639f1 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py @@ -1,166 +1,171 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI from fastapi.testclient import TestClient - -from docs_src.request_forms_and_files.tutorial001 import app - -client = TestClient(app) - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file", "fileb", "token"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"}, - "fileb": {"title": "Fileb", "type": "string", "format": "binary"}, - "token": {"title": "Token", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from fastapi.utils import match_pydantic_error_url -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_forms_and_files.tutorial001 import app + + return app -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -token_required = { - "detail": [ - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]} - -file_and_token_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client -def test_post_form_no_body(): +def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_form_no_file(): +def test_post_form_no_file(client: TestClient): response = client.post("/files/", data={"token": "foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_file_no_token(tmp_path): +def test_post_file_no_token(tmp_path, app: FastAPI): path = tmp_path / "test.txt" path.write_bytes(b"<file content>") @@ -168,10 +173,45 @@ def test_post_file_no_token(tmp_path): with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 422, response.text - assert response.json() == token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_files_and_token(tmp_path): +def test_post_files_and_token(tmp_path, app: FastAPI): patha = tmp_path / "test.txt" pathb = tmp_path / "testb.txt" patha.write_text("<file content>") @@ -190,3 +230,91 @@ def test_post_files_and_token(tmp_path): "token": "foo", "fileb_content_type": "text/plain", } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file", "fileb", "token"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"}, + "fileb": { + "title": "Fileb", + "type": "string", + "format": "binary", + }, + "token": {"title": "Token", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py new file mode 100644 index 0000000000..009568048e --- /dev/null +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py @@ -0,0 +1,320 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_forms_and_files.tutorial001_an import app + + return app + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client + + +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_form_no_file(client: TestClient): + response = client.post("/files/", data={"token": "foo"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_json(client: TestClient): + response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_file_no_token(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_files_and_token(tmp_path, app: FastAPI): + patha = tmp_path / "test.txt" + pathb = tmp_path / "testb.txt" + patha.write_text("<file content>") + pathb.write_text("<file b content>") + + client = TestClient(app) + with patha.open("rb") as filea, pathb.open("rb") as fileb: + response = client.post( + "/files/", + data={"token": "foo"}, + files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "file_size": 14, + "token": "foo", + "fileb_content_type": "text/plain", + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file", "fileb", "token"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"}, + "fileb": { + "title": "Fileb", + "type": "string", + "format": "binary", + }, + "token": {"title": "Token", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py new file mode 100644 index 0000000000..3d007e90ba --- /dev/null +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py @@ -0,0 +1,328 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from ...utils import needs_py39 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_forms_and_files.tutorial001_an_py39 import app + + return app + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_post_form_no_file(client: TestClient): + response = client.post("/files/", data={"token": "foo"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_post_file_no_token(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"<file content>") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_post_files_and_token(tmp_path, app: FastAPI): + patha = tmp_path / "test.txt" + pathb = tmp_path / "testb.txt" + patha.write_text("<file content>") + pathb.write_text("<file b content>") + + client = TestClient(app) + with patha.open("rb") as filea, pathb.open("rb") as fileb: + response = client.post( + "/files/", + data={"token": "foo"}, + files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "file_size": 14, + "token": "foo", + "fileb_content_type": "text/plain", + } + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file", "fileb", "token"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"}, + "fileb": { + "title": "Fileb", + "type": "string", + "format": "binary", + }, + "token": {"title": "Token", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003.py b/tests/test_tutorial/test_response_model/test_tutorial003.py index e1bde5d133..384c8e0f14 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003.py @@ -1,106 +1,10 @@ +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/user/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserOut"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create User", - "operationId": "create_user_user__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserIn"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "UserOut": { - "title": "UserOut", - "required": ["username", "email"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string", "format": "email"}, - "full_name": {"title": "Full Name", "type": "string"}, - }, - }, - "UserIn": { - "title": "UserIn", - "required": ["username", "password", "email"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "email": {"title": "Email", "type": "string", "format": "email"}, - "full_name": {"title": "Full Name", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_user(): response = client.post( @@ -118,3 +22,130 @@ def test_post_user(): "email": "foo@example.com", "full_name": "Grave Dohl", } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/user/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserOut"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "UserOut": { + "title": "UserOut", + "required": IsOneOf( + ["username", "email", "full_name"], + # TODO: remove when deprecating Pydantic v1 + ["username", "email"], + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + }, + }, + "UserIn": { + "title": "UserIn", + "required": ["username", "password", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py new file mode 100644 index 0000000000..3a6a0b20d9 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -0,0 +1,151 @@ +from dirty_equals import IsDict, IsOneOf +from fastapi.testclient import TestClient + +from docs_src.response_model.tutorial003_01 import app + +client = TestClient(app) + + +def test_post_user(): + response = client.post( + "/user/", + json={ + "username": "foo", + "password": "fighter", + "email": "foo@example.com", + "full_name": "Grave Dohl", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "foo", + "email": "foo@example.com", + "full_name": "Grave Dohl", + } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/user/": { + "post": { + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/BaseUser"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "BaseUser": { + "title": "BaseUser", + "required": IsOneOf( + ["username", "email", "full_name"], + # TODO: remove when deprecating Pydantic v1 + ["username", "email"], + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "UserIn": { + "title": "UserIn", + "required": ["username", "email", "password"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "password": {"title": "Password", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py new file mode 100644 index 0000000000..6985b9de6a --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py @@ -0,0 +1,160 @@ +import pytest +from dirty_equals import IsDict, IsOneOf +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.response_model.tutorial003_01_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_post_user(client: TestClient): + response = client.post( + "/user/", + json={ + "username": "foo", + "password": "fighter", + "email": "foo@example.com", + "full_name": "Grave Dohl", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "foo", + "email": "foo@example.com", + "full_name": "Grave Dohl", + } + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/user/": { + "post": { + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/BaseUser"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "BaseUser": { + "title": "BaseUser", + "required": IsOneOf( + ["username", "email", "full_name"], + # TODO: remove when deprecating Pydantic v1 + ["username", "email"], + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "UserIn": { + "title": "UserIn", + "required": ["username", "email", "password"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "password": {"title": "Password", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_02.py b/tests/test_tutorial/test_response_model/test_tutorial003_02.py new file mode 100644 index 0000000000..eabd203456 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_02.py @@ -0,0 +1,93 @@ +from fastapi.testclient import TestClient + +from docs_src.response_model.tutorial003_02 import app + +client = TestClient(app) + + +def test_get_portal(): + response = client.get("/portal") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Here's your interdimensional portal."} + + +def test_get_redirect(): + response = client.get("/portal", params={"teleport": True}, follow_redirects=False) + assert response.status_code == 307, response.text + assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/portal": { + "get": { + "summary": "Get Portal", + "operationId": "get_portal_portal_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Teleport", + "type": "boolean", + "default": False, + }, + "name": "teleport", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_03.py b/tests/test_tutorial/test_response_model/test_tutorial003_03.py new file mode 100644 index 0000000000..970ff58450 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_03.py @@ -0,0 +1,34 @@ +from fastapi.testclient import TestClient + +from docs_src.response_model.tutorial003_03 import app + +client = TestClient(app) + + +def test_get_portal(): + response = client.get("/teleport", follow_redirects=False) + assert response.status_code == 307, response.text + assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/teleport": { + "get": { + "summary": "Get Teleport", + "operationId": "get_teleport_teleport_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_04.py b/tests/test_tutorial/test_response_model/test_tutorial003_04.py new file mode 100644 index 0000000000..4aa80145a5 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_04.py @@ -0,0 +1,9 @@ +import pytest +from fastapi.exceptions import FastAPIError + + +def test_invalid_response_model(): + with pytest.raises(FastAPIError): + from docs_src.response_model.tutorial003_04 import app + + assert app # pragma: no cover diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py new file mode 100644 index 0000000000..b876facc8b --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py @@ -0,0 +1,12 @@ +import pytest +from fastapi.exceptions import FastAPIError + +from ...utils import needs_py310 + + +@needs_py310 +def test_invalid_response_model(): + with pytest.raises(FastAPIError): + from docs_src.response_model.tutorial003_04_py310 import app + + assert app # pragma: no cover diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05.py b/tests/test_tutorial/test_response_model/test_tutorial003_05.py new file mode 100644 index 0000000000..c7a39cc748 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_05.py @@ -0,0 +1,93 @@ +from fastapi.testclient import TestClient + +from docs_src.response_model.tutorial003_05 import app + +client = TestClient(app) + + +def test_get_portal(): + response = client.get("/portal") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Here's your interdimensional portal."} + + +def test_get_redirect(): + response = client.get("/portal", params={"teleport": True}, follow_redirects=False) + assert response.status_code == 307, response.text + assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/portal": { + "get": { + "summary": "Get Portal", + "operationId": "get_portal_portal_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Teleport", + "type": "boolean", + "default": False, + }, + "name": "teleport", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py new file mode 100644 index 0000000000..f80d62572e --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py @@ -0,0 +1,103 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.response_model.tutorial003_05_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_get_portal(client: TestClient): + response = client.get("/portal") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Here's your interdimensional portal."} + + +@needs_py310 +def test_get_redirect(client: TestClient): + response = client.get("/portal", params={"teleport": True}, follow_redirects=False) + assert response.status_code == 307, response.text + assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/portal": { + "get": { + "summary": "Get Portal", + "operationId": "get_portal_portal_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Teleport", + "type": "boolean", + "default": False, + }, + "name": "teleport", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py index 9827dab8a3..3a3aee38aa 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py @@ -1,99 +1,9 @@ import pytest +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/user/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserOut"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create User", - "operationId": "create_user_user__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserIn"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "UserOut": { - "title": "UserOut", - "required": ["username", "email"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string", "format": "email"}, - "full_name": {"title": "Full Name", "type": "string"}, - }, - }, - "UserIn": { - "title": "UserIn", - "required": ["username", "password", "email"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "email": {"title": "Email", "type": "string", "format": "email"}, - "full_name": {"title": "Full Name", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -103,13 +13,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_post_user(client: TestClient): response = client.post( @@ -127,3 +30,131 @@ def test_post_user(client: TestClient): "email": "foo@example.com", "full_name": "Grave Dohl", } + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/user/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserOut"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "UserOut": { + "title": "UserOut", + "required": IsOneOf( + ["username", "email", "full_name"], + # TODO: remove when deprecating Pydantic v1 + ["username", "email"], + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + }, + }, + "UserIn": { + "title": "UserIn", + "required": ["username", "password", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "email": { + "title": "Email", + "type": "string", + "format": "email", + }, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py index 8c98c6de30..e9bde18dd5 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -1,103 +1,11 @@ import pytest +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - @pytest.mark.parametrize( "url,data", @@ -123,3 +31,109 @@ def test_get(url, data): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == data + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": IsOneOf( + ["name", "description", "price", "tax", "tags"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py index 7fc86fafab..6f8a3cbea8 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py @@ -1,95 +1,9 @@ import pytest +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -99,13 +13,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 @pytest.mark.parametrize( "url,data", @@ -131,3 +38,110 @@ def test_get(url, data, client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == data + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": IsOneOf( + ["name", "description", "price", "tax", "tags"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py index 405fe79f50..cfaa1eba21 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py @@ -1,95 +1,9 @@ import pytest +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -99,13 +13,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 @pytest.mark.parametrize( "url,data", @@ -131,3 +38,110 @@ def test_get(url, data, client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == data + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": IsOneOf( + ["name", "description", "price", "tax", "tags"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index 476b172d3d..b20864c07a 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -1,130 +1,10 @@ +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}/name": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Name", - "operationId": "read_item_name_items__item_id__name_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/items/{item_id}/public": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Public Data", - "operationId": "read_item_public_data_items__item_id__public_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_read_item_name(): response = client.get("/items/bar/name") @@ -140,3 +20,137 @@ def test_read_item_public_data(): "description": "The Bar fighters", "price": 62, } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}/name": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Name", + "operationId": "read_item_name_items__item_id__name_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/items/{item_id}/public": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Public Data", + "operationId": "read_item_public_data_items__item_id__public_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": IsOneOf( + ["name", "description", "price", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py index 389a302e08..de552c8f22 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py @@ -1,123 +1,9 @@ import pytest +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}/name": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Name", - "operationId": "read_item_name_items__item_id__name_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/items/{item_id}/public": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Public Data", - "operationId": "read_item_public_data_items__item_id__public_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -127,13 +13,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_read_item_name(client: TestClient): response = client.get("/items/bar/name") @@ -150,3 +29,138 @@ def test_read_item_public_data(client: TestClient): "description": "The Bar fighters", "price": 62, } + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}/name": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Name", + "operationId": "read_item_name_items__item_id__name_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/items/{item_id}/public": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Public Data", + "operationId": "read_item_public_data_items__item_id__public_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": IsOneOf( + ["name", "description", "price", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index 38eb31e54f..1e47e2eadb 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -1,130 +1,10 @@ +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}/name": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Name", - "operationId": "read_item_name_items__item_id__name_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/items/{item_id}/public": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Public Data", - "operationId": "read_item_public_data_items__item_id__public_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_read_item_name(): response = client.get("/items/bar/name") @@ -140,3 +20,137 @@ def test_read_item_public_data(): "description": "The Bar fighters", "price": 62, } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}/name": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Name", + "operationId": "read_item_name_items__item_id__name_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/items/{item_id}/public": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Public Data", + "operationId": "read_item_public_data_items__item_id__public_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": IsOneOf( + ["name", "description", "price", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py index f870f39261..40058b12de 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py @@ -1,123 +1,9 @@ import pytest +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}/name": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Name", - "operationId": "read_item_name_items__item_id__name_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/items/{item_id}/public": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Public Data", - "operationId": "read_item_public_data_items__item_id__public_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -127,13 +13,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_read_item_name(client: TestClient): response = client.get("/items/bar/name") @@ -150,3 +29,138 @@ def test_read_item_public_data(client: TestClient): "description": "The Bar fighters", "price": 62, } + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}/name": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Name", + "operationId": "read_item_name_items__item_id__name_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/items/{item_id}/public": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Public Data", + "operationId": "read_item_public_data_items__item_id__public_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": IsOneOf( + ["name", "description", "price", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py new file mode 100644 index 0000000000..98b1873554 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py @@ -0,0 +1,133 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial001 import app + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"type": "number", "title": "Price"}, + "tax": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Tax", + }, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "description": "A very nice Item", + "name": "Foo", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py new file mode 100644 index 0000000000..3520ef61da --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py @@ -0,0 +1,127 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial001_pv1 import app + + client = TestClient(app) + return client + + +@needs_pydanticv1 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "price": {"type": "number", "title": "Price"}, + "tax": {"type": "number", "title": "Tax"}, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py new file mode 100644 index 0000000000..e63e33cda5 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py @@ -0,0 +1,135 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@needs_pydanticv2 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py310 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"type": "number", "title": "Price"}, + "tax": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Tax", + }, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "description": "A very nice Item", + "name": "Foo", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py new file mode 100644 index 0000000000..e036d6b68b --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py @@ -0,0 +1,129 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310, needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial001_py310_pv1 import app + + client = TestClient(app) + return client + + +@needs_py310 +@needs_pydanticv1 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py310 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "price": {"type": "number", "title": "Price"}, + "tax": {"type": "number", "title": "Tax"}, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index badf66b3d1..eac0d1e29b 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -1,124 +1,10 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.schema_extra_example.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - # Test required and embedded body parameters with no bodies sent def test_post_body_example(): @@ -132,3 +18,151 @@ def test_post_body_example(): }, ) assert response.status_code == 200 + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py new file mode 100644 index 0000000000..a9cecd0983 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py @@ -0,0 +1,168 @@ +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.schema_extra_example.tutorial004_an import app + +client = TestClient(app) + + +# Test required and embedded body parameters with no bodies sent +def test_post_body_example(): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py new file mode 100644 index 0000000000..b6a7355996 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py @@ -0,0 +1,177 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial004_an_py310 import app + + client = TestClient(app) + return client + + +# Test required and embedded body parameters with no bodies sent +@needs_py310 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py new file mode 100644 index 0000000000..2493194a00 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py @@ -0,0 +1,177 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial004_an_py39 import app + + client = TestClient(app) + return client + + +# Test required and embedded body parameters with no bodies sent +@needs_py39 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py index d326a5a092..15f54bd5a6 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py @@ -1,117 +1,9 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -121,13 +13,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - # Test required and embedded body parameters with no bodies sent @needs_py310 def test_post_body_example(client: TestClient): @@ -141,3 +26,152 @@ def test_post_body_example(client: TestClient): }, ) assert response.status_code == 200 + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py new file mode 100644 index 0000000000..94a40ed5a3 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py @@ -0,0 +1,166 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial005 import app + + client = TestClient(app) + return client + + +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict({"$ref": "#/components/schemas/Item"}) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + ), + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py new file mode 100644 index 0000000000..da92f98f6a --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py @@ -0,0 +1,166 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial005_an import app + + client = TestClient(app) + return client + + +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict({"$ref": "#/components/schemas/Item"}) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + ), + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py new file mode 100644 index 0000000000..9109cb14ef --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py @@ -0,0 +1,170 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial005_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py310 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict({"$ref": "#/components/schemas/Item"}) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + ), + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py new file mode 100644 index 0000000000..fd4ec0575e --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py @@ -0,0 +1,170 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial005_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py39 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict({"$ref": "#/components/schemas/Item"}) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + ), + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py new file mode 100644 index 0000000000..05df534223 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py @@ -0,0 +1,170 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial005_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py310 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict({"$ref": "#/components/schemas/Item"}) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + ), + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial001.py b/tests/test_tutorial/test_security/test_tutorial001.py index 8a033c4f26..417bed8f7a 100644 --- a/tests/test_tutorial/test_security/test_tutorial001.py +++ b/tests/test_tutorial/test_security/test_tutorial001.py @@ -4,40 +4,6 @@ from docs_src.security.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2PasswordBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_no_token(): response = client.get("/items") @@ -57,3 +23,35 @@ def test_incorrect_token(): assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + } + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial001_an.py b/tests/test_tutorial/test_security/test_tutorial001_an.py new file mode 100644 index 0000000000..59460da7ff --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial001_an.py @@ -0,0 +1,57 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial001_an import app + +client = TestClient(app) + + +def test_no_token(): + response = client.get("/items") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(): + response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + +def test_incorrect_token(): + response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + } + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial001_an_py39.py b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py new file mode 100644 index 0000000000..d8e7127736 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py @@ -0,0 +1,68 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_no_token(client: TestClient): + response = client.get("/items") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_token(client: TestClient): + response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + +@needs_py39 +def test_incorrect_token(client: TestClient): + response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + } + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index 5951078343..18d4680f62 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -1,119 +1,10 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.security.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_token_post": { - "title": "Body_login_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - }, - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_login(): response = client.post("/token", data={"username": "johndoe", "password": "secret"}) @@ -174,3 +65,143 @@ def test_inactive_user(): response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "Inactive user"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial003_an.py b/tests/test_tutorial/test_security/test_tutorial003_an.py new file mode 100644 index 0000000000..a8f64d0c60 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial003_an.py @@ -0,0 +1,207 @@ +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from docs_src.security.tutorial003_an import app + +client = TestClient(app) + + +def test_login(): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} + + +def test_login_incorrect_password(): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_login_incorrect_username(): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_no_token(): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(): + response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + } + + +def test_incorrect_token(): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_incorrect_token_type(): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_inactive_user(): + response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py new file mode 100644 index 0000000000..7cbbcee2f5 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py @@ -0,0 +1,223 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial003_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} + + +@needs_py310 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + } + + +@needs_py310 +def test_incorrect_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_inactive_user(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py new file mode 100644 index 0000000000..7b21fbcc92 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py @@ -0,0 +1,223 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial003_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} + + +@needs_py39 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + } + + +@needs_py39 +def test_incorrect_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_inactive_user(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial003_py310.py b/tests/test_tutorial/test_security/test_tutorial003_py310.py index 26f5c097ff..512504534f 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial003_py310.py @@ -1,112 +1,9 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_token_post": { - "title": "Body_login_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - }, - }, -} - @pytest.fixture(name="client") def get_client(): @@ -116,13 +13,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_login(client: TestClient): response = client.post("/token", data={"username": "johndoe", "password": "secret"}) @@ -190,3 +80,144 @@ def test_inactive_user(client: TestClient): response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "Inactive user"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index e8697339ff..c669c306dd 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.security.tutorial005 import ( @@ -10,178 +11,6 @@ from docs_src.security.tutorial005 import ( client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def get_access_token(username="johndoe", password="secret", scope=None): data = {"username": username, "password": password} @@ -345,3 +174,236 @@ def test_read_system_status_no_token(): assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial005_an.py b/tests/test_tutorial/test_security/test_tutorial005_an.py new file mode 100644 index 0000000000..aaab04f78f --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005_an.py @@ -0,0 +1,409 @@ +from dirty_equals import IsDict, IsOneOf +from fastapi.testclient import TestClient + +from docs_src.security.tutorial005_an import ( + app, + create_access_token, + fake_users_db, + get_password_hash, + verify_password, +) + +client = TestClient(app) + + +def get_access_token(username="johndoe", password="secret", scope=None): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +def test_login(): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +def test_login_incorrect_password(): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_login_incorrect_username(): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_no_token(): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(): + access_token = get_access_token(scope="me") + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +def test_incorrect_token(): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_incorrect_token_type(): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_verify_password(): + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +def test_get_password_hash(): + assert get_password_hash("secretalice") + + +def test_create_access_token(): + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +def test_token_no_sub(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_no_username(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_no_scope(): + access_token = get_access_token() + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not enough permissions"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_inexistent_user(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_inactive_user(): + access_token = get_access_token( + username="alice", password="secretalice", scope="me" + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +def test_read_items(): + access_token = get_access_token(scope="me items") + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +def test_read_system_status(): + access_token = get_access_token() + response = client.get( + "/status/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +def test_read_system_status_no_token(): + response = client.get("/status/") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py new file mode 100644 index 0000000000..243d0773c2 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py @@ -0,0 +1,437 @@ +import pytest +from dirty_equals import IsDict, IsOneOf +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial005_an_py310 import app + + client = TestClient(app) + return client + + +def get_access_token( + *, username="johndoe", password="secret", scope=None, client: TestClient +): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +@needs_py310 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +@needs_py310 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_token(client: TestClient): + access_token = get_access_token(scope="me", client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +@needs_py310 +def test_incorrect_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_verify_password(): + from docs_src.security.tutorial005_an_py310 import fake_users_db, verify_password + + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +@needs_py310 +def test_get_password_hash(): + from docs_src.security.tutorial005_an_py310 import get_password_hash + + assert get_password_hash("secretalice") + + +@needs_py310 +def test_create_access_token(): + from docs_src.security.tutorial005_an_py310 import create_access_token + + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +@needs_py310 +def test_token_no_sub(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_no_username(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_no_scope(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not enough permissions"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_inexistent_user(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_inactive_user(client: TestClient): + access_token = get_access_token( + username="alice", password="secretalice", scope="me", client=client + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +@needs_py310 +def test_read_items(client: TestClient): + access_token = get_access_token(scope="me items", client=client) + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +@needs_py310 +def test_read_system_status(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/status/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +@needs_py310 +def test_read_system_status_no_token(client: TestClient): + response = client.get("/status/") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py new file mode 100644 index 0000000000..17a3f9aa2a --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py @@ -0,0 +1,437 @@ +import pytest +from dirty_equals import IsDict, IsOneOf +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial005_an_py39 import app + + client = TestClient(app) + return client + + +def get_access_token( + *, username="johndoe", password="secret", scope=None, client: TestClient +): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +@needs_py39 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +@needs_py39 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_token(client: TestClient): + access_token = get_access_token(scope="me", client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +@needs_py39 +def test_incorrect_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_verify_password(): + from docs_src.security.tutorial005_an_py39 import fake_users_db, verify_password + + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +@needs_py39 +def test_get_password_hash(): + from docs_src.security.tutorial005_an_py39 import get_password_hash + + assert get_password_hash("secretalice") + + +@needs_py39 +def test_create_access_token(): + from docs_src.security.tutorial005_an_py39 import create_access_token + + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +@needs_py39 +def test_token_no_sub(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_no_username(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_no_scope(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not enough permissions"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_inexistent_user(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_inactive_user(client: TestClient): + access_token = get_access_token( + username="alice", password="secretalice", scope="me", client=client + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +@needs_py39 +def test_read_items(client: TestClient): + access_token = get_access_token(scope="me items", client=client) + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +@needs_py39 +def test_read_system_status(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/status/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +@needs_py39 +def test_read_system_status_no_token(client: TestClient): + response = client.get("/status/") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial005_py310.py b/tests/test_tutorial/test_security/test_tutorial005_py310.py index 3144a23657..06455cd632 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py310.py @@ -1,174 +1,9 @@ import pytest +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, -} - @pytest.fixture(name="client") def get_client(): @@ -178,13 +13,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def get_access_token( *, username="johndoe", password="secret", scope=None, client: TestClient ): @@ -373,3 +201,237 @@ def test_read_system_status_no_token(client: TestClient): assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial005_py39.py b/tests/test_tutorial/test_security/test_tutorial005_py39.py index 290136e179..9455bfb4ef 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py39.py @@ -1,174 +1,9 @@ import pytest +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, -} - @pytest.fixture(name="client") def get_client(): @@ -178,13 +13,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def get_access_token( *, username="johndoe", password="secret", scope=None, client: TestClient ): @@ -373,3 +201,237 @@ def test_read_system_status_no_token(client: TestClient): assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial006.py b/tests/test_tutorial/test_security/test_tutorial006.py index bbfef9f7c4..dc459b6fd0 100644 --- a/tests/test_tutorial/test_security/test_tutorial006.py +++ b/tests/test_tutorial/test_security/test_tutorial006.py @@ -6,35 +6,6 @@ from docs_src.security.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_basic(): response = client.get("/users/me", auth=("john", "secret")) @@ -65,3 +36,30 @@ def test_security_http_basic_non_basic_credentials(): assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial006_an.py b/tests/test_tutorial/test_security/test_tutorial006_an.py new file mode 100644 index 0000000000..52ddd938f9 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial006_an.py @@ -0,0 +1,65 @@ +from base64 import b64encode + +from fastapi.testclient import TestClient + +from docs_src.security.tutorial006_an import app + +client = TestClient(app) + + +def test_security_http_basic(): + response = client.get("/users/me", auth=("john", "secret")) + assert response.status_code == 200, response.text + assert response.json() == {"username": "john", "password": "secret"} + + +def test_security_http_basic_no_credentials(): + response = client.get("/users/me") + assert response.json() == {"detail": "Not authenticated"} + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + + +def test_security_http_basic_invalid_credentials(): + response = client.get( + "/users/me", headers={"Authorization": "Basic notabase64token"} + ) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_security_http_basic_non_basic_credentials(): + payload = b64encode(b"johnsecret").decode("ascii") + auth_header = f"Basic {payload}" + response = client.get("/users/me", headers={"Authorization": auth_header}) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } diff --git a/tests/test_tutorial/test_security/test_tutorial006_an_py39.py b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py new file mode 100644 index 0000000000..52b22e5739 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py @@ -0,0 +1,77 @@ +from base64 import b64encode + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial006_an import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_security_http_basic(client: TestClient): + response = client.get("/users/me", auth=("john", "secret")) + assert response.status_code == 200, response.text + assert response.json() == {"username": "john", "password": "secret"} + + +@needs_py39 +def test_security_http_basic_no_credentials(client: TestClient): + response = client.get("/users/me") + assert response.json() == {"detail": "Not authenticated"} + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + + +@needs_py39 +def test_security_http_basic_invalid_credentials(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Basic notabase64token"} + ) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} + + +@needs_py39 +def test_security_http_basic_non_basic_credentials(client: TestClient): + payload = b64encode(b"johnsecret").decode("ascii") + auth_header = f"Basic {payload}" + response = client.get("/users/me", headers={"Authorization": auth_header}) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } diff --git a/docs/uk/overrides/.gitignore b/tests/test_tutorial/test_separate_openapi_schemas/__init__.py similarity index 100% rename from docs/uk/overrides/.gitignore rename to tests/test_tutorial/test_separate_openapi_schemas/__init__.py diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py new file mode 100644 index 0000000000..cdfae9f8c1 --- /dev/null +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py @@ -0,0 +1,133 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + from docs_src.separate_openapi_schemas.tutorial001 import app + + client = TestClient(app) + return client + + +def test_create_item(client: TestClient) -> None: + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "description": None} + + +def test_read_items(client: TestClient) -> None: + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + {"name": "Plumbus", "description": None}, + ] + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py new file mode 100644 index 0000000000..3b22146f63 --- /dev/null +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py @@ -0,0 +1,136 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + from docs_src.separate_openapi_schemas.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_create_item(client: TestClient) -> None: + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "description": None} + + +@needs_py310 +def test_read_items(client: TestClient) -> None: + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + {"name": "Plumbus", "description": None}, + ] + + +@needs_py310 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py new file mode 100644 index 0000000000..991abe8113 --- /dev/null +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py @@ -0,0 +1,136 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + from docs_src.separate_openapi_schemas.tutorial001_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_create_item(client: TestClient) -> None: + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "description": None} + + +@needs_py39 +def test_read_items(client: TestClient) -> None: + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + {"name": "Plumbus", "description": None}, + ] + + +@needs_py39 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py new file mode 100644 index 0000000000..d2cf7945b7 --- /dev/null +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py @@ -0,0 +1,133 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + from docs_src.separate_openapi_schemas.tutorial002 import app + + client = TestClient(app) + return client + + +def test_create_item(client: TestClient) -> None: + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "description": None} + + +def test_read_items(client: TestClient) -> None: + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + {"name": "Plumbus", "description": None}, + ] + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py new file mode 100644 index 0000000000..89c9ce9770 --- /dev/null +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py @@ -0,0 +1,136 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + from docs_src.separate_openapi_schemas.tutorial002_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_create_item(client: TestClient) -> None: + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "description": None} + + +@needs_py310 +def test_read_items(client: TestClient) -> None: + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + {"name": "Plumbus", "description": None}, + ] + + +@needs_py310 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py new file mode 100644 index 0000000000..6ac3d8f791 --- /dev/null +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py @@ -0,0 +1,136 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + from docs_src.separate_openapi_schemas.tutorial002_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_create_item(client: TestClient) -> None: + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "description": None} + + +@needs_py39 +def test_read_items(client: TestClient) -> None: + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + {"name": "Plumbus", "description": None}, + ] + + +@needs_py39 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_settings/test_app02.py b/tests/test_tutorial/test_settings/test_app02.py index fd32b8766f..eced88c044 100644 --- a/tests/test_tutorial/test_settings/test_app02.py +++ b/tests/test_tutorial/test_settings/test_app02.py @@ -1,17 +1,20 @@ -from fastapi.testclient import TestClient from pytest import MonkeyPatch -from docs_src.settings.app02 import main, test_main - -client = TestClient(main.app) +from ...utils import needs_pydanticv2 +@needs_pydanticv2 def test_settings(monkeypatch: MonkeyPatch): + from docs_src.settings.app02 import main + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") settings = main.get_settings() assert settings.app_name == "Awesome API" assert settings.items_per_user == 50 +@needs_pydanticv2 def test_override_settings(): + from docs_src.settings.app02 import test_main + test_main.test_app() diff --git a/tests/test_tutorial/test_settings/test_tutorial001.py b/tests/test_tutorial/test_settings/test_tutorial001.py new file mode 100644 index 0000000000..eb30dbcee4 --- /dev/null +++ b/tests/test_tutorial/test_settings/test_tutorial001.py @@ -0,0 +1,19 @@ +from fastapi.testclient import TestClient +from pytest import MonkeyPatch + +from ...utils import needs_pydanticv2 + + +@needs_pydanticv2 +def test_settings(monkeypatch: MonkeyPatch): + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + from docs_src.settings.tutorial001 import app + + client = TestClient(app) + response = client.get("/info") + assert response.status_code == 200, response.text + assert response.json() == { + "app_name": "Awesome API", + "admin_email": "admin@example.com", + "items_per_user": 50, + } diff --git a/tests/test_tutorial/test_settings/test_tutorial001_pv1.py b/tests/test_tutorial/test_settings/test_tutorial001_pv1.py new file mode 100644 index 0000000000..e4659de665 --- /dev/null +++ b/tests/test_tutorial/test_settings/test_tutorial001_pv1.py @@ -0,0 +1,19 @@ +from fastapi.testclient import TestClient +from pytest import MonkeyPatch + +from ...utils import needs_pydanticv1 + + +@needs_pydanticv1 +def test_settings(monkeypatch: MonkeyPatch): + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + from docs_src.settings.tutorial001_pv1 import app + + client = TestClient(app) + response = client.get("/info") + assert response.status_code == 200, response.text + assert response.json() == { + "app_name": "Awesome API", + "admin_email": "admin@example.com", + "items_per_user": 50, + } diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases.py b/tests/test_tutorial/test_sql_databases/test_sql_databases.py index 9d03ce61ba..03e7474334 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases.py @@ -3,284 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Users Users Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - }, - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create User", - "operationId": "create_user_users__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserCreate"} - } - }, - "required": True, - }, - }, - }, - "/users/{user_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read User", - "operationId": "read_user_users__user_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - } - }, - "/users/{user_id}/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Item For User", - "operationId": "create_item_for_user_users__user_id__items__post", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemCreate"} - } - }, - "required": True, - }, - } - }, - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ItemCreate": { - "title": "ItemCreate", - "required": ["title"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - }, - "Item": { - "title": "Item", - "required": ["title", "id", "owner_id"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "owner_id": {"title": "Owner Id", "type": "integer"}, - }, - }, - "User": { - "title": "User", - "required": ["email", "id", "is_active"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "is_active": {"title": "Is Active", "type": "boolean"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - "default": [], - }, - }, - }, - "UserCreate": { - "title": "UserCreate", - "required": ["email", "password"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from ...utils import needs_pydanticv1 @pytest.fixture(scope="module") @@ -303,12 +29,8 @@ def client(tmp_path_factory: pytest.TempPathFactory): os.chdir(cwd) -def test_openapi_schema(client): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -320,6 +42,8 @@ def test_create_user(client): assert response.status_code == 400, response.text +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -328,11 +52,15 @@ def test_get_user(client): assert "id" in data +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -341,6 +69,8 @@ def test_get_users(client): assert "id" in data[0] +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -358,6 +88,8 @@ def test_create_item(client): assert item_to_check["description"] == item["description"] +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -366,3 +98,322 @@ def test_read_items(client): first_item = data[0] assert "title" in first_item assert "description" in first_item + + +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Users Users Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + }, + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_users__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserCreate"} + } + }, + "required": True, + }, + }, + }, + "/users/{user_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read User", + "operationId": "read_user_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + } + ], + } + }, + "/users/{user_id}/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item For User", + "operationId": "create_item_for_user_users__user_id__items__post", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemCreate"} + } + }, + "required": True, + }, + } + }, + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ItemCreate": { + "title": "ItemCreate", + "required": ["title"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + }, + }, + "Item": { + "title": "Item", + "required": ["title", "id", "owner_id"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), + "id": {"title": "Id", "type": "integer"}, + "owner_id": {"title": "Owner Id", "type": "integer"}, + }, + }, + "User": { + "title": "User", + "required": ["email", "id", "is_active"], + "type": "object", + "properties": { + "email": {"title": "Email", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + "is_active": {"title": "Is Active", "type": "boolean"}, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + "default": [], + }, + }, + }, + "UserCreate": { + "title": "UserCreate", + "required": ["email", "password"], + "type": "object", + "properties": { + "email": {"title": "Email", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py index fbaa8938a4..a503ef2a6a 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py @@ -2,284 +2,10 @@ import importlib from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Users Users Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - }, - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create User", - "operationId": "create_user_users__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserCreate"} - } - }, - "required": True, - }, - }, - }, - "/users/{user_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read User", - "operationId": "read_user_users__user_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - } - }, - "/users/{user_id}/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Item For User", - "operationId": "create_item_for_user_users__user_id__items__post", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemCreate"} - } - }, - "required": True, - }, - } - }, - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ItemCreate": { - "title": "ItemCreate", - "required": ["title"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - }, - "Item": { - "title": "Item", - "required": ["title", "id", "owner_id"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "owner_id": {"title": "Owner Id", "type": "integer"}, - }, - }, - "User": { - "title": "User", - "required": ["email", "id", "is_active"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "is_active": {"title": "Is Active", "type": "boolean"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - "default": [], - }, - }, - }, - "UserCreate": { - "title": "UserCreate", - "required": ["email", "password"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from ...utils import needs_pydanticv1 @pytest.fixture(scope="module") @@ -299,12 +25,8 @@ def client(): test_db.unlink() -def test_openapi_schema(client): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -316,6 +38,8 @@ def test_create_user(client): assert response.status_code == 400, response.text +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -324,11 +48,15 @@ def test_get_user(client): assert "id" in data +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -337,6 +65,8 @@ def test_get_users(client): assert "id" in data[0] +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -360,6 +90,8 @@ def test_create_item(client): assert item_to_check["description"] == item["description"] +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -368,3 +100,322 @@ def test_read_items(client): first_item = data[0] assert "title" in first_item assert "description" in first_item + + +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Users Users Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + }, + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_users__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserCreate"} + } + }, + "required": True, + }, + }, + }, + "/users/{user_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read User", + "operationId": "read_user_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + } + ], + } + }, + "/users/{user_id}/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item For User", + "operationId": "create_item_for_user_users__user_id__items__post", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemCreate"} + } + }, + "required": True, + }, + } + }, + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ItemCreate": { + "title": "ItemCreate", + "required": ["title"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + }, + }, + "Item": { + "title": "Item", + "required": ["title", "id", "owner_id"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), + "id": {"title": "Id", "type": "integer"}, + "owner_id": {"title": "Owner Id", "type": "integer"}, + }, + }, + "User": { + "title": "User", + "required": ["email", "id", "is_active"], + "type": "object", + "properties": { + "email": {"title": "Email", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + "is_active": {"title": "Is Active", "type": "boolean"}, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + "default": [], + }, + }, + }, + "UserCreate": { + "title": "UserCreate", + "required": ["email", "password"], + "type": "object", + "properties": { + "email": {"title": "Email", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py index d131b4b6a9..d54cc65527 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py @@ -3,286 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py310 - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Users Users Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - }, - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create User", - "operationId": "create_user_users__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserCreate"} - } - }, - "required": True, - }, - }, - }, - "/users/{user_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read User", - "operationId": "read_user_users__user_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - } - }, - "/users/{user_id}/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Item For User", - "operationId": "create_item_for_user_users__user_id__items__post", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemCreate"} - } - }, - "required": True, - }, - } - }, - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ItemCreate": { - "title": "ItemCreate", - "required": ["title"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - }, - "Item": { - "title": "Item", - "required": ["title", "id", "owner_id"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "owner_id": {"title": "Owner Id", "type": "integer"}, - }, - }, - "User": { - "title": "User", - "required": ["email", "id", "is_active"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "is_active": {"title": "Is Active", "type": "boolean"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - "default": [], - }, - }, - }, - "UserCreate": { - "title": "UserCreate", - "required": ["email", "password"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from ...utils import needs_py310, needs_pydanticv1 @pytest.fixture(scope="module") @@ -307,13 +31,8 @@ def client(tmp_path_factory: pytest.TempPathFactory): @needs_py310 -def test_openapi_schema(client): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -@needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -326,6 +45,8 @@ def test_create_user(client): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -335,12 +56,16 @@ def test_get_user(client): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -350,6 +75,8 @@ def test_get_users(client): @needs_py310 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -374,6 +101,8 @@ def test_create_item(client): @needs_py310 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -382,3 +111,323 @@ def test_read_items(client): first_item = data[0] assert "title" in first_item assert "description" in first_item + + +@needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Users Users Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + }, + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_users__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserCreate"} + } + }, + "required": True, + }, + }, + }, + "/users/{user_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read User", + "operationId": "read_user_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + } + ], + } + }, + "/users/{user_id}/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item For User", + "operationId": "create_item_for_user_users__user_id__items__post", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemCreate"} + } + }, + "required": True, + }, + } + }, + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ItemCreate": { + "title": "ItemCreate", + "required": ["title"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + }, + }, + "Item": { + "title": "Item", + "required": ["title", "id", "owner_id"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), + "id": {"title": "Id", "type": "integer"}, + "owner_id": {"title": "Owner Id", "type": "integer"}, + }, + }, + "User": { + "title": "User", + "required": ["email", "id", "is_active"], + "type": "object", + "properties": { + "email": {"title": "Email", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + "is_active": {"title": "Is Active", "type": "boolean"}, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + "default": [], + }, + }, + }, + "UserCreate": { + "title": "UserCreate", + "required": ["email", "password"], + "type": "object", + "properties": { + "email": {"title": "Email", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py index 470fb52fd5..4e43995e63 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py @@ -3,286 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Users Users Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - }, - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create User", - "operationId": "create_user_users__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserCreate"} - } - }, - "required": True, - }, - }, - }, - "/users/{user_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read User", - "operationId": "read_user_users__user_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - } - }, - "/users/{user_id}/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Item For User", - "operationId": "create_item_for_user_users__user_id__items__post", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemCreate"} - } - }, - "required": True, - }, - } - }, - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ItemCreate": { - "title": "ItemCreate", - "required": ["title"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - }, - "Item": { - "title": "Item", - "required": ["title", "id", "owner_id"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "owner_id": {"title": "Owner Id", "type": "integer"}, - }, - }, - "User": { - "title": "User", - "required": ["email", "id", "is_active"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "is_active": {"title": "Is Active", "type": "boolean"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - "default": [], - }, - }, - }, - "UserCreate": { - "title": "UserCreate", - "required": ["email", "password"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from ...utils import needs_py39, needs_pydanticv1 @pytest.fixture(scope="module") @@ -307,13 +31,8 @@ def client(tmp_path_factory: pytest.TempPathFactory): @needs_py39 -def test_openapi_schema(client): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -@needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -326,6 +45,8 @@ def test_create_user(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -335,12 +56,16 @@ def test_get_user(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -350,6 +75,8 @@ def test_get_users(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -374,6 +101,8 @@ def test_create_item(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -382,3 +111,323 @@ def test_read_items(client): first_item = data[0] assert "title" in first_item assert "description" in first_item + + +@needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Users Users Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + }, + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_users__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserCreate"} + } + }, + "required": True, + }, + }, + }, + "/users/{user_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read User", + "operationId": "read_user_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + } + ], + } + }, + "/users/{user_id}/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item For User", + "operationId": "create_item_for_user_users__user_id__items__post", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemCreate"} + } + }, + "required": True, + }, + } + }, + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ItemCreate": { + "title": "ItemCreate", + "required": ["title"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + }, + }, + "Item": { + "title": "Item", + "required": ["title", "id", "owner_id"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), + "id": {"title": "Id", "type": "integer"}, + "owner_id": {"title": "Owner Id", "type": "integer"}, + }, + }, + "User": { + "title": "User", + "required": ["email", "id", "is_active"], + "type": "object", + "properties": { + "email": {"title": "Email", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + "is_active": {"title": "Is Active", "type": "boolean"}, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + "default": [], + }, + }, + }, + "UserCreate": { + "title": "UserCreate", + "required": ["email", "password"], + "type": "object", + "properties": { + "email": {"title": "Email", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py index dc6a1db157..b89b8b0317 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py @@ -3,286 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py310 - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Users Users Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - }, - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create User", - "operationId": "create_user_users__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserCreate"} - } - }, - "required": True, - }, - }, - }, - "/users/{user_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read User", - "operationId": "read_user_users__user_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - } - }, - "/users/{user_id}/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Item For User", - "operationId": "create_item_for_user_users__user_id__items__post", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemCreate"} - } - }, - "required": True, - }, - } - }, - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ItemCreate": { - "title": "ItemCreate", - "required": ["title"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - }, - "Item": { - "title": "Item", - "required": ["title", "id", "owner_id"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "owner_id": {"title": "Owner Id", "type": "integer"}, - }, - }, - "User": { - "title": "User", - "required": ["email", "id", "is_active"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "is_active": {"title": "Is Active", "type": "boolean"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - "default": [], - }, - }, - }, - "UserCreate": { - "title": "UserCreate", - "required": ["email", "password"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from ...utils import needs_py310, needs_pydanticv1 @pytest.fixture(scope="module", name="client") @@ -306,13 +30,8 @@ def get_client(tmp_path_factory: pytest.TempPathFactory): @needs_py310 -def test_openapi_schema(client): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -@needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -325,6 +44,8 @@ def test_create_user(client): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -334,12 +55,16 @@ def test_get_user(client): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -349,6 +74,8 @@ def test_get_users(client): @needs_py310 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -373,6 +100,8 @@ def test_create_item(client): @needs_py310 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -381,3 +110,323 @@ def test_read_items(client): first_item = data[0] assert "title" in first_item assert "description" in first_item + + +@needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Users Users Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + }, + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_users__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserCreate"} + } + }, + "required": True, + }, + }, + }, + "/users/{user_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read User", + "operationId": "read_user_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + } + ], + } + }, + "/users/{user_id}/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item For User", + "operationId": "create_item_for_user_users__user_id__items__post", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemCreate"} + } + }, + "required": True, + }, + } + }, + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ItemCreate": { + "title": "ItemCreate", + "required": ["title"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + }, + }, + "Item": { + "title": "Item", + "required": ["title", "id", "owner_id"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), + "id": {"title": "Id", "type": "integer"}, + "owner_id": {"title": "Owner Id", "type": "integer"}, + }, + }, + "User": { + "title": "User", + "required": ["email", "id", "is_active"], + "type": "object", + "properties": { + "email": {"title": "Email", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + "is_active": {"title": "Is Active", "type": "boolean"}, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + "default": [], + }, + }, + }, + "UserCreate": { + "title": "UserCreate", + "required": ["email", "password"], + "type": "object", + "properties": { + "email": {"title": "Email", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py index ebf55ed015..13351bc810 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py @@ -3,286 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Users Users Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - }, - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create User", - "operationId": "create_user_users__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserCreate"} - } - }, - "required": True, - }, - }, - }, - "/users/{user_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read User", - "operationId": "read_user_users__user_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - } - }, - "/users/{user_id}/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Item For User", - "operationId": "create_item_for_user_users__user_id__items__post", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemCreate"} - } - }, - "required": True, - }, - } - }, - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ItemCreate": { - "title": "ItemCreate", - "required": ["title"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - }, - "Item": { - "title": "Item", - "required": ["title", "id", "owner_id"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "owner_id": {"title": "Owner Id", "type": "integer"}, - }, - }, - "User": { - "title": "User", - "required": ["email", "id", "is_active"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "is_active": {"title": "Is Active", "type": "boolean"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - "default": [], - }, - }, - }, - "UserCreate": { - "title": "UserCreate", - "required": ["email", "password"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} +from ...utils import needs_py39, needs_pydanticv1 @pytest.fixture(scope="module", name="client") @@ -306,13 +30,8 @@ def get_client(tmp_path_factory: pytest.TempPathFactory): @needs_py39 -def test_openapi_schema(client): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -@needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -325,6 +44,8 @@ def test_create_user(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -334,12 +55,16 @@ def test_get_user(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -349,6 +74,8 @@ def test_get_users(client): @needs_py39 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -373,6 +100,8 @@ def test_create_item(client): @needs_py39 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -381,3 +110,323 @@ def test_read_items(client): first_item = data[0] assert "title" in first_item assert "description" in first_item + + +@needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Users Users Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + }, + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create User", + "operationId": "create_user_users__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserCreate"} + } + }, + "required": True, + }, + }, + }, + "/users/{user_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read User", + "operationId": "read_user_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + } + ], + } + }, + "/users/{user_id}/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item For User", + "operationId": "create_item_for_user_users__user_id__items__post", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemCreate"} + } + }, + "required": True, + }, + } + }, + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ItemCreate": { + "title": "ItemCreate", + "required": ["title"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + }, + }, + "Item": { + "title": "Item", + "required": ["title", "id", "owner_id"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), + "id": {"title": "Id", "type": "integer"}, + "owner_id": {"title": "Owner Id", "type": "integer"}, + }, + }, + "User": { + "title": "User", + "required": ["email", "id", "is_active"], + "type": "object", + "properties": { + "email": {"title": "Email", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + "is_active": {"title": "Is Active", "type": "boolean"}, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + "default": [], + }, + }, + }, + "UserCreate": { + "title": "UserCreate", + "required": ["email", "password"], + "type": "object", + "properties": { + "email": {"title": "Email", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_sql_databases/test_testing_databases.py b/tests/test_tutorial/test_sql_databases/test_testing_databases.py index 6f667dea03..ce6ce230c8 100644 --- a/tests/test_tutorial/test_sql_databases/test_testing_databases.py +++ b/tests/test_tutorial/test_sql_databases/test_testing_databases.py @@ -4,7 +4,11 @@ from pathlib import Path import pytest +from ...utils import needs_pydanticv1 + +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_testing_dbs(tmp_path_factory: pytest.TempPathFactory): tmp_path = tmp_path_factory.mktemp("data") cwd = os.getcwd() diff --git a/tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py b/tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py index 9e6b3f3e2c..545d63c2a8 100644 --- a/tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py @@ -4,10 +4,12 @@ from pathlib import Path import pytest -from ...utils import needs_py310 +from ...utils import needs_py310, needs_pydanticv1 @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_testing_dbs_py39(tmp_path_factory: pytest.TempPathFactory): tmp_path = tmp_path_factory.mktemp("data") cwd = os.getcwd() diff --git a/tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py b/tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py index 0b27adf44a..99bfd3fa8a 100644 --- a/tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py @@ -4,10 +4,12 @@ from pathlib import Path import pytest -from ...utils import needs_py39 +from ...utils import needs_py39, needs_pydanticv1 @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_testing_dbs_py39(tmp_path_factory: pytest.TempPathFactory): tmp_path = tmp_path_factory.mktemp("data") cwd = os.getcwd() diff --git a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py b/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py deleted file mode 100644 index 1b4a7b302c..0000000000 --- a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py +++ /dev/null @@ -1,420 +0,0 @@ -import time -from pathlib import Path -from unittest.mock import MagicMock - -import pytest -from fastapi.testclient import TestClient - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Users Users Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create User", - "operationId": "create_user_users__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/users/{user_id}": { - "get": { - "summary": "Read User", - "operationId": "read_user_users__user_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/{user_id}/items/": { - "post": { - "summary": "Create Item For User", - "operationId": "create_item_for_user_users__user_id__items__post", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/slowusers/": { - "get": { - "summary": "Read Slow Users", - "operationId": "read_slow_users_slowusers__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Slow Users Slowusers Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["title", "id", "owner_id"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "owner_id": {"title": "Owner Id", "type": "integer"}, - }, - }, - "ItemCreate": { - "title": "ItemCreate", - "required": ["title"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - }, - "User": { - "title": "User", - "required": ["email", "id", "is_active"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "is_active": {"title": "Is Active", "type": "boolean"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - "default": [], - }, - }, - }, - "UserCreate": { - "title": "UserCreate", - "required": ["email", "password"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -@pytest.fixture(scope="module") -def client(): - # Import while creating the client to create the DB after starting the test session - from docs_src.sql_databases_peewee.sql_app.main import app - - test_db = Path("./test.db") - with TestClient(app) as c: - yield c - test_db.unlink() - - -def test_openapi_schema(client): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_create_user(client): - test_user = {"email": "johndoe@example.com", "password": "secret"} - response = client.post("/users/", json=test_user) - assert response.status_code == 200, response.text - data = response.json() - assert test_user["email"] == data["email"] - assert "id" in data - response = client.post("/users/", json=test_user) - assert response.status_code == 400, response.text - - -def test_get_user(client): - response = client.get("/users/1") - assert response.status_code == 200, response.text - data = response.json() - assert "email" in data - assert "id" in data - - -def test_inexistent_user(client): - response = client.get("/users/999") - assert response.status_code == 404, response.text - - -def test_get_users(client): - response = client.get("/users/") - assert response.status_code == 200, response.text - data = response.json() - assert "email" in data[0] - assert "id" in data[0] - - -time.sleep = MagicMock() - - -def test_get_slowusers(client): - response = client.get("/slowusers/") - assert response.status_code == 200, response.text - data = response.json() - assert "email" in data[0] - assert "id" in data[0] - - -def test_create_item(client): - item = {"title": "Foo", "description": "Something that fights"} - response = client.post("/users/1/items/", json=item) - assert response.status_code == 200, response.text - item_data = response.json() - assert item["title"] == item_data["title"] - assert item["description"] == item_data["description"] - assert "id" in item_data - assert "owner_id" in item_data - response = client.get("/users/1") - assert response.status_code == 200, response.text - user_data = response.json() - item_to_check = [it for it in user_data["items"] if it["id"] == item_data["id"]][0] - assert item_to_check["title"] == item["title"] - assert item_to_check["description"] == item["description"] - response = client.get("/users/1") - assert response.status_code == 200, response.text - user_data = response.json() - item_to_check = [it for it in user_data["items"] if it["id"] == item_data["id"]][0] - assert item_to_check["title"] == item["title"] - assert item_to_check["description"] == item["description"] - - -def test_read_items(client): - response = client.get("/items/") - assert response.status_code == 200, response.text - data = response.json() - assert data - first_item = data[0] - assert "title" in first_item - assert "description" in first_item diff --git a/tests/test_tutorial/test_sub_applications/test_tutorial001.py b/tests/test_tutorial/test_sub_applications/test_tutorial001.py index 00e9aec577..0790d207be 100644 --- a/tests/test_tutorial/test_sub_applications/test_tutorial001.py +++ b/tests/test_tutorial/test_sub_applications/test_tutorial001.py @@ -5,7 +5,7 @@ from docs_src.sub_applications.tutorial001 import app client = TestClient(app) openapi_schema_main = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/app": { @@ -23,7 +23,7 @@ openapi_schema_main = { }, } openapi_schema_sub = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/sub": { diff --git a/tests/test_tutorial/test_templates/test_tutorial001.py b/tests/test_tutorial/test_templates/test_tutorial001.py index bfee5c0902..4d4729425e 100644 --- a/tests/test_tutorial/test_templates/test_tutorial001.py +++ b/tests/test_tutorial/test_templates/test_tutorial001.py @@ -16,7 +16,10 @@ def test_main(): client = TestClient(app) response = client.get("/items/foo") assert response.status_code == 200, response.text - assert b"<h1>Item ID: foo</h1>" in response.content + assert ( + b'<h1><a href="http://testserver/items/foo">Item ID: foo</a></h1>' + in response.content + ) response = client.get("/static/styles.css") assert response.status_code == 200, response.text assert b"color: green;" in response.content diff --git a/tests/test_tutorial/test_testing/test_main.py b/tests/test_tutorial/test_testing/test_main.py index e6747fffd0..fe34980813 100644 --- a/tests/test_tutorial/test_testing/test_main.py +++ b/tests/test_tutorial/test_testing/test_main.py @@ -1,30 +1,28 @@ from docs_src.app_testing.test_main import client, test_read_main -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Main", - "operationId": "read_main__get", - } - } - }, -} + +def test_main(): + test_read_main() def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_main(): - test_read_main() + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Main", + "operationId": "read_main__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_testing/test_main_b_an.py b/tests/test_tutorial/test_testing/test_main_b_an.py new file mode 100644 index 0000000000..b64c5f7107 --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main_b_an.py @@ -0,0 +1,10 @@ +from docs_src.app_testing.app_b_an import test_main + + +def test_app(): + test_main.test_create_existing_item() + test_main.test_create_item() + test_main.test_create_item_bad_token() + test_main.test_read_inexistent_item() + test_main.test_read_item() + test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_main_b_an_py310.py b/tests/test_tutorial/test_testing/test_main_b_an_py310.py new file mode 100644 index 0000000000..194700b6dd --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main_b_an_py310.py @@ -0,0 +1,13 @@ +from ...utils import needs_py310 + + +@needs_py310 +def test_app(): + from docs_src.app_testing.app_b_an_py310 import test_main + + test_main.test_create_existing_item() + test_main.test_create_item() + test_main.test_create_item_bad_token() + test_main.test_read_inexistent_item() + test_main.test_read_item() + test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_main_b_an_py39.py b/tests/test_tutorial/test_testing/test_main_b_an_py39.py new file mode 100644 index 0000000000..2f8a13623f --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main_b_an_py39.py @@ -0,0 +1,13 @@ +from ...utils import needs_py39 + + +@needs_py39 +def test_app(): + from docs_src.app_testing.app_b_an_py39 import test_main + + test_main.test_create_existing_item() + test_main.test_create_item() + test_main.test_create_item_bad_token() + test_main.test_read_inexistent_item() + test_main.test_read_item() + test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_tutorial001.py b/tests/test_tutorial/test_testing/test_tutorial001.py index 7dea477f0e..471e896c93 100644 --- a/tests/test_tutorial/test_testing/test_tutorial001.py +++ b/tests/test_tutorial/test_testing/test_tutorial001.py @@ -1,30 +1,28 @@ from docs_src.app_testing.tutorial001 import client, test_read_main -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Main", - "operationId": "read_main__get", - } - } - }, -} + +def test_main(): + test_read_main() def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_main(): - test_read_main() + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Main", + "operationId": "read_main__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_testing/test_tutorial003.py b/tests/test_tutorial/test_testing/test_tutorial003.py index d9e16390ed..2a5d670712 100644 --- a/tests/test_tutorial/test_testing/test_tutorial003.py +++ b/tests/test_tutorial/test_testing/test_tutorial003.py @@ -1,5 +1,7 @@ -from docs_src.app_testing.tutorial003 import test_read_items +import pytest def test_main(): + with pytest.warns(DeprecationWarning): + from docs_src.app_testing.tutorial003 import test_read_items test_read_items() diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py new file mode 100644 index 0000000000..fc1f9149a6 --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py @@ -0,0 +1,56 @@ +from docs_src.dependency_testing.tutorial001_an import ( + app, + client, + test_override_in_items, + test_override_in_items_with_params, + test_override_in_items_with_q, +) + + +def test_override_in_items_run(): + test_override_in_items() + + +def test_override_in_items_with_q_run(): + test_override_in_items_with_q() + + +def test_override_in_items_with_params_run(): + test_override_in_items_with_params() + + +def test_override_in_users(): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_users_with_q(): + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_users_with_params(): + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_normal_app(): + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py new file mode 100644 index 0000000000..a3d27f47f4 --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py @@ -0,0 +1,75 @@ +from ...utils import needs_py310 + + +@needs_py310 +def test_override_in_items_run(): + from docs_src.dependency_testing.tutorial001_an_py310 import test_override_in_items + + test_override_in_items() + + +@needs_py310 +def test_override_in_items_with_q_run(): + from docs_src.dependency_testing.tutorial001_an_py310 import ( + test_override_in_items_with_q, + ) + + test_override_in_items_with_q() + + +@needs_py310 +def test_override_in_items_with_params_run(): + from docs_src.dependency_testing.tutorial001_an_py310 import ( + test_override_in_items_with_params, + ) + + test_override_in_items_with_params() + + +@needs_py310 +def test_override_in_users(): + from docs_src.dependency_testing.tutorial001_an_py310 import client + + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_q(): + from docs_src.dependency_testing.tutorial001_an_py310 import client + + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_params(): + from docs_src.dependency_testing.tutorial001_an_py310 import client + + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_normal_app(): + from docs_src.dependency_testing.tutorial001_an_py310 import app, client + + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py new file mode 100644 index 0000000000..f03ed5e07a --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py @@ -0,0 +1,75 @@ +from ...utils import needs_py39 + + +@needs_py39 +def test_override_in_items_run(): + from docs_src.dependency_testing.tutorial001_an_py39 import test_override_in_items + + test_override_in_items() + + +@needs_py39 +def test_override_in_items_with_q_run(): + from docs_src.dependency_testing.tutorial001_an_py39 import ( + test_override_in_items_with_q, + ) + + test_override_in_items_with_q() + + +@needs_py39 +def test_override_in_items_with_params_run(): + from docs_src.dependency_testing.tutorial001_an_py39 import ( + test_override_in_items_with_params, + ) + + test_override_in_items_with_params() + + +@needs_py39 +def test_override_in_users(): + from docs_src.dependency_testing.tutorial001_an_py39 import client + + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +@needs_py39 +def test_override_in_users_with_q(): + from docs_src.dependency_testing.tutorial001_an_py39 import client + + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py39 +def test_override_in_users_with_params(): + from docs_src.dependency_testing.tutorial001_an_py39 import client + + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py39 +def test_normal_app(): + from docs_src.dependency_testing.tutorial001_an_py39 import app, client + + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py new file mode 100644 index 0000000000..776b916ff0 --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py @@ -0,0 +1,75 @@ +from ...utils import needs_py310 + + +@needs_py310 +def test_override_in_items_run(): + from docs_src.dependency_testing.tutorial001_py310 import test_override_in_items + + test_override_in_items() + + +@needs_py310 +def test_override_in_items_with_q_run(): + from docs_src.dependency_testing.tutorial001_py310 import ( + test_override_in_items_with_q, + ) + + test_override_in_items_with_q() + + +@needs_py310 +def test_override_in_items_with_params_run(): + from docs_src.dependency_testing.tutorial001_py310 import ( + test_override_in_items_with_params, + ) + + test_override_in_items_with_params() + + +@needs_py310 +def test_override_in_users(): + from docs_src.dependency_testing.tutorial001_py310 import client + + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_q(): + from docs_src.dependency_testing.tutorial001_py310 import client + + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_params(): + from docs_src.dependency_testing.tutorial001_py310 import client + + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_normal_app(): + from docs_src.dependency_testing.tutorial001_py310 import app, client + + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an.py b/tests/test_tutorial/test_websockets/test_tutorial002_an.py new file mode 100644 index 0000000000..ec78d70d30 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_an.py @@ -0,0 +1,88 @@ +import pytest +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from docs_src.websockets.tutorial002_an import app + + +def test_main(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"<!DOCTYPE html>" in response.content + + +def test_websocket_with_cookie(): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +def test_websocket_with_header(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +def test_websocket_with_header_and_query(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +def test_websocket_no_credentials(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +def test_websocket_invalid_data(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py b/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py new file mode 100644 index 0000000000..23b4bcb788 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from ...utils import needs_py310 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial002_an_py310 import app + + return app + + +@needs_py310 +def test_main(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"<!DOCTYPE html>" in response.content + + +@needs_py310 +def test_websocket_with_cookie(app: FastAPI): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +@needs_py310 +def test_websocket_with_header(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +@needs_py310 +def test_websocket_with_header_and_query(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +@needs_py310 +def test_websocket_no_credentials(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +@needs_py310 +def test_websocket_invalid_data(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py b/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py new file mode 100644 index 0000000000..2d77f05b38 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from ...utils import needs_py39 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial002_an_py39 import app + + return app + + +@needs_py39 +def test_main(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"<!DOCTYPE html>" in response.content + + +@needs_py39 +def test_websocket_with_cookie(app: FastAPI): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +@needs_py39 +def test_websocket_with_header(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +@needs_py39 +def test_websocket_with_header_and_query(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +@needs_py39 +def test_websocket_no_credentials(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +@needs_py39 +def test_websocket_invalid_data(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_py310.py b/tests/test_tutorial/test_websockets/test_tutorial002_py310.py new file mode 100644 index 0000000000..03bc27bdfd --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_py310.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from ...utils import needs_py310 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial002_py310 import app + + return app + + +@needs_py310 +def test_main(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"<!DOCTYPE html>" in response.content + + +@needs_py310 +def test_websocket_with_cookie(app: FastAPI): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +@needs_py310 +def test_websocket_with_header(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +@needs_py310 +def test_websocket_with_header_and_query(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +@needs_py310 +def test_websocket_no_credentials(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +@needs_py310 +def test_websocket_invalid_data(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial003_py39.py b/tests/test_tutorial/test_websockets/test_tutorial003_py39.py new file mode 100644 index 0000000000..06c4a92796 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial003_py39.py @@ -0,0 +1,50 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial003_py39 import app + + return app + + +@pytest.fixture(name="html") +def get_html(): + from docs_src.websockets.tutorial003_py39 import html + + return html + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + + return client + + +@needs_py39 +def test_get(client: TestClient, html: str): + response = client.get("/") + assert response.text == html + + +@needs_py39 +def test_websocket_handle_disconnection(client: TestClient): + with client.websocket_connect("/ws/1234") as connection, client.websocket_connect( + "/ws/5678" + ) as connection_two: + connection.send_text("Hello from 1234") + data1 = connection.receive_text() + assert data1 == "You wrote: Hello from 1234" + data2 = connection_two.receive_text() + client1_says = "Client #1234 says: Hello from 1234" + assert data2 == client1_says + data1 = connection.receive_text() + assert data1 == client1_says + connection_two.close() + data1 = connection.receive_text() + assert data1 == "Client #5678 left the chat" diff --git a/tests/test_typing_python39.py b/tests/test_typing_python39.py index b1ea635e77..26775efd48 100644 --- a/tests/test_typing_python39.py +++ b/tests/test_typing_python39.py @@ -1,10 +1,10 @@ from fastapi import FastAPI from fastapi.testclient import TestClient -from .utils import needs_py39 +from .utils import needs_py310 -@needs_py39 +@needs_py310 def test_typing(): types = { list[int]: [1, 2, 3], diff --git a/tests/test_union_body.py b/tests/test_union_body.py index 3e424de07d..c15acacd18 100644 --- a/tests/test_union_body.py +++ b/tests/test_union_body.py @@ -1,5 +1,6 @@ from typing import Optional, Union +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -22,95 +23,6 @@ def save_union_body(item: Union[OtherItem, Item]): client = TestClient(app) -item_openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Save Union Body", - "operationId": "save_union_body_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Item", - "anyOf": [ - {"$ref": "#/components/schemas/OtherItem"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "OtherItem": { - "title": "OtherItem", - "required": ["price"], - "type": "object", - "properties": {"price": {"title": "Price", "type": "integer"}}, - }, - "Item": { - "title": "Item", - "type": "object", - "properties": {"name": {"title": "Name", "type": "string"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_item_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == item_openapi_schema - def test_post_other_item(): response = client.post("/items/", json={"price": 100}) @@ -122,3 +34,103 @@ def test_post_item(): response = client.post("/items/", json={"name": "Foo"}) assert response.status_code == 200, response.text assert response.json() == {"item": {"name": "Foo"}} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Save Union Body", + "operationId": "save_union_body_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Item", + "anyOf": [ + {"$ref": "#/components/schemas/OtherItem"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "OtherItem": { + "title": "OtherItem", + "required": ["price"], + "type": "object", + "properties": {"price": {"title": "Price", "type": "integer"}}, + }, + "Item": { + "title": "Item", + "type": "object", + "properties": IsDict( + { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"name": {"title": "Name", "type": "string"}} + ), + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_union_inherited_body.py b/tests/test_union_inherited_body.py index 9ee981b24e..ef75d459ea 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -1,5 +1,6 @@ from typing import Optional, Union +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -23,99 +24,6 @@ def save_union_different_body(item: Union[ExtendedItem, Item]): client = TestClient(app) -inherited_item_openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Save Union Different Body", - "operationId": "save_union_different_body_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Item", - "anyOf": [ - {"$ref": "#/components/schemas/ExtendedItem"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "type": "object", - "properties": {"name": {"title": "Name", "type": "string"}}, - }, - "ExtendedItem": { - "title": "ExtendedItem", - "required": ["age"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_inherited_item_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == inherited_item_openapi_schema - - def test_post_extended_item(): response = client.post("/items/", json={"name": "Foo", "age": 5}) assert response.status_code == 200, response.text @@ -126,3 +34,115 @@ def test_post_item(): response = client.post("/items/", json={"name": "Foo"}) assert response.status_code == 200, response.text assert response.json() == {"item": {"name": "Foo"}} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Save Union Different Body", + "operationId": "save_union_different_body_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Item", + "anyOf": [ + {"$ref": "#/components/schemas/ExtendedItem"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ) + }, + }, + "ExtendedItem": { + "title": "ExtendedItem", + "required": ["age"], + "type": "object", + "properties": { + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_validate_response.py b/tests/test_validate_response.py index 62f51c960d..cd97007a44 100644 --- a/tests/test_validate_response.py +++ b/tests/test_validate_response.py @@ -2,8 +2,9 @@ from typing import List, Optional, Union import pytest from fastapi import FastAPI +from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel app = FastAPI() @@ -50,12 +51,12 @@ client = TestClient(app) def test_invalid(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalid") def test_invalid_none(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalidnone") @@ -74,10 +75,10 @@ def test_valid_none_none(): def test_double_invalid(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/innerinvalid") def test_invalid_list(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalidlist") diff --git a/tests/test_validate_response_dataclass.py b/tests/test_validate_response_dataclass.py index f2cfa7a11b..0415988a0b 100644 --- a/tests/test_validate_response_dataclass.py +++ b/tests/test_validate_response_dataclass.py @@ -2,8 +2,8 @@ from typing import List, Optional import pytest from fastapi import FastAPI +from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient -from pydantic import ValidationError from pydantic.dataclasses import dataclass app = FastAPI() @@ -39,15 +39,15 @@ client = TestClient(app) def test_invalid(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalid") def test_double_invalid(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/innerinvalid") def test_invalid_list(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalidlist") diff --git a/docs/zh/overrides/.gitignore b/tests/test_validate_response_recursive/__init__.py similarity index 100% rename from docs/zh/overrides/.gitignore rename to tests/test_validate_response_recursive/__init__.py diff --git a/tests/test_validate_response_recursive.py b/tests/test_validate_response_recursive/app_pv1.py similarity index 58% rename from tests/test_validate_response_recursive.py rename to tests/test_validate_response_recursive/app_pv1.py index 3a4b10e0ca..4cfc4b3eef 100644 --- a/tests/test_validate_response_recursive.py +++ b/tests/test_validate_response_recursive/app_pv1.py @@ -1,7 +1,6 @@ from typing import List from fastapi import FastAPI -from fastapi.testclient import TestClient from pydantic import BaseModel app = FastAPI() @@ -49,32 +48,3 @@ def get_recursive_submodel(): } ], } - - -client = TestClient(app) - - -def test_recursive(): - response = client.get("/items/recursive") - assert response.status_code == 200, response.text - assert response.json() == { - "sub_items": [{"name": "subitem", "sub_items": []}], - "name": "item", - } - - response = client.get("/items/recursive-submodel") - assert response.status_code == 200, response.text - assert response.json() == { - "name": "item", - "sub_items1": [ - { - "name": "subitem", - "sub_items2": [ - { - "name": "subsubitem", - "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], - } - ], - } - ], - } diff --git a/tests/test_validate_response_recursive/app_pv2.py b/tests/test_validate_response_recursive/app_pv2.py new file mode 100644 index 0000000000..8c93a83490 --- /dev/null +++ b/tests/test_validate_response_recursive/app_pv2.py @@ -0,0 +1,51 @@ +from typing import List + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class RecursiveItem(BaseModel): + sub_items: List["RecursiveItem"] = [] + name: str + + +RecursiveItem.model_rebuild() + + +class RecursiveSubitemInSubmodel(BaseModel): + sub_items2: List["RecursiveItemViaSubmodel"] = [] + name: str + + +class RecursiveItemViaSubmodel(BaseModel): + sub_items1: List[RecursiveSubitemInSubmodel] = [] + name: str + + +RecursiveSubitemInSubmodel.model_rebuild() +RecursiveItemViaSubmodel.model_rebuild() + + +@app.get("/items/recursive", response_model=RecursiveItem) +def get_recursive(): + return {"name": "item", "sub_items": [{"name": "subitem", "sub_items": []}]} + + +@app.get("/items/recursive-submodel", response_model=RecursiveItemViaSubmodel) +def get_recursive_submodel(): + return { + "name": "item", + "sub_items1": [ + { + "name": "subitem", + "sub_items2": [ + { + "name": "subsubitem", + "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], + } + ], + } + ], + } diff --git a/tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py b/tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py new file mode 100644 index 0000000000..de578ae031 --- /dev/null +++ b/tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py @@ -0,0 +1,33 @@ +from fastapi.testclient import TestClient + +from ..utils import needs_pydanticv1 + + +@needs_pydanticv1 +def test_recursive(): + from .app_pv1 import app + + client = TestClient(app) + response = client.get("/items/recursive") + assert response.status_code == 200, response.text + assert response.json() == { + "sub_items": [{"name": "subitem", "sub_items": []}], + "name": "item", + } + + response = client.get("/items/recursive-submodel") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "item", + "sub_items1": [ + { + "name": "subitem", + "sub_items2": [ + { + "name": "subsubitem", + "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], + } + ], + } + ], + } diff --git a/tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py b/tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py new file mode 100644 index 0000000000..7d45e7fe40 --- /dev/null +++ b/tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py @@ -0,0 +1,33 @@ +from fastapi.testclient import TestClient + +from ..utils import needs_pydanticv2 + + +@needs_pydanticv2 +def test_recursive(): + from .app_pv2 import app + + client = TestClient(app) + response = client.get("/items/recursive") + assert response.status_code == 200, response.text + assert response.json() == { + "sub_items": [{"name": "subitem", "sub_items": []}], + "name": "item", + } + + response = client.get("/items/recursive-submodel") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "item", + "sub_items1": [ + { + "name": "subitem", + "sub_items2": [ + { + "name": "subsubitem", + "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], + } + ], + } + ], + } diff --git a/tests/test_webhooks_security.py b/tests/test_webhooks_security.py new file mode 100644 index 0000000000..21a694cb54 --- /dev/null +++ b/tests/test_webhooks_security.py @@ -0,0 +1,126 @@ +from datetime import datetime + +from fastapi import FastAPI, Security +from fastapi.security import HTTPBearer +from fastapi.testclient import TestClient +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + +bearer_scheme = HTTPBearer() + + +class Subscription(BaseModel): + username: str + monthly_fee: float + start_date: datetime + + +@app.webhooks.post("new-subscription") +def new_subscription( + body: Subscription, token: Annotated[str, Security(bearer_scheme)] +): + """ + When a new user subscribes to your service we'll send you a POST request with this + data to the URL that you register for the event `new-subscription` in the dashboard. + """ + + +client = TestClient(app) + + +def test_dummy_webhook(): + # Just for coverage + new_subscription(body={}, token="Bearer 123") + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": {}, + "webhooks": { + "new-subscription": { + "post": { + "summary": "New Subscription", + "description": "When a new user subscribes to your service we'll send you a POST request with this\ndata to the URL that you register for the event `new-subscription` in the dashboard.", + "operationId": "new_subscriptionnew_subscription_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Subscription"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "security": [{"HTTPBearer": []}], + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Subscription": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "monthly_fee": {"type": "number", "title": "Monthly Fee"}, + "start_date": { + "type": "string", + "format": "date-time", + "title": "Start Date", + }, + }, + "type": "object", + "required": ["username", "monthly_fee", "start_date"], + "title": "Subscription", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + }, + "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}}, + }, + } diff --git a/tests/test_ws_dependencies.py b/tests/test_ws_dependencies.py new file mode 100644 index 0000000000..ccb1c4b7da --- /dev/null +++ b/tests/test_ws_dependencies.py @@ -0,0 +1,73 @@ +import json +from typing import List + +from fastapi import APIRouter, Depends, FastAPI, WebSocket +from fastapi.testclient import TestClient +from typing_extensions import Annotated + + +def dependency_list() -> List[str]: + return [] + + +DepList = Annotated[List[str], Depends(dependency_list)] + + +def create_dependency(name: str): + def fun(deps: DepList): + deps.append(name) + + return Depends(fun) + + +router = APIRouter(dependencies=[create_dependency("router")]) +prefix_router = APIRouter(dependencies=[create_dependency("prefix_router")]) +app = FastAPI(dependencies=[create_dependency("app")]) + + +@app.websocket("/", dependencies=[create_dependency("index")]) +async def index(websocket: WebSocket, deps: DepList): + await websocket.accept() + await websocket.send_text(json.dumps(deps)) + await websocket.close() + + +@router.websocket("/router", dependencies=[create_dependency("routerindex")]) +async def routerindex(websocket: WebSocket, deps: DepList): + await websocket.accept() + await websocket.send_text(json.dumps(deps)) + await websocket.close() + + +@prefix_router.websocket("/", dependencies=[create_dependency("routerprefixindex")]) +async def routerprefixindex(websocket: WebSocket, deps: DepList): + await websocket.accept() + await websocket.send_text(json.dumps(deps)) + await websocket.close() + + +app.include_router(router, dependencies=[create_dependency("router2")]) +app.include_router( + prefix_router, prefix="/prefix", dependencies=[create_dependency("prefix_router2")] +) + + +def test_index(): + client = TestClient(app) + with client.websocket_connect("/") as websocket: + data = json.loads(websocket.receive_text()) + assert data == ["app", "index"] + + +def test_routerindex(): + client = TestClient(app) + with client.websocket_connect("/router") as websocket: + data = json.loads(websocket.receive_text()) + assert data == ["app", "router2", "router", "routerindex"] + + +def test_routerprefixindex(): + client = TestClient(app) + with client.websocket_connect("/prefix/") as websocket: + data = json.loads(websocket.receive_text()) + assert data == ["app", "prefix_router2", "prefix_router", "routerprefixindex"] diff --git a/tests/test_ws_router.py b/tests/test_ws_router.py index c312821e96..240a42bb0c 100644 --- a/tests/test_ws_router.py +++ b/tests/test_ws_router.py @@ -1,4 +1,16 @@ -from fastapi import APIRouter, Depends, FastAPI, WebSocket +import functools + +import pytest +from fastapi import ( + APIRouter, + Depends, + FastAPI, + Header, + WebSocket, + WebSocketDisconnect, + status, +) +from fastapi.middleware import Middleware from fastapi.testclient import TestClient router = APIRouter() @@ -63,9 +75,44 @@ async def router_native_prefix_ws(websocket: WebSocket): await websocket.close() -app.include_router(router) -app.include_router(prefix_router, prefix="/prefix") -app.include_router(native_prefix_route) +async def ws_dependency_err(): + raise NotImplementedError() + + +@router.websocket("/depends-err/") +async def router_ws_depends_err(websocket: WebSocket, data=Depends(ws_dependency_err)): + pass # pragma: no cover + + +async def ws_dependency_validate(x_missing: str = Header()): + pass # pragma: no cover + + +@router.websocket("/depends-validate/") +async def router_ws_depends_validate( + websocket: WebSocket, data=Depends(ws_dependency_validate) +): + pass # pragma: no cover + + +class CustomError(Exception): + pass + + +@router.websocket("/custom_error/") +async def router_ws_custom_error(websocket: WebSocket): + raise CustomError() + + +def make_app(app=None, **kwargs): + app = app or FastAPI(**kwargs) + app.include_router(router) + app.include_router(prefix_router, prefix="/prefix") + app.include_router(native_prefix_route) + return app + + +app = make_app(app) def test_app(): @@ -125,3 +172,100 @@ def test_router_with_params(): assert data == "path/to/file" data = websocket.receive_text() assert data == "a_query_param" + + +def test_wrong_uri(): + """ + Verify that a websocket connection to a non-existent endpoing returns in a shutdown + """ + client = TestClient(app) + with pytest.raises(WebSocketDisconnect) as e: + with client.websocket_connect("/no-router/"): + pass # pragma: no cover + assert e.value.code == status.WS_1000_NORMAL_CLOSURE + + +def websocket_middleware(middleware_func): + """ + Helper to create a Starlette pure websocket middleware + """ + + def middleware_constructor(app): + @functools.wraps(app) + async def wrapped_app(scope, receive, send): + if scope["type"] != "websocket": + return await app(scope, receive, send) # pragma: no cover + + async def call_next(): + return await app(scope, receive, send) + + websocket = WebSocket(scope, receive=receive, send=send) + return await middleware_func(websocket, call_next) + + return wrapped_app + + return middleware_constructor + + +def test_depend_validation(): + """ + Verify that a validation in a dependency invokes the correct exception handler + """ + caught = [] + + @websocket_middleware + async def catcher(websocket, call_next): + try: + return await call_next() + except Exception as e: # pragma: no cover + caught.append(e) + raise + + myapp = make_app(middleware=[Middleware(catcher)]) + + client = TestClient(myapp) + with pytest.raises(WebSocketDisconnect) as e: + with client.websocket_connect("/depends-validate/"): + pass # pragma: no cover + # the validation error does produce a close message + assert e.value.code == status.WS_1008_POLICY_VIOLATION + # and no error is leaked + assert caught == [] + + +def test_depend_err_middleware(): + """ + Verify that it is possible to write custom WebSocket middleware to catch errors + """ + + @websocket_middleware + async def errorhandler(websocket: WebSocket, call_next): + try: + return await call_next() + except Exception as e: + await websocket.close(code=status.WS_1006_ABNORMAL_CLOSURE, reason=repr(e)) + + myapp = make_app(middleware=[Middleware(errorhandler)]) + client = TestClient(myapp) + with pytest.raises(WebSocketDisconnect) as e: + with client.websocket_connect("/depends-err/"): + pass # pragma: no cover + assert e.value.code == status.WS_1006_ABNORMAL_CLOSURE + assert "NotImplementedError" in e.value.reason + + +def test_depend_err_handler(): + """ + Verify that it is possible to write custom WebSocket middleware to catch errors + """ + + async def custom_handler(websocket: WebSocket, exc: CustomError) -> None: + await websocket.close(1002, "foo") + + myapp = make_app(exception_handlers={CustomError: custom_handler}) + client = TestClient(myapp) + with pytest.raises(WebSocketDisconnect) as e: + with client.websocket_connect("/custom_error/"): + pass # pragma: no cover + assert e.value.code == 1002 + assert "foo" in e.value.reason diff --git a/tests/utils.py b/tests/utils.py index 5305424c45..460c028f7f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,8 +1,11 @@ import sys import pytest +from fastapi._compat import PYDANTIC_V2 needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+") needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) +needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2") +needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1")