diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/DISCUSSION_TEMPLATE/questions.yml similarity index 87% rename from .github/ISSUE_TEMPLATE/question.yml rename to .github/DISCUSSION_TEMPLATE/questions.yml index 3b16b4ad0..3726b7d18 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 👆 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 55749398f..a8f4c4de2 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 322b6536a..000000000 --- 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 000000000..c01e34b6d --- /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/notify-translations/app/main.py b/.github/actions/notify-translations/app/main.py index d4ba0ecfc..494fe6ad8 100644 --- a/.github/actions/notify-translations/app/main.py +++ b/.github/actions/notify-translations/app/main.py @@ -1,10 +1,11 @@ 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 @@ -13,12 +14,172 @@ 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: + 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"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}" + 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}" + if already_notified_comment: + logging.info( + f"This PR #{pr.number} was already notified in comment: {already_notified_comment.url}" ) - 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: - logging.info( - f"Changing labels in a closed PR doesn't trigger comments, PR: {pr.number}" - ) + 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 4338e1326..000000000 --- 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/app/main.py b/.github/actions/people/app/main.py index 31756a5fc..2bf59f25e 100644 --- a/.github/actions/people/app/main.py +++ b/.github/actions/people/app/main.py @@ -4,7 +4,7 @@ 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 @@ -12,6 +12,50 @@ from github import Github from pydantic import BaseModel, BaseSettings, SecretStr 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 +175,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 +208,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 +310,9 @@ class PRsResponse(BaseModel): data: PRsResponseData +# Sponsors + + class SponsorEntity(BaseModel): login: str avatarUrl: str @@ -264,10 +358,16 @@ class Settings(BaseSettings): 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,10 +375,16 @@ 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(response.text) + raise RuntimeError(response.text) return data @@ -288,6 +394,21 @@ def get_graphql_issue_edges(*, settings: Settings, after: Union[str, None] = Non 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.parse_obj(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) @@ -300,7 +421,7 @@ def get_graphql_sponsor_edges(*, settings: Settings, after: Union[str, None] = N 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 +447,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 @@ -425,13 +611,13 @@ if __name__ == "__main__": logging.info(f"Using config: {settings.json()}") g = Github(settings.input_standard_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 +626,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 +639,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/workflows/build-docs.yml b/.github/workflows/build-docs.yml index b9bd521b3..68a180e38 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -7,7 +7,7 @@ on: types: [opened, synchronize] jobs: build-docs: - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - name: Dump GitHub context env: @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.11" - uses: actions/cache@v3 id: cache with: diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 2fcb7595e..fdd24414c 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -4,6 +4,7 @@ on: pull_request_target: types: - labeled + - closed jobs: notify-translations: diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index 4b47b4072..cca1329e7 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -15,6 +15,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + # 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 diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 7d31a9c64..cf0db59ab 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -16,7 +16,7 @@ jobs: rm -rf ./site mkdir ./site - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.24.2 + uses: dawidd6/action-download-artifact@v2.26.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-docs.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8ffb493a4..c2fdb8e17 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,7 +31,7 @@ jobs: - name: Build distribution run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.5.2 + uses: pypa/gh-action-pypi-publish@v1.6.4 with: password: ${{ secrets.PYPI_API_TOKEN }} - name: Dump GitHub context diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 7559c24c0..421720433 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -20,7 +20,7 @@ jobs: - run: pip install smokeshow - - uses: dawidd6/action-download-artifact@v2.24.2 + - uses: dawidd6/action-download-artifact@v2.26.0 with: workflow: test.yml commit: ${{ github.event.workflow_run.head_sha }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ddc43c942..1235516d3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,3 +75,19 @@ jobs: with: name: coverage-html path: htmlcov + + # 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: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 96f097caa..25e797d24 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 @@ -12,20 +14,20 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v3.2.2 + rev: v3.3.1 hooks: - id: pyupgrade args: - --py3-plus - --keep-runtime-typing - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.138 + rev: v0.0.254 hooks: - id: ruff args: - --fix - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort name: isort (python) @@ -36,7 +38,7 @@ repos: name: isort (pyi) types: [pyi] - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 23.1.0 hooks: - id: black ci: diff --git a/README.md b/README.md index 7c4a6c4b4..39030ef52 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,14 @@ The key features are: - - + + @@ -102,6 +102,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 diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index d549f37a3..7f4a490d8 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -80,7 +82,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -111,6 +113,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -131,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 8c3c42b5f..f6e0a6b01 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -81,7 +83,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -112,6 +114,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -132,6 +136,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index 934c5842b..af5810778 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,13 @@ articles: english: + - 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/ @@ -300,6 +308,11 @@ articles: author_link: https://fullstackstation.com/author/figonking/ link: https://fullstackstation.com/fastapi-trien-khai-bang-docker/ title: 'FASTAPI: TRIỂN KHAI BẰNG DOCKER' + 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__` diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 1953df801..2a8573f19 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -2,15 +2,12 @@ sponsors: - - login: jina-ai avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4 url: https://github.com/jina-ai -- - login: Doist - avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4 - url: https://github.com/Doist - - login: cryptapi +- - login: armand-sauzay + avatarUrl: https://avatars.githubusercontent.com/u/35524799?u=56e3e944bfe62770d1709c09552d2efc6d285ca6&v=4 + url: https://github.com/armand-sauzay +- - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi - - login: jrobbins-LiveData - avatarUrl: https://avatars.githubusercontent.com/u/79278744?u=bae8175fc3f09db281aca1f97a9ddc1a914a8c4f&v=4 - url: https://github.com/jrobbins-LiveData - - login: nihpo avatarUrl: https://avatars.githubusercontent.com/u/1841030?u=0264956d7580f7e46687a762a7baa629f84cf97c&v=4 url: https://github.com/nihpo @@ -32,6 +29,9 @@ sponsors: - login: investsuite avatarUrl: https://avatars.githubusercontent.com/u/73833632?v=4 url: https://github.com/investsuite + - 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 @@ -44,36 +44,24 @@ sponsors: - - 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: matallan - avatarUrl: https://avatars.githubusercontent.com/u/12107723?v=4 - url: https://github.com/matallan - 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: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP - - login: johnadjei avatarUrl: https://avatars.githubusercontent.com/u/767860?v=4 url: https://github.com/johnadjei - - login: gvisniuc - avatarUrl: https://avatars.githubusercontent.com/u/1614747?u=502dfdb2b087ddcf5460026297c98c7907bc2795&v=4 - url: https://github.com/gvisniuc - login: HiredScore avatarUrl: https://avatars.githubusercontent.com/u/3908850?v=4 url: https://github.com/HiredScore + - login: ianshan0915 + avatarUrl: https://avatars.githubusercontent.com/u/5893101?u=a178d247d882578b1d1ef214b2494e52eb28634c&v=4 + url: https://github.com/ianshan0915 - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie @@ -83,6 +71,9 @@ sponsors: - - 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: AccentDesign avatarUrl: https://avatars.githubusercontent.com/u/2429332?v=4 url: https://github.com/AccentDesign @@ -110,9 +101,6 @@ sponsors: - - login: indeedeng avatarUrl: https://avatars.githubusercontent.com/u/2905043?v=4 url: https://github.com/indeedeng - - login: A-Edge - avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 - url: https://github.com/A-Edge - - login: Kludex avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex @@ -134,9 +122,6 @@ sponsors: - 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 @@ -152,30 +137,24 @@ sponsors: - login: jqueguiner avatarUrl: https://avatars.githubusercontent.com/u/690878?u=bd65cc1f228ce6455e56dfaca3ef47c33bc7c3b0&v=4 url: https://github.com/jqueguiner - - login: alexsantos - avatarUrl: https://avatars.githubusercontent.com/u/932219?v=4 - url: https://github.com/alexsantos + - login: iobruno + avatarUrl: https://avatars.githubusercontent.com/u/901651?u=460bc34ac298dca9870aafe3a1560a2ae789bc4a&v=4 + url: https://github.com/iobruno - 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: coffeewasmyidea avatarUrl: https://avatars.githubusercontent.com/u/1636488?u=8e32a4f200eff54dd79cd79d55d254bfce5e946d&v=4 url: https://github.com/coffeewasmyidea + - login: jonakoudijs + avatarUrl: https://avatars.githubusercontent.com/u/1906344?u=5ca0c9a1a89b6a2ba31abe35c66bdc07af60a632&v=4 + url: https://github.com/jonakoudijs - login: corleyma - avatarUrl: https://avatars.githubusercontent.com/u/2080732?u=aed2ff652294a87d666b1c3f6dbe98104db76d26&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/2080732?u=c61f9a4bbc45a45f5d855f93e5f6e0fc8b32c468&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 @@ -185,51 +164,36 @@ sponsors: - login: ColliotL avatarUrl: https://avatars.githubusercontent.com/u/3412402?u=ca64b07ecbef2f9da1cc2cac3f37522aa4814902&v=4 url: https://github.com/ColliotL - - login: grillazz - avatarUrl: https://avatars.githubusercontent.com/u/3415861?u=453cd1725c8d7fe3e258016bc19cff861d4fcb53&v=4 - url: https://github.com/grillazz - 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: 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 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 - login: oliverxchen avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 url: https://github.com/oliverxchen - - login: Rey8d01 - avatarUrl: https://avatars.githubusercontent.com/u/4836190?u=5942598a23a377602c1669522334ab5ebeaf9165&v=4 - url: https://github.com/Rey8d01 - - login: ScrimForever - avatarUrl: https://avatars.githubusercontent.com/u/5040124?u=091ec38bfe16d6e762099e91309b59f248616a65&v=4 - url: https://github.com/ScrimForever - login: ennui93 avatarUrl: https://avatars.githubusercontent.com/u/5300907?u=5b5452725ddb391b2caaebf34e05aba873591c3a&v=4 url: https://github.com/ennui93 - 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 @@ -248,6 +212,9 @@ sponsors: - login: Rehket avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 url: https://github.com/Rehket + - login: ValentinCalomme + avatarUrl: https://avatars.githubusercontent.com/u/7288672?u=e09758c7a36c49f0fb3574abe919cbd344fdc2d6&v=4 + url: https://github.com/ValentinCalomme - login: hiancdtrsnm avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4 url: https://github.com/hiancdtrsnm @@ -257,15 +224,9 @@ sponsors: - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow - - login: bapi24 - avatarUrl: https://avatars.githubusercontent.com/u/11890901?u=45cc721d8f66ad2f62b086afc3d4761d0c16b9c6&v=4 - url: https://github.com/bapi24 - 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: dannywade avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 url: https://github.com/dannywade @@ -273,20 +234,26 @@ sponsors: 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 + avatarUrl: https://avatars.githubusercontent.com/u/15187159?u=7480e0eaf959e9c5dfe3a05286f2ea4588c0a3c6&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: abdalla19977 + avatarUrl: https://avatars.githubusercontent.com/u/17257234?v=4 + url: https://github.com/abdalla19977 - login: wedwardbeck avatarUrl: https://avatars.githubusercontent.com/u/19333237?u=1de4ae2bf8d59eb4c013f21d863cbe0f2010575f&v=4 url: https://github.com/wedwardbeck - - login: m4knV - avatarUrl: https://avatars.githubusercontent.com/u/19666130?u=843383978814886be93c137d10d2e20e9f13af07&v=4 - url: https://github.com/m4knV - login: Filimoa - avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=75e02d102d2ee3e3d793e555fa5c63045913ccb0&v=4 + 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: Pablongo24 + avatarUrl: https://avatars.githubusercontent.com/u/24843427?u=78a6798469889d7a0690449fc667c39e13d5c6a9&v=4 + url: https://github.com/Pablongo24 - login: Joeriksson avatarUrl: https://avatars.githubusercontent.com/u/25037079?v=4 url: https://github.com/Joeriksson @@ -317,9 +284,12 @@ sponsors: - login: ProteinQure avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 url: https://github.com/ProteinQure - - login: faviasono - avatarUrl: https://avatars.githubusercontent.com/u/37707874?u=f0b75ca4248987c08ed8fb8ed682e7e74d5d7091&v=4 - url: https://github.com/faviasono + - login: askurihin + avatarUrl: https://avatars.githubusercontent.com/u/37978981?v=4 + url: https://github.com/askurihin + - login: arleybri18 + avatarUrl: https://avatars.githubusercontent.com/u/39681546?u=5c028f81324b0e8c73b3c15bc4e7b0218d2ba0c3&v=4 + url: https://github.com/arleybri18 - login: ybressler avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4 url: https://github.com/ybressler @@ -329,18 +299,21 @@ sponsors: - 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: 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: thisistheplace + avatarUrl: https://avatars.githubusercontent.com/u/57633545?u=a3f3a7f8ace8511c6c067753f6eb6aee0db11ac6&v=4 + url: https://github.com/thisistheplace + - login: kyjoconn + avatarUrl: https://avatars.githubusercontent.com/u/58443406?u=a3e9c2acfb7ba62edda9334aba61cf027f41f789&v=4 + url: https://github.com/kyjoconn + - login: A-Edge + avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 + url: https://github.com/A-Edge - login: yakkonaut avatarUrl: https://avatars.githubusercontent.com/u/60633704?u=90a71fd631aa998ba4a96480788f017c9904e07b&v=4 url: https://github.com/yakkonaut @@ -350,31 +323,28 @@ sponsors: - 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: fpiem - avatarUrl: https://avatars.githubusercontent.com/u/77389987?u=f667a25cd4832b28801189013b74450e06cc232c&v=4 - url: https://github.com/fpiem - 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: osawa-koki + avatarUrl: https://avatars.githubusercontent.com/u/94336223?u=59c6fe6945bcbbaff87b2a794238671b060620d2&v=4 + url: https://github.com/osawa-koki - 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 +- - login: pawamoy + avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 + url: https://github.com/pawamoy + - login: linux-china avatarUrl: https://avatars.githubusercontent.com/u/46711?u=cd77c65338b158750eb84dc7ff1acf3209ccfc4f&v=4 url: https://github.com/linux-china - login: ddanier @@ -389,12 +359,6 @@ sponsors: - 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 - login: adamghill avatarUrl: https://avatars.githubusercontent.com/u/317045?u=f1349d5ffe84a19f324e204777859fbf69ddf633&v=4 url: https://github.com/adamghill @@ -404,9 +368,6 @@ sponsors: - login: dmig avatarUrl: https://avatars.githubusercontent.com/u/388564?v=4 url: https://github.com/dmig - - login: rinckd - avatarUrl: https://avatars.githubusercontent.com/u/546002?u=8ec88ab721a5636346f19dcd677a6f323058be8b&v=4 - url: https://github.com/rinckd - login: securancy avatarUrl: https://avatars.githubusercontent.com/u/606673?v=4 url: https://github.com/securancy @@ -419,39 +380,27 @@ sponsors: - 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: 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: 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 @@ -467,11 +416,14 @@ sponsors: - login: Baghdady92 avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 url: https://github.com/Baghdady92 + - login: KentShikama + avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4 + url: https://github.com/KentShikama - 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 + avatarUrl: https://avatars.githubusercontent.com/u/7719209?v=4 url: https://github.com/mattwelke - login: hcristea avatarUrl: https://avatars.githubusercontent.com/u/7814406?u=61d7a4fcf846983a4606788eac25e1c6c1209ba8&v=4 @@ -479,9 +431,6 @@ sponsors: - 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: xncbf avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=866a1311e4bd3ec5ae84185c4fcc99f397c883d7&v=4 url: https://github.com/xncbf @@ -506,9 +455,9 @@ sponsors: - 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: TheR1D + avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b2923ac17fe6e2a7c9ea14800351ddb92f79b100&v=4 + url: https://github.com/TheR1D - login: cdsre avatarUrl: https://avatars.githubusercontent.com/u/16945936?v=4 url: https://github.com/cdsre @@ -518,21 +467,18 @@ sponsors: - 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=4df05a7296c207b91c5d7c7a11c29df5ab313e2b&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: pers0n4 - avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=444441027bc2c9f9db68e8047d65ff23d25699cf&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4 url: https://github.com/pers0n4 - login: SebTota avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 url: https://github.com/SebTota + - 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 @@ -549,7 +495,7 @@ 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=76cdc0a8b4e88c7d3e58dccb4b2670839e1247b4&v=4 url: https://github.com/bnkc - login: declon avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 @@ -564,26 +510,14 @@ sponsors: 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=9ddf8023ca3326381ba8fb77285ae36598a15de3&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&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: arrrrrmin - avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=2a812c1a2ec58227ed01778837f255143de9df97&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=5265858add14a6822bd145f7547323cf078563e6&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: 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 @@ -602,21 +536,12 @@ sponsors: - login: realabja avatarUrl: https://avatars.githubusercontent.com/u/66185192?u=001e2dd9297784f4218997981b4e6fa8357bb70b&v=4 url: https://github.com/realabja - - login: alessio-proietti - avatarUrl: https://avatars.githubusercontent.com/u/67370599?u=8ac73db1e18e946a7681f173abdb640516f88515&v=4 - url: https://github.com/alessio-proietti - - login: pondDevThai - avatarUrl: https://avatars.githubusercontent.com/u/71592181?u=08af9a59bccfd8f6b101de1005aa9822007d0a44&v=4 - url: https://github.com/pondDevThai -- - login: wardal - avatarUrl: https://avatars.githubusercontent.com/u/15804042?v=4 - url: https://github.com/wardal - - login: gabrielmbmb - avatarUrl: https://avatars.githubusercontent.com/u/29572918?u=6d1e00b5d558e96718312ff910a2318f47cc3145&v=4 - url: https://github.com/gabrielmbmb + - login: garydsong + avatarUrl: https://avatars.githubusercontent.com/u/105745865?u=03cc1aa9c978be0020e5a1ce1ecca323dd6c8d65&v=4 + url: https://github.com/garydsong +- - login: Leon0824 + avatarUrl: https://avatars.githubusercontent.com/u/1922026?v=4 + url: https://github.com/Leon0824 - login: danburonline avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&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 diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 51940a6b1..412f4517a 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,158 +1,158 @@ maintainers: - login: tiangolo - answers: 1837 - prs: 360 + answers: 1827 + prs: 384 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 374 + count: 376 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu - count: 262 + count: 237 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&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: JarroVGIT count: 192 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: euri10 - count: 166 + count: 151 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 - login: phy25 - count: 130 + count: 126 avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 - login: iudeen - count: 87 + count: 116 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen +- login: jgould22 + count: 101 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 - 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: ghandic + count: 71 + avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 + url: https://github.com/ghandic - login: falkben - count: 59 + count: 57 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 url: https://github.com/falkben -- login: jgould22 - count: 55 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 - 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: insomnes - count: 46 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes - login: Dustyposa count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa +- login: insomnes + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes +- login: frankie567 + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 + url: https://github.com/frankie567 +- login: acidjunk + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk +- login: odiseo0 + count: 42 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 + url: https://github.com/odiseo0 - login: adriangb count: 40 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=75087f0cf0e9f725f3cd18a899218b6c63ae60d3&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=1e2c2c9b39f5c9b780fb933d8995cf08ec235a47&v=4 url: https://github.com/adriangb - login: includeamin - count: 39 + count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin - 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: 33 - 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: wshayes - count: 29 - avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 - url: https://github.com/wshayes +- login: yinziyan1206 + count: 34 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 +- login: chbndrhnns + count: 34 + avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 + url: https://github.com/chbndrhnns - login: panla - count: 29 + count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 url: https://github.com/panla -- login: ghandic - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 - url: https://github.com/ghandic +- 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: 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: caeser1996 + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 + url: https://github.com/caeser1996 +- login: rafsaf + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 + url: https://github.com/rafsaf - login: nsidnev - count: 22 + count: 20 avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 url: https://github.com/nsidnev +- login: acnebs + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 + url: https://github.com/acnebs - login: chris-allnutt - count: 21 + count: 20 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt - 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/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 + avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 + url: https://github.com/zoliknemet - login: nkhitrov count: 17 avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4 @@ -161,75 +161,79 @@ experts: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 url: https://github.com/harunyasar -- login: waynerv +- login: Hultner + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 + url: https://github.com/Hultner +- login: jonatasoli count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 - url: https://github.com/waynerv + avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 + url: https://github.com/jonatasoli - login: dstlny count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny -- login: jonatasoli +- login: jorgerpo count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 - url: https://github.com/jonatasoli -- login: mbroton + avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 + url: https://github.com/jorgerpo +- login: ghost count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 - url: https://github.com/mbroton + avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 + url: https://github.com/ghost +- login: simondale00 + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/33907262?v=4 + url: https://github.com/simondale00 - login: hellocoldworld count: 14 avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4 url: https://github.com/hellocoldworld -- login: haizaar - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/58201?u=dd40d99a3e1935d0b768f122bfe2258d6ea53b2b&v=4 - url: https://github.com/haizaar -- login: valentin994 +- login: waynerv + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 + url: https://github.com/waynerv +- login: mbroton 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 -- login: n8sty - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty + avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 + url: https://github.com/mbroton last_month_active: +- login: mr-st0rm + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/48455163?u=6b83550e4e70bea57cd2fdb41e717aeab7f64a91&v=4 + url: https://github.com/mr-st0rm +- login: caeser1996 + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 + url: https://github.com/caeser1996 +- login: ebottos94 + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 + url: https://github.com/ebottos94 - login: jgould22 - count: 9 + count: 6 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 -- login: yinziyan1206 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: iudeen - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen - login: Kludex count: 5 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex -- login: JarroVGIT - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 - url: https://github.com/JarroVGIT -- login: TheJumpyWizard +- login: clemens-tolboom count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/90986815?u=67e9c13c9f063dd4313db7beb64eaa2f3a37f1fe&v=4 - url: https://github.com/TheJumpyWizard -- login: mbroton + avatarUrl: https://avatars.githubusercontent.com/u/371014?v=4 + url: https://github.com/clemens-tolboom +- login: williamjamir + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/5083518?u=b76ca8e08b906a86fa195fb817dd94e8d9d3d8f6&v=4 + url: https://github.com/williamjamir +- login: nymous count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 - url: https://github.com/mbroton -- login: mateoradman + avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 + url: https://github.com/nymous +- login: frankie567 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/48420316?u=066f36b8e8e263b0d90798113b0f291d3266db7c&v=4 - url: https://github.com/mateoradman + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 + url: https://github.com/frankie567 top_contributors: - login: waynerv count: 25 @@ -239,6 +243,10 @@ top_contributors: count: 22 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi +- login: Kludex + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + url: https://github.com/Kludex - login: jaystone776 count: 17 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 @@ -247,10 +255,6 @@ top_contributors: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&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: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 @@ -269,7 +273,7 @@ top_contributors: url: https://github.com/Serrones - login: RunningIkkyu count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=efb5b45b55584450507834f279ce48d4d64dea2f&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 url: https://github.com/RunningIkkyu - login: hard-coders count: 7 @@ -279,6 +283,10 @@ top_contributors: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 url: https://github.com/rjNemo +- 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 @@ -295,10 +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: NinaHwang count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 - url: https://github.com/batlopes + avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4 + url: https://github.com/NinaHwang +- login: Xewus + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 @@ -327,25 +339,21 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas -- login: NinaHwang - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 - url: https://github.com/NinaHwang top_reviewers: - login: Kludex - count: 109 + count: 111 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan - count: 70 + count: 75 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan - login: yezz123 - count: 59 + count: 71 avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 url: https://github.com/yezz123 - login: tokusumi - count: 50 + count: 51 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi - login: waynerv @@ -360,6 +368,10 @@ top_reviewers: count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd +- login: iudeen + count: 44 + 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,22 +384,18 @@ top_reviewers: count: 33 avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4 url: https://github.com/AdrianDeAnda -- login: iudeen - count: 33 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen - login: ArcLightSlavik 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: 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 @@ -396,6 +404,14 @@ top_reviewers: count: 23 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu +- login: LorhanSohaky + count: 22 + avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 + url: https://github.com/LorhanSohaky +- login: rjNemo + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 + url: https://github.com/rjNemo - login: hard-coders count: 20 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 @@ -404,10 +420,10 @@ top_reviewers: 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: odiseo0 + count: 19 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 + url: https://github.com/odiseo0 - login: Smlep count: 17 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 @@ -436,22 +452,26 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 -- login: odiseo0 +- login: Ryandaydev count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 - url: https://github.com/odiseo0 + avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=809f3d1074d04bbc28012a7f17f06ea56f5bd71a&v=4 + url: https://github.com/Ryandaydev +- login: Xewus + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: sh0nk count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 url: https://github.com/sh0nk +- login: peidrao + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=5401640e0b961cc199dee39ec79e162c7833cd6b&v=4 + url: https://github.com/peidrao - login: RunningIkkyu count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=efb5b45b55584450507834f279ce48d4d64dea2f&v=4 + 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: solomein-sv count: 11 avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 @@ -472,10 +492,10 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp -- login: peidrao +- login: r0b2g1t count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=88c2cb42a99e0f50cdeae3606992568184783ee5&v=4 - url: https://github.com/peidrao + avatarUrl: https://avatars.githubusercontent.com/u/5357541?u=6428442d875d5d71aaa1bb38bb11c4be1a526bc2&v=4 + url: https://github.com/r0b2g1t - login: izaguerreiro count: 9 avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 @@ -496,10 +516,18 @@ top_reviewers: 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: axel584 + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/1334088?v=4 + url: https://github.com/axel584 - login: blt232018 count: 8 avatarUrl: https://avatars.githubusercontent.com/u/43393471?u=172b0e0391db1aa6c1706498d6dfcb003c8a4857&v=4 @@ -508,23 +536,3 @@ top_reviewers: 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: dimaqq - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/662249?v=4 - url: https://github.com/dimaqq -- 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 749f528c5..b9be9810d 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -8,9 +8,6 @@ gold: - 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://doist.com/careers/9B437B1615-wa-senior-backend-engineer-python - title: Help us migrate doist to FastAPI - img: https://fastapi.tiangolo.com/img/sponsors/doist.svg silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas @@ -30,9 +27,12 @@ silver: - 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://www.svix.com/ + title: Svix - Webhooks as a service + img: https://fastapi.tiangolo.com/img/sponsors/svix.svg bronze: - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source title: Biosecurity risk assessments made easy. diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index a8bfb0b1b..70a7548e4 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -13,3 +13,6 @@ logins: - BLUE-DEVIL1134 - ObliviousAI - Doist + - nihpo + - svix + - armand-sauzay diff --git a/docs/en/docs/advanced/additional-status-codes.md b/docs/en/docs/advanced/additional-status-codes.md index b61f88b93..416444d3b 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.6+" + + ```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.6+ 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 5e79776ca..402c5d755 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.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.6+ 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.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.6+ 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.6+" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.6+ 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.6+" + + ```Python hl_lines="21" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.6+ 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/events.md b/docs/en/docs/advanced/events.md index 7cd2998f0..6b7de4130 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 event handlers (functions) that need to be executed before the application starts up, or when the application is shutting down. +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**. -These functions can be declared with `async def` or normal `def`. +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 - Only event handlers for the main application will be executed, not for [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}. + The recommended way to handle the *startup* and *shutdown* is using the `lifespan` parameter of the `FastAPI` app as described above. + + You can probably skip this part. -## `startup` event +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`. + +### `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 + +🚨 Have 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/generate-clients.md b/docs/en/docs/advanced/generate-clients.md index f31a248d0..f62c0b57c 100644 --- a/docs/en/docs/advanced/generate-clients.md +++ b/docs/en/docs/advanced/generate-clients.md @@ -16,16 +16,16 @@ If you are building a **frontend**, a very interesting alternative is ../../../docs_src/generate_clients/tutorial001.py!} + ```Python hl_lines="7-9 12-13 16-17 21" + {!> ../../../docs_src/generate_clients/tutorial001_py39.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="7-9 12-13 16-17 21" - {!> ../../../docs_src/generate_clients/tutorial001_py39.py!} + ```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`. @@ -128,17 +128,16 @@ 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.9+" -=== "Python 3.6 and above" - - ```Python hl_lines="23 28 36" - {!> ../../../docs_src/generate_clients/tutorial002.py!} + ```Python hl_lines="21 26 34" + {!> ../../../docs_src/generate_clients/tutorial002_py39.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="21 26 34" - {!> ../../../docs_src/generate_clients/tutorial002_py39.py!} + ```Python hl_lines="23 28 36" + {!> ../../../docs_src/generate_clients/tutorial002.py!} ``` ### Generate a TypeScript Client with Tags @@ -186,16 +185,16 @@ 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 3.9+" - ```Python hl_lines="8-9 12" - {!> ../../../docs_src/generate_clients/tutorial003.py!} + ```Python hl_lines="6-7 10" + {!> ../../../docs_src/generate_clients/tutorial003_py39.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="6-7 10" - {!> ../../../docs_src/generate_clients/tutorial003_py39.py!} + ```Python hl_lines="8-9 12" + {!> ../../../docs_src/generate_clients/tutorial003.py!} ``` ### Generate a TypeScript Client with Custom Operation IDs diff --git a/docs/en/docs/advanced/middleware.md b/docs/en/docs/advanced/middleware.md index 3bf49e392..9219f1d2c 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/security/http-basic-auth.md b/docs/en/docs/advanced/security/http-basic-auth.md index 90c516808..8177a4b28 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.6+" + + ```Python hl_lines="2 7 11" + {!> ../../../docs_src/security/tutorial006_an.py!} + ``` + +=== "Python 3.6+ 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.6+" + + ```Python hl_lines="1 12-24" + {!> ../../../docs_src/security/tutorial007_an.py!} + ``` + +=== "Python 3.6+ 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: @@ -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.6+" + + ```Python hl_lines="26-30" + {!> ../../../docs_src/security/tutorial007_an.py!} + ``` + +=== "Python 3.6+ 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/oauth2-scopes.md b/docs/en/docs/advanced/security/oauth2-scopes.md index be51325cd..41cd61683 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.6+" + + ```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 152" + {!> ../../../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 153" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.6+ 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 153" + {!> ../../../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.6+" + + ```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.6+ 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.6+" + + ```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="152" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="153" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="153" + {!> ../../../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.6+" + + ```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 165" + {!> ../../../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 166" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4 139 166" + {!> ../../../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.6+" + + ```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.6+ 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.6+" + + ```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.6+ 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.6+" + + ```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.6+ 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.6+" + + ```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.6+ 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 d5151b585..60ec9c92c 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -216,9 +216,26 @@ Notice that now we don't create a default instance `settings = Settings()`. Now we create a dependency that returns a new `config.Settings()`. -```Python hl_lines="5 11-12" -{!../../../docs_src/settings/app02/main.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an_py39/main.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.6+ 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. @@ -227,9 +244,26 @@ Now we create a dependency that returns a new `config.Settings()`. And then we can require it from the *path operation function* as a dependency and use it anywhere we need it. -```Python hl_lines="16 18-20" -{!../../../docs_src/settings/app02/main.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an_py39/main.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.6+ 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 @@ -304,9 +338,26 @@ we would create that object for each request, and we would be reading the `.env` But as we are using the `@lru_cache()` decorator on top, the `Settings` object will be created only once, the first time it's called. ✔️ -```Python hl_lines="1 10" -{!../../../docs_src/settings/app03/main.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an_py39/main.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an/main.py!} + ``` + +=== "Python 3.6+ 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. diff --git a/docs/en/docs/advanced/testing-dependencies.md b/docs/en/docs/advanced/testing-dependencies.md index 7bba82fb7..ee48a735d 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.6+" + + ```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.6+ 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 3cf840819..94cf191d2 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.6+" + + ```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.6+ 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.6+" + + ```Python hl_lines="81-83" + {!> ../../../docs_src/websockets/tutorial003.py!} + ``` To try it out: diff --git a/docs/en/docs/async.md b/docs/en/docs/async.md index 6f34a9c9c..3d4b1956a 100644 --- a/docs/en/docs/async.md +++ b/docs/en/docs/async.md @@ -283,7 +283,7 @@ For example: ### Concurrency + Parallelism: Web + Machine Learning -With **FastAPI** you can take the advantage of concurrency that is very common for web development (the same main attractive of NodeJS). +With **FastAPI** you can take the advantage of concurrency that is very common for web development (the same main attraction of NodeJS). But you can also exploit the benefits of parallelism and multiprocessing (having multiple processes running in parallel) for **CPU bound** workloads like those in Machine Learning systems. diff --git a/docs/en/docs/deployment/concepts.md b/docs/en/docs/deployment/concepts.md index 22604ceeb..77419f8b0 100644 --- a/docs/en/docs/deployment/concepts.md +++ b/docs/en/docs/deployment/concepts.md @@ -235,7 +235,7 @@ Here are some possible combinations and strategies: * One Uvicorn **process manager** would listen on the **IP** and **port**, and it would start **multiple Uvicorn worker processes** * **Kubernetes** and other distributed **container systems** * Something in the **Kubernetes** layer would listen on the **IP** and **port**. The replication would be by having **multiple containers**, each with **one Uvicorn process** running -* **Cloud services** that handle this for your +* **Cloud services** that handle this for you * The cloud service will probably **handle replication for you**. It would possibly let you define **a process to run**, or a **container image** to use, in any case, it would most probably be **a single Uvicorn process**, and the cloud service would be in charge of replicating it. !!! tip diff --git a/docs/en/docs/external-links.md b/docs/en/docs/external-links.md index 55db55599..0c91470bc 100644 --- a/docs/en/docs/external-links.md +++ b/docs/en/docs/external-links.md @@ -56,6 +56,15 @@ Here's an incomplete list of some of them. {% endfor %} {% endif %} +### Taiwanese + +{% if external_links %} +{% for article in external_links.articles.taiwanese %} + +* {{ article.title }} by {{ article.author }}. +{% endfor %} +{% endif %} + ## Podcasts {% if external_links %} diff --git a/docs/en/docs/fastapi-people.md b/docs/en/docs/fastapi-people.md index 0cbcb69c9..20caaa1ee 100644 --- a/docs/en/docs/fastapi-people.md +++ b/docs/en/docs/fastapi-people.md @@ -28,7 +28,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 +36,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 +52,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 +60,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 +169,7 @@ They are supporting my work with **FastAPI** (and others), mainly through source code here. diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index a7ac9415f..48a88ee96 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 @@ -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 @@ -144,7 +152,7 @@ Here's what to have 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,7 +227,7 @@ 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. @@ -229,9 +237,9 @@ There is also the previous + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index deb8ab5d5..9a81f14d1 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -99,6 +99,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 diff --git a/docs/en/docs/project-generation.md b/docs/en/docs/project-generation.md index 2f3d6c1d3..8ba34fa11 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 3d22ee620..693613a36 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.6+" 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,16 +224,16 @@ 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 3.9+" - ```Python hl_lines="1 4" - {!> ../../../docs_src/python_types/tutorial007.py!} + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial007_py39.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="1" - {!> ../../../docs_src/python_types/tutorial007_py39.py!} + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial007.py!} ``` This means: @@ -243,16 +249,16 @@ 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 3.9+" - ```Python hl_lines="1 4" - {!> ../../../docs_src/python_types/tutorial008.py!} + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008_py39.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="1" - {!> ../../../docs_src/python_types/tutorial008_py39.py!} + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008.py!} ``` This means: @@ -267,18 +273,18 @@ 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 3.10+" - ```Python hl_lines="1 4" - {!> ../../../docs_src/python_types/tutorial008b.py!} + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008b_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="1" - {!> ../../../docs_src/python_types/tutorial008b_py310.py!} + ```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`. @@ -299,22 +305,22 @@ 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 4" - {!> ../../../docs_src/python_types/tutorial009.py!} + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial009_py310.py!} ``` -=== "Python 3.6 and above - alternative" +=== "Python 3.6+" ```Python hl_lines="1 4" - {!> ../../../docs_src/python_types/tutorial009b.py!} + {!> ../../../docs_src/python_types/tutorial009.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+ alternative" - ```Python hl_lines="1" - {!> ../../../docs_src/python_types/tutorial009_py310.py!} + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009b.py!} ``` #### Using `Union` or `Optional` @@ -360,17 +366,7 @@ 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" - - * `List` - * `Tuple` - * `Set` - * `Dict` - * `Union` - * `Optional` - * ...and others. - -=== "Python 3.9 and above" +=== "Python 3.10+" You can use the same builtin types as generics (with square brackets and types inside): @@ -382,10 +378,12 @@ These types that take type parameters in square brackets are called **Generic ty And the same as with Python 3.6, from the `typing` module: * `Union` - * `Optional` + * `Optional` (the same as with Python 3.6) * ...and others. -=== "Python 3.10 and above" + 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): @@ -397,10 +395,18 @@ These types that take type parameters in square brackets are called **Generic ty And the same as with Python 3.6, from the `typing` module: * `Union` - * `Optional` (the same as with Python 3.6) + * `Optional` * ...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. +=== "Python 3.6+" + + * `List` + * `Tuple` + * `Set` + * `Dict` + * `Union` + * `Optional` + * ...and others. ### Classes as types @@ -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.6+" ```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.6+" + + 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/release-notes.md b/docs/en/docs/release-notes.md index b65460294..7a9a09eed 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,363 @@ ## Latest Changes + +## 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 + +* ✨ Add support for function return type annotations to declare the `response_model`. Initial PR [#1436](https://github.com/tiangolo/fastapi/pull/1436) by [@uriyyo](https://github.com/uriyyo). + +Now you can declare the return type / `response_model` in the function return type annotation: + +```python +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + + +@app.get("/items/") +async def read_items() -> list[Item]: + return [ + Item(name="Portal Gun", price=42.0), + Item(name="Plumbus", price=32.0), + ] +``` + +FastAPI will use the return type annotation to perform: + +* Data validation +* Automatic documentation + * It could power automatic client generators +* **Data filtering** + +Before this version it was only supported via the `response_model` parameter. + +Read more about it in the new docs: [Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/). + +### Docs + +* 📝 Add External Link: Authorization on FastAPI with Casbin. PR [#5712](https://github.com/tiangolo/fastapi/pull/5712) by [@Xhy-5000](https://github.com/Xhy-5000). +* ✏ Fix typo in `docs/en/docs/async.md`. PR [#5785](https://github.com/tiangolo/fastapi/pull/5785) by [@Kingdageek](https://github.com/Kingdageek). +* ✏ Fix typo in `docs/en/docs/deployment/concepts.md`. PR [#5824](https://github.com/tiangolo/fastapi/pull/5824) by [@kelbyfaessler](https://github.com/kelbyfaessler). + +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/fastapi-people.md`. PR [#5577](https://github.com/tiangolo/fastapi/pull/5577) by [@Xewus](https://github.com/Xewus). +* 🌐 Fix typo in Chinese translation for `docs/zh/docs/benchmarks.md`. PR [#4269](https://github.com/tiangolo/fastapi/pull/4269) by [@15027668g](https://github.com/15027668g). +* 🌐 Add Korean translation for `docs/tutorial/cors.md`. PR [#3764](https://github.com/tiangolo/fastapi/pull/3764) by [@NinaHwang](https://github.com/NinaHwang). + +### Internal + +* ⬆ Update coverage[toml] requirement from <7.0,>=6.5.0 to >=6.5.0,<8.0. PR [#5801](https://github.com/tiangolo/fastapi/pull/5801) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update uvicorn[standard] requirement from <0.19.0,>=0.12.0 to >=0.12.0,<0.21.0 for development. PR [#5795](https://github.com/tiangolo/fastapi/pull/5795) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.24.3. PR [#5842](https://github.com/tiangolo/fastapi/pull/5842) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👥 Update FastAPI People. PR [#5825](https://github.com/tiangolo/fastapi/pull/5825) by [@github-actions[bot]](https://github.com/apps/github-actions). +* ⬆ Bump types-ujson from 5.5.0 to 5.6.0.0. PR [#5735](https://github.com/tiangolo/fastapi/pull/5735) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.5.2 to 1.6.4. PR [#5750](https://github.com/tiangolo/fastapi/pull/5750) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Add GitHub Action gate/check. PR [#5492](https://github.com/tiangolo/fastapi/pull/5492) by [@webknjaz](https://github.com/webknjaz). +* 🔧 Update sponsors, add Svix. PR [#5848](https://github.com/tiangolo/fastapi/pull/5848) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Remove Doist sponsor. PR [#5847](https://github.com/tiangolo/fastapi/pull/5847) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Update sqlalchemy requirement from <=1.4.41,>=1.3.18 to >=1.3.18,<1.4.43. PR [#5540](https://github.com/tiangolo/fastapi/pull/5540) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump nwtgck/actions-netlify from 1.2.4 to 2.0.0. PR [#5757](https://github.com/tiangolo/fastapi/pull/5757) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Refactor CI artifact upload/download for docs previews. PR [#5793](https://github.com/tiangolo/fastapi/pull/5793) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.5.1 to 1.5.2. PR [#5714](https://github.com/tiangolo/fastapi/pull/5714) by [@dependabot[bot]](https://github.com/apps/dependabot). @@ -1130,7 +1487,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 @@ -1685,11 +2042,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). diff --git a/docs/en/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md index 191a4ca0f..178297192 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.9+" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="14 16 23 26" + {!> ../../../docs_src/background_tasks/tutorial002_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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 d201953df..daa7353a2 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -112,9 +112,26 @@ So we put them in their own `dependencies` module (`app/dependencies.py`). We will now use a simple dependency to read a custom `X-Token` header: -```Python hl_lines="1 4-6" -{!../../../docs_src/bigger_applications/app/dependencies.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="3 6-8" + {!> ../../../docs_src/bigger_applications/app_an_py39/dependencies.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 5-7" + {!> ../../../docs_src/bigger_applications/app_an/dependencies.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="1 4-6" + {!> ../../../docs_src/bigger_applications/app/dependencies.py!} + ``` !!! tip We are using an invented header to simplify this example. @@ -189,7 +206,7 @@ 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`. diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index 0cfe576d6..8966032ff 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.9+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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.9+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12-15" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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 31dd27fed..b214092c9 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="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.6+" ```Python hl_lines="19-21" - {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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,16 +62,16 @@ 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 3.10+" - ```Python hl_lines="22" - {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="20" - {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} + ```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). @@ -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.6+" + + ```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.6+ 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.py!} + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" - ```Python hl_lines="26" + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```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.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.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.9+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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 d51f171d6..ffa0c0d0e 100644 --- a/docs/en/docs/tutorial/body-nested-models.md +++ b/docs/en/docs/tutorial/body-nested-models.md @@ -6,16 +6,16 @@ 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 3.10+" - ```Python hl_lines="14" - {!> ../../../docs_src/body_nested_models/tutorial001.py!} + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="12" - {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} + ```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. @@ -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.6+" - ```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.6+" - ```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.6+" - ```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.6+" - ```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: @@ -196,22 +196,22 @@ To see all the options you have, checkout the docs for ../../../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.6+" - ```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. @@ -220,22 +220,22 @@ The string will be checked to be a valid URL, and documented in JSON Schema / Op You can also use Pydantic models as subtypes of `list`, `set`, etc: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="20" - {!> ../../../docs_src/body_nested_models/tutorial006.py!} + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="20" {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="18" - {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006.py!} ``` This will expect (convert, validate, document, etc) a JSON body like: @@ -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.6+" - ```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,16 +308,16 @@ images: list[Image] as in: -=== "Python 3.6 and above" +=== "Python 3.9+" - ```Python hl_lines="15" - {!> ../../../docs_src/body_nested_models/tutorial008.py!} + ```Python hl_lines="13" + {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="13" - {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} + ```Python hl_lines="15" + {!> ../../../docs_src/body_nested_models/tutorial008.py!} ``` ## Editor support everywhere @@ -348,16 +348,16 @@ 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 3.9+" - ```Python hl_lines="9" - {!> ../../../docs_src/body_nested_models/tutorial009.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="7" - {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} + ```Python hl_lines="9" + {!> ../../../docs_src/body_nested_models/tutorial009.py!} ``` !!! tip diff --git a/docs/en/docs/tutorial/body-updates.md b/docs/en/docs/tutorial/body-updates.md index 7d8675060..a32948db1 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.6+" - ```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. @@ -67,22 +67,22 @@ That would generate a `dict` with only the data that was set when creating the ` 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 3.10+" - ```Python hl_lines="34" - {!> ../../../docs_src/body_updates/tutorial002.py!} + ```Python hl_lines="32" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="34" {!> ../../../docs_src/body_updates/tutorial002_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="32" - {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002.py!} ``` ### Using Pydantic's `update` parameter @@ -91,22 +91,22 @@ Now, you can create a copy of the existing model using `.copy()`, and pass the ` Like `stored_item_model.copy(update=update_data)`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="35" - {!> ../../../docs_src/body_updates/tutorial002.py!} + ```Python hl_lines="33" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="35" {!> ../../../docs_src/body_updates/tutorial002_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="33" - {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002.py!} ``` ### Partial updates recap @@ -124,22 +124,22 @@ In summary, to apply partial updates you would: * 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.6+" - ```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 509005936..172b91fdf 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -19,16 +19,16 @@ To declare a **request** body, you use ../../../docs_src/body/tutorial001.py!} + ```Python hl_lines="2" + {!> ../../../docs_src/body/tutorial001_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="2" - {!> ../../../docs_src/body/tutorial001_py310.py!} + ```Python hl_lines="4" + {!> ../../../docs_src/body/tutorial001.py!} ``` ## Create your data model @@ -37,16 +37,16 @@ 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 3.10+" - ```Python hl_lines="7-11" - {!> ../../../docs_src/body/tutorial001.py!} + ```Python hl_lines="5-9" + {!> ../../../docs_src/body/tutorial001_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="5-9" - {!> ../../../docs_src/body/tutorial001_py310.py!} + ```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. @@ -75,16 +75,16 @@ 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 3.10+" - ```Python hl_lines="18" - {!> ../../../docs_src/body/tutorial001.py!} + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial001_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="16" - {!> ../../../docs_src/body/tutorial001_py310.py!} + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} ``` ...and declare its type as the model you created, `Item`. @@ -149,16 +149,16 @@ But you would get the same editor support with ../../../docs_src/body/tutorial002.py!} + ```Python hl_lines="19" + {!> ../../../docs_src/body/tutorial002_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="19" - {!> ../../../docs_src/body/tutorial002_py310.py!} + ```Python hl_lines="21" + {!> ../../../docs_src/body/tutorial002.py!} ``` ## Request body + path parameters @@ -167,16 +167,16 @@ 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 3.10+" - ```Python hl_lines="17-18" - {!> ../../../docs_src/body/tutorial003.py!} + ```Python hl_lines="15-16" + {!> ../../../docs_src/body/tutorial003_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="15-16" - {!> ../../../docs_src/body/tutorial003_py310.py!} + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} ``` ## Request body + path + query parameters @@ -185,16 +185,16 @@ You can also declare **body**, **path** and **query** parameters, all at the sam **FastAPI** will recognize each of them and take the data from the correct place. -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="18" - {!> ../../../docs_src/body/tutorial004.py!} + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial004_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="16" - {!> ../../../docs_src/body/tutorial004_py310.py!} + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} ``` The function parameters will be recognized as follows: diff --git a/docs/en/docs/tutorial/cookie-params.md b/docs/en/docs/tutorial/cookie-params.md index 221cdfffb..111e93458 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.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index fb41ba1f6..498d935fe 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.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 and above" +=== "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.6+ 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.9+" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" + + ```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!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + Pay attention to the `__init__` method used to create the instance of the class: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="12" - {!> ../../../docs_src/dependencies/tutorial002.py!} + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" + + ```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!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + ...it has the same parameters as our previous `common_parameters`: -=== "Python 3.6 and above" +=== "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.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" + + ```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.6+ 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.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 and above" +=== "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.6+ 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.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` + +=== "Python 3.6+" + + ```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.6+" + + ```Python + commons: Annotated[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). +=== "Python 3.6+ 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.6+" + + ```Python + commons: Annotated[Any, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6+ 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.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" + + ```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.6+ 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.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` + +=== "Python 3.6+" + + ```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.6+" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` ...you write: -```Python -commons: CommonQueryParams = Depends() -``` +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, Depends()] + ``` -You declare the dependency as the type of the parameter, and you use `Depends()` as its "default" value (that after the `=`) for that function's parameter, without any parameter in `Depends()`, instead of having to write the full class *again* inside of `Depends(CommonQueryParams)`. +=== "Python 3.6 non-Annotated" + + !!! tip + 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.6+" + + ```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.6+ 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 a1bbcc6c7..935555339 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -14,9 +14,26 @@ The *path operation decorator* receives an optional argument `dependencies`. It should be a `list` of `Depends()`: -```Python hl_lines="17" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.6 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 normal dependencies. But their value (if they return any) won't be passed to your *path operation function*. @@ -40,17 +57,51 @@ You can use the same dependency *functions* you use normally. They can declare request requirements (like headers) or other sub-dependencies: -```Python hl_lines="6 11" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="8 13" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="7 12" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.6 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.6+" + + ```Python hl_lines="9 14" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.6 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.6+" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.6 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 a7300f0b7..8a5422ac8 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -66,9 +66,26 @@ You can have sub-dependencies and "trees" of sub-dependencies of any size and sh For example, `dependency_c` can have a dependency on `dependency_b`, and `dependency_b` on `dependency_a`: -```Python hl_lines="4 12 20" -{!../../../docs_src/dependencies/tutorial008.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="6 14 22" + {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="5 13 21" + {!> ../../../docs_src/dependencies/tutorial008_an.py!} + ``` + +=== "Python 3.6+ 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,9 +93,26 @@ In this case `dependency_c`, to execute its exit code, needs the value from `dep And, in turn, `dependency_b` needs the value from `dependency_a` (here named `dep_a`) to be available for its exit code. -```Python hl_lines="16-17 24-25" -{!../../../docs_src/dependencies/tutorial008.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="18-19 26-27" + {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="17-18 25-26" + {!> ../../../docs_src/dependencies/tutorial008_an.py!} + ``` + +=== "Python 3.6+ 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 dependencies with `yield` and `return` mixed. diff --git a/docs/en/docs/tutorial/dependencies/global-dependencies.md b/docs/en/docs/tutorial/dependencies/global-dependencies.md index bcd61d52b..0989b31d4 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.6+" + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial012_an.py!} + ``` + +=== "Python 3.6 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 5078c0096..80087a4a7 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -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-9" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" ```Python hl_lines="8-11" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" + + ```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.6+ 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**. @@ -63,40 +87,90 @@ And then it just returns a `dict` containing those values. ### 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.6+" + + ```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.6+ 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="13 18" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" ```Python hl_lines="15 20" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="16 21" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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 +200,45 @@ This way you write shared code once and **FastAPI** takes care of calling it for You just pass it to `Depends` and **FastAPI** knows how to do the rest. +## Share `Annotated` dependencies + +In the examples above, you see that there's a tiny bit of **code duplication**. + +When you need to use the `common_parameters()` dependency, you have to write the whole parameter with the type annotation and `Depends()`: + +```Python +commons: Annotated[dict, Depends(common_parameters)] +``` + +But because we are using `Annotated`, we can store that `Annotated` value in a variable and use it in multiple places: + +=== "Python 3.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.6+" + + ```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. diff --git a/docs/en/docs/tutorial/dependencies/sub-dependencies.md b/docs/en/docs/tutorial/dependencies/sub-dependencies.md index a5b40c9ad..b50de1a46 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.6+" + + ```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.6 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.9+" + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" + + ```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.6 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.6+" + + ```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.6 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 hl_lines="4 21" + {!> ../../../docs_src/encoder/tutorial001_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="4 21" - {!> ../../../docs_src/encoder/tutorial001_py310.py!} + ```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`. diff --git a/docs/en/docs/tutorial/extra-data-types.md b/docs/en/docs/tutorial/extra-data-types.md index fb3efd318..7d6ffbc78 100644 --- a/docs/en/docs/tutorial/extra-data-types.md +++ b/docs/en/docs/tutorial/extra-data-types.md @@ -55,28 +55,76 @@ Here are some of the additional data types you can use: 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.9+" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 3 13-17" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} ``` -=== "Python 3.10 and above" +=== "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!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="1 2 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.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.6 and above" +=== "Python 3.10+" ```Python hl_lines="18-19" - {!> ../../../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="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```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.6+ 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 72fc74894..e91e879e4 100644 --- a/docs/en/docs/tutorial/extra-models.md +++ b/docs/en/docs/tutorial/extra-models.md @@ -17,16 +17,16 @@ 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="9 11 16 22 24 29-30 33-35 40-41" - {!> ../../../docs_src/extra_models/tutorial001.py!} + ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" + {!> ../../../docs_src/extra_models/tutorial001_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" - {!> ../../../docs_src/extra_models/tutorial001_py310.py!} + ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" + {!> ../../../docs_src/extra_models/tutorial001.py!} ``` ### About `**user_in.dict()` @@ -158,16 +158,16 @@ 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 3.10+" - ```Python hl_lines="9 15-16 19-20 23-24" - {!> ../../../docs_src/extra_models/tutorial002.py!} + ```Python hl_lines="7 13-14 17-18 21-22" + {!> ../../../docs_src/extra_models/tutorial002_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7 13-14 17-18 21-22" - {!> ../../../docs_src/extra_models/tutorial002_py310.py!} + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} ``` ## `Union` or `anyOf` @@ -181,16 +181,16 @@ 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 3.10+" ```Python hl_lines="1 14-15 18-20 33" - {!> ../../../docs_src/extra_models/tutorial003.py!} + {!> ../../../docs_src/extra_models/tutorial003_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" ```Python hl_lines="1 14-15 18-20 33" - {!> ../../../docs_src/extra_models/tutorial003_py310.py!} + {!> ../../../docs_src/extra_models/tutorial003.py!} ``` ### `Union` in Python 3.10 @@ -213,16 +213,16 @@ 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 3.9+" - ```Python hl_lines="1 20" - {!> ../../../docs_src/extra_models/tutorial004.py!} + ```Python hl_lines="18" + {!> ../../../docs_src/extra_models/tutorial004_py39.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="18" - {!> ../../../docs_src/extra_models/tutorial004_py39.py!} + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} ``` ## Response with arbitrary `dict` @@ -233,16 +233,16 @@ 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 3.9+" - ```Python hl_lines="1 8" - {!> ../../../docs_src/extra_models/tutorial005.py!} + ```Python hl_lines="6" + {!> ../../../docs_src/extra_models/tutorial005_py39.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="6" - {!> ../../../docs_src/extra_models/tutorial005_py39.py!} + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} ``` ## Recap diff --git a/docs/en/docs/tutorial/header-params.md b/docs/en/docs/tutorial/header-params.md index 8e294416c..9e928cdc6 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.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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.6+" + + ```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.6+ 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.6+" + + ```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.6+ 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,24 +157,51 @@ 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_py39.py!} + {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial003_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ non-Annotated" + + !!! 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/metadata.md b/docs/en/docs/tutorial/metadata.md index 78f17031a..cf13e7470 100644 --- a/docs/en/docs/tutorial/metadata.md +++ b/docs/en/docs/tutorial/metadata.md @@ -101,7 +101,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/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md index 884a762e2..7d4d4bcca 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.6+" - ```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.6+" - ```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.6+" - ```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.6+" - ```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.6+" - ```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 cec54b0fb..70ba5ac50 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -4,38 +4,86 @@ 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="3" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ```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.6+" + + ```Python hl_lines="3-4" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + ## Declare metadata 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.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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 +93,14 @@ For example, to declare a `title` metadata value for the path parameter `item_id ## Order the parameters as you need +!!! tip + This is probably not as important or necessary if you use `Annotated`. + Let's say that you want to declare the query parameter `q` as a required `str`. And you don't need to declare anything else for that parameter, so you don't really need to use `Query`. -But you still need to use `Path` for the `item_id` path parameter. +But you still need to use `Path` for the `item_id` path parameter. And you don't want to use `Annotated` for some reason. Python will complain if you put a value with a "default" before a value that doesn't have a "default". @@ -59,13 +110,44 @@ It doesn't matter for **FastAPI**. It will detect the parameters by their names, So, you can declare your function as: -```Python hl_lines="7" -{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} -``` +=== "Python 3.6 non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002.py!} + ``` + +But have in mind that if you use `Annotated`, you won't have this problem, it won't matter as you're not using the function parameter default values for `Query()` or `Path()`. + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```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 +157,48 @@ Python won't do anything with that `*`, but it will know that all the following {!../../../docs_src/path_params_numeric_validations/tutorial003.py!} ``` +### Better with `Annotated` + +Have in mind that if you use `Annotated`, as you are not using function parameter default values, you won't have this problem, and yo probably won't need to use `*`. + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```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.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an.py!} + ``` + +=== "Python 3.6+ 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 +207,26 @@ The same applies for: * `gt`: `g`reater `t`han * `le`: `l`ess than or `e`qual -```Python hl_lines="9" -{!../../../docs_src/path_params_numeric_validations/tutorial005.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an.py!} + ``` + +=== "Python 3.6+ 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 +238,26 @@ So, `0.5` would be a valid value. But `0.0` or `0` would not. And the same for lt. -```Python hl_lines="11" -{!../../../docs_src/path_params_numeric_validations/tutorial006.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="13" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an.py!} + ``` + +=== "Python 3.6+ 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/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 060e1d58a..6a5a507b9 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -4,16 +4,16 @@ Let's take this application as example: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7" - {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} + ```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. @@ -27,39 +27,117 @@ 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.6+" - ```Python hl_lines="1" - {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + In versions of Python below Python 3.9 you import `Annotation` from `typing_extensions`. + + It will already be installed with FastAPI. + + ```Python hl_lines="3-4" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} ``` -## Use `Query` as the default value +## 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}? -And now use it as the default value of your parameter, setting the parameter `max_length` to 50: +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.6 and above" +=== "Python 3.6+" + + ```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.6+" + + ```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.6+" + + ```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 wll now: + +* **Validate** the data making sure that the max length is 50 characters +* Show a **clear error** for the client when the data is not valid +* **Document** the parameter in the OpenAPI schema *path operation* (so it will show up in the **automatic docs UI**) + +## Alternative (old) `Query` as the default value + +Previous versions of FastAPI (before 0.95.0) required you to use `Query` as the default value of your parameter, instead of putting it in `Annotated`, there's a high chance that you will see code using it around, so I'll explain it to you. + +!!! 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.6+" + + ```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 +145,7 @@ So: q: Union[str, None] = Query(default=None) ``` -...makes the parameter optional, the same as: +...makes the parameter optional, with a default value of `None`, the same as: ```Python q: Union[str, None] = None @@ -79,7 +157,7 @@ And in Python 3.10 and above: q: str | None = Query(default=None) ``` -...makes the parameter optional, the same as: +...makes the parameter optional, with a default value of `None`, the same as: ```Python q: str | None = None @@ -112,38 +190,124 @@ q: Union[str, None] = Query(default=None, max_length=50) This will validate the data, show a clear error when the data is not valid, and document the parameter in the OpenAPI schema *path operation*. +### `Query` as the default value or in `Annotated` + +Have in mind that when using `Query` inside of `Annotated` you cannot use the `default` parameter for `Query`. + +Instead use the actual default value of the function parameter. Otherwise, it would be inconsistent. + +For example, this is not allowed: + +```Python +q: Annotated[str Query(default="rick")] = "morty" +``` + +...because it's not clear if the default value should be `"rick"` or `"morty"`. + +So, you would use (preferably): + +```Python +q: Annotated[str, Query()] = "rick" +``` + +...or in older code bases you will find: + +```Python +q: str = Query(default="rick") +``` + +### Advantages of `Annotated` + +**Using `Annotated` is recommended** instead of the default value in function parameters, it is **better** for multiple reasons. 🤓 + +The **default** value of the **function parameter** is the **actual default** value, that's more intuitive with Python in general. 😌 + +You could **call** that same function in **other places** without FastAPI, and it would **work as expected**. If there's a **required** parameter (without a default value), your **editor** will let you know with an error, **Python** will also complain if you run it without passing the required parameter. + +When you don't use `Annotated` and instead use the **(old) default value style**, if you call that function without FastAPI in **other place**, you have to **remember** to pass the arguments to the function for it to work correctly, otherwise the values will be different from what you expect (e.g. `QueryInfo` or something similar instead of `str`). And your editor won't complain, and Python won't complain running that function, only when the operations inside error out. + +Because `Annotated` can have more than one metadata annotation, you could now even use the same function with other tools, like Typer. 🚀 + ## Add more validations You can also add a parameter `min_length`: -=== "Python 3.6 and above" +=== "Python 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.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an.py!} ``` -=== "Python 3.10 and above" +=== "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!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} + ``` + ## Add regular expressions You can define a regular expression that the parameter should match: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="11" - {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```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!} ``` +=== "Python 3.6+ 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 checks that the received parameter value: * `^`: starts with the following characters, doesn't have characters before. @@ -156,16 +320,33 @@ But whenever you need them and go and learn them, know that you can already use ## Default values -The same way that you can pass `None` as the value for the `default` parameter, you can pass other values. +You can, of course, use default values other than `None`. Let's say that you want to declare the `q` query parameter to have a `min_length` of `3`, and to have a default value of `"fixedquery"`: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial005.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an.py!} + ``` + +=== "Python 3.6+ 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 +364,70 @@ q: Union[str, None] = None But we are now declaring it with `Query`, for example like: -```Python -q: Union[str, None] = Query(default=None, min_length=3) -``` +=== "Annotated" + + ```Python + q: Annotated[Union[str, None], Query(min_length=3)] = None + ``` + +=== "non-Annotated" + + ```Python + q: Union[str, None] = Query(default=None, min_length=3) + ``` So, when you need to declare a value as required while using `Query`, you can simply not declare a default value: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial006.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an.py!} + ``` + +=== "Python 3.6+ 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.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006b_an.py!} + ``` + +=== "Python 3.6+ 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,20 +440,44 @@ 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.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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. @@ -233,12 +485,29 @@ To do that, you can declare that `None` is a valid type but still use `default=. If you feel uncomfortable using `...`, you can also import and use `Required` from Pydantic: -```Python hl_lines="2 8" -{!../../../docs_src/query_params_str_validations/tutorial006d.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/query_params_str_validations/tutorial006d_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="2 9" + {!> ../../../docs_src/query_params_str_validations/tutorial006d_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="2 8" + {!> ../../../docs_src/query_params_str_validations/tutorial006d.py!} + ``` !!! tip - Remember that in most of the cases, when something is required, you can simply omit the `default` parameter, so you normally don't have to use `...` nor `Required`. + Remember that in most of the cases, when something is required, you can simply omit the default, so you normally don't have to use `...` nor `Required`. ## Query parameter list / multiple values @@ -246,24 +515,51 @@ 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_py39.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" + + ```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.6+ non-Annotated" + + !!! 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 +590,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.6+" + + ```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.6+ 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,9 +641,26 @@ the default of `q` will be: `["foo", "bar"]` and your response will be: You can also use `list` directly instead of `List[str]` (or `list[str]` in Python 3.9+): -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial013.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an.py!} + ``` + +=== "Python 3.6+ 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. @@ -349,32 +680,80 @@ That information will be included in the generated OpenAPI and used by the docum 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.6+" + + ```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.6+ 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="13" - {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} + ```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.10 and above" +=== "Python 3.6+" + + ```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="12" {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="13" + {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} + ``` + ## Alias parameters Imagine that you want the parameter to be `item-query`. @@ -393,18 +772,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.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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 +816,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="18" - {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```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="17" {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="18" + {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} + ``` + The docs will show it like this: @@ -433,18 +860,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.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" + + ```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.6+ 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. diff --git a/docs/en/docs/tutorial/query-params.md b/docs/en/docs/tutorial/query-params.md index eec55502a..0b74b10f8 100644 --- a/docs/en/docs/tutorial/query-params.md +++ b/docs/en/docs/tutorial/query-params.md @@ -63,16 +63,16 @@ 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 3.10+" - ```Python hl_lines="9" - {!> ../../../docs_src/query_params/tutorial002.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial002_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7" - {!> ../../../docs_src/query_params/tutorial002_py310.py!} + ```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. @@ -84,16 +84,16 @@ 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 3.10+" - ```Python hl_lines="9" - {!> ../../../docs_src/query_params/tutorial003.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial003_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7" - {!> ../../../docs_src/query_params/tutorial003_py310.py!} + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} ``` In this case, if you go to: @@ -137,16 +137,16 @@ And you don't have to declare them in any specific order. They will be detected by name: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="8 10" - {!> ../../../docs_src/query_params/tutorial004.py!} + ```Python hl_lines="6 8" + {!> ../../../docs_src/query_params/tutorial004_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="6 8" - {!> ../../../docs_src/query_params/tutorial004_py310.py!} + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} ``` ## Required query parameters @@ -203,16 +203,16 @@ 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 3.10+" - ```Python hl_lines="10" - {!> ../../../docs_src/query_params/tutorial006.py!} + ```Python hl_lines="8" + {!> ../../../docs_src/query_params/tutorial006_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="8" - {!> ../../../docs_src/query_params/tutorial006_py310.py!} + ```Python hl_lines="10" + {!> ../../../docs_src/query_params/tutorial006.py!} ``` In this case, there are 3 query parameters: diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index 783783c58..1fe1e7a33 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.6+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ 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.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ 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`. @@ -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.6+" + + ```Python hl_lines="13" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ 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.py!} + {!> ../../../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 and above" +=== "Python 3.10+ non-Annotated" - ```Python hl_lines="7 14" + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7 15" {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.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.6+" + + ```Python hl_lines="8 14" + {!> ../../../docs_src/request_files/tutorial001_03_an.py!} + ``` + +=== "Python 3.6+ 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.6+" + + ```Python hl_lines="11 16" + {!> ../../../docs_src/request_files/tutorial002_an.py!} ``` -=== "Python 3.9 and above" +=== "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.6+ 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.6+" - ```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.6+ 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 6844f8c65..1818946c4 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.6+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ 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.6+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ 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 26922685a..5d441a614 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.6+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.6+ 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.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.6+ 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 ab68314e8..2181cfb5a 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -1,6 +1,51 @@ -# Response Model +# Response Model - Return Type -You can declare the model used for the response with the parameter `response_model` in any of the *path operations*: +You can declare the type used for the response by annotating the *path operation function* **return type**. + +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.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.6+" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01.py!} + ``` + +FastAPI will use this return type to: + +* **Validate** the returned data. + * If the data is invalid (e.g. you are missing a field), it means that *your* app code is broken, not returning what it should, and it will return a server error instead of returning incorrect data. This way you and your clients can be certain that they will receive the data and the data shape expected. +* Add a **JSON Schema** for the response, in the OpenAPI *path operation*. + * This will be used by the **automatic docs**. + * It will also be used by automatic client code generation tools. + +But most importantly: + +* It will **limit and filter** the output data to what is defined in the return type. + * This is particularly important for **security**, we'll see more of that below. + +## `response_model` Parameter + +There are some cases where you need or want to return some data that is not exactly what the type declares. + +For example, you could want to **return a dictionary** or a database object, but **declare it as a Pydantic model**. This way the Pydantic model would do all the data documentation, validation, etc. for the object that you returned (e.g. a dictionary or database object). + +If you added the return type annotation, tools and editors would complain with a (correct) error telling you that your function is returning a type (e.g. a dict) that is different from what you declared (e.g. a Pydantic model). + +In those cases, you can use the *path operation decorator* parameter `response_model` instead of the return type. + +You can use the `response_model` parameter in any of the *path operations*: * `@app.get()` * `@app.post()` @@ -8,57 +53,58 @@ You can declare the model used for the response with the parameter `response_mod * `@app.delete()` * etc. -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="17" - {!> ../../../docs_src/response_model/tutorial001.py!} + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" - ```Python hl_lines="17" + ```Python hl_lines="17 22 24-27" {!> ../../../docs_src/response_model/tutorial001_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="15" - {!> ../../../docs_src/response_model/tutorial001_py310.py!} + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001.py!} ``` !!! note Notice that `response_model` is a parameter of the "decorator" method (`get`, `post`, etc). Not of your *path operation function*, like all the parameters and body. -It receives the same type you would declare for a Pydantic model attribute, so, it can be a Pydantic model, but it can also be, e.g. a `list` of Pydantic models, like `List[Item]`. +`response_model` receives the same type you would declare for a Pydantic model field, so, it can be a Pydantic model, but it can also be, e.g. a `list` of Pydantic models, like `List[Item]`. + +FastAPI will use this `response_model` to do all the data documentation, validation, etc. and also to **convert and filter the output data** to its type declaration. -FastAPI will use this `response_model` to: +!!! tip + If you have strict type checks in your editor, mypy, etc, you can declare the function return type as `Any`. -* Convert the output data to its type declaration. -* Validate the data. -* Add a JSON Schema for the response, in the OpenAPI *path operation*. -* Will be used by the automatic documentation systems. + That way you tell the editor that you are intentionally returning anything. But FastAPI will still do the data documentation, validation, filtering, etc. with the `response_model`. -But most importantly: +### `response_model` Priority -* Will limit the output data to that of the model. We'll see how that's important below. +If you declare both a return type and a `response_model`, the `response_model` will take priority and be used by FastAPI. -!!! note "Technical Details" - The response model is declared in this parameter instead of as a function return type annotation, because the path function may not actually return that response model but rather return a `dict`, database object or some other model, and then use the `response_model` to perform the field limiting and serialization. +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 3.10+" - ```Python hl_lines="9 11" - {!> ../../../docs_src/response_model/tutorial002.py!} + ```Python hl_lines="7 9" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7 9" - {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ```Python hl_lines="9 11" + {!> ../../../docs_src/response_model/tutorial002.py!} ``` !!! info @@ -69,72 +115,126 @@ 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 3.10+" - ```Python hl_lines="17-18" - {!> ../../../docs_src/response_model/tutorial002.py!} + ```Python hl_lines="16" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="15-16" - {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ```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 the user themself is sending the password. +In this case, it might not be a problem, because it's the same user sending the password. But if we use the same model for another *path operation*, we could be sending our user's passwords to every client. !!! danger - Never store the plain password of a user or send it in a response. + Never store the plain password of a user or send it in a response like this, unless you know all the caveats and you know what you are doing. ## Add an output model We can instead create an input model with the plaintext password and an output model without it: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="9 11 16" - {!> ../../../docs_src/response_model/tutorial003.py!} + {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7 9 14" - {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ```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 3.10+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" ```Python hl_lines="24" {!> ../../../docs_src/response_model/tutorial003.py!} ``` -=== "Python 3.10 and above" +...we declared the `response_model` to be our model `UserOut`, that doesn't include the password: + +=== "Python 3.10+" ```Python hl_lines="22" {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` -...we declared the `response_model` to be our model `UserOut`, that doesn't include the password: - -=== "Python 3.6 and above" +=== "Python 3.6+" ```Python hl_lines="22" {!> ../../../docs_src/response_model/tutorial003.py!} ``` -=== "Python 3.10 and above" +So, **FastAPI** will take care of filtering out all the data that is not declared in the output model (using Pydantic). - ```Python hl_lines="20" - {!> ../../../docs_src/response_model/tutorial003_py310.py!} +### `response_model` or Return Type + +In this case, because the two models are different, if we annotated the function return type as `UserOut`, the editor and tools would complain that we are returning an invalid type, as those are different classes. + +That's why in this example we have to declare it in the `response_model` parameter. + +...but continue reading below to see how to overcome that. + +## Return Type and Data Filtering + +Let's continue from the previous example. We wanted to **annotate the function with one type** but return something that includes **more data**. + +We want FastAPI to keep **filtering** the data using the response model. + +In the previous example, because the classes were different, we had to use the `response_model` parameter. But that also means that we don't get the support from the editor and tools checking the function return type. + +But in most of the cases where we need to do something like this, we want the model just to **filter/remove** some of the data as in this example. + +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.10+" + + ```Python hl_lines="7-10 13-14 18" + {!> ../../../docs_src/response_model/tutorial003_01_py310.py!} ``` -So, **FastAPI** will take care of filtering out all the data that is not declared in the output model (using Pydantic). +=== "Python 3.6+" + + ```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. 🤓 + +### Type Annotations and Tooling + +First let's see how editors, mypy and other tools would see this. + +`BaseUser` has the base fields. Then `UserIn` inherits from `BaseUser` and adds the `password` field, so, it will include all the fields from both models. + +We annotate the function return type as `BaseUser`, but we are actually returning a `UserIn` instance. + +The editor, mypy, and other tools won't complain about this because, in typing terms, `UserIn` is a subclass of `BaseUser`, which means it's a *valid* type when what is expected is anything that is a `BaseUser`. + +### FastAPI Data Filtering + +Now, for FastAPI, it will see the return type and make sure that what you return includes **only** the fields that are declared in the type. + +FastAPI does several things internally with Pydantic to make sure that those same rules of class inheritance are not used for the returned data filtering, otherwise you could end up returning much more data than what you expected. + +This way, you can get the best of both worlds: type annotations with **tooling support** and **data filtering**. ## See it in the docs @@ -146,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.6+" + + ```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.6+" + + ```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.6+" - ```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`. @@ -180,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.6+" - ```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. @@ -273,16 +441,16 @@ 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 3.10+" - ```Python hl_lines="31 37" - {!> ../../../docs_src/response_model/tutorial005.py!} + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial005_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="29 35" - {!> ../../../docs_src/response_model/tutorial005_py310.py!} + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial005.py!} ``` !!! tip @@ -294,16 +462,16 @@ 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 3.10+" - ```Python hl_lines="31 37" - {!> ../../../docs_src/response_model/tutorial006.py!} + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial006_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="29 35" - {!> ../../../docs_src/response_model/tutorial006_py310.py!} + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial006.py!} ``` ## Recap diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 94347018d..5312254d9 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -8,16 +8,16 @@ Here are several ways to do it. You can declare an `example` for a Pydantic model using `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="15-23" - {!> ../../../docs_src/schema_extra_example/tutorial001.py!} + ```Python hl_lines="13-21" + {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="13-21" - {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} + ```Python hl_lines="15-23" + {!> ../../../docs_src/schema_extra_example/tutorial001.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. @@ -33,16 +33,16 @@ When using `Field()` with Pydantic models, you can also declare extra info for t You can use this to add `example` for each field: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="4 10-13" - {!> ../../../docs_src/schema_extra_example/tutorial002.py!} + ```Python hl_lines="2 8-11" + {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="2 8-11" - {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} + ```Python hl_lines="4 10-13" + {!> ../../../docs_src/schema_extra_example/tutorial002.py!} ``` !!! warning @@ -66,18 +66,39 @@ you can also declare a data `example` or a group of `examples` with additional i Here we pass an `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-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.6+" + + ```Python hl_lines="23-28" + {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10+ non-Annotated" ```Python hl_lines="18-23" {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="20-25" + {!> ../../../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`: @@ -97,18 +118,39 @@ 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" +=== "Python 3.10+" - ```Python hl_lines="21-47" - {!> ../../../docs_src/schema_extra_example/tutorial004.py!} + ```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.6+" + + ```Python hl_lines="24-50" + {!> ../../../docs_src/schema_extra_example/tutorial004_an.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10+ non-Annotated" ```Python hl_lines="19-45" {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="21-47" + {!> ../../../docs_src/schema_extra_example/tutorial004.py!} + ``` + ### Examples in the docs UI With `examples` added to `Body()` the `/docs` would look like: diff --git a/docs/en/docs/tutorial/security/first-steps.md b/docs/en/docs/tutorial/security/first-steps.md index 45ffaab90..5765cf2d6 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.6+" + + ```Python + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.6+ 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.6+" + + ```Python hl_lines="7" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.6+ 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.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.6+ 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 13e867216..1a8c5d9a8 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.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.6+ 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.6+" + + ```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.6+ 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.9+" + + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="26" + {!> ../../../docs_src/security/tutorial002_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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.9+" + + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20-23 27-28" + {!> ../../../docs_src/security/tutorial002_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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.9+" + + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" + + ```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.6+ 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. @@ -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.9+" + + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" + + ```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.6+ 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/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md index 41f00fd41..deb722b96 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.9+" + + ```Python hl_lines="7 48 55-56 59-60 69-75" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="7 49 56-57 60-61 70-76" + {!> ../../../docs_src/security/tutorial004_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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.9+" + + ```Python hl_lines="6 12-14 28-30 78-86" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="6 13-15 29-31 79-87" + {!> ../../../docs_src/security/tutorial004_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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.9+" + + ```Python hl_lines="89-106" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="90-107" + {!> ../../../docs_src/security/tutorial004_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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.9+" + + ```Python hl_lines="117-132" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="118-133" + {!> ../../../docs_src/security/tutorial004_an.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. ```Python hl_lines="114-127" {!> ../../../docs_src/security/tutorial004_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="115-128" + {!> ../../../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. diff --git a/docs/en/docs/tutorial/security/simple-oauth2.md b/docs/en/docs/tutorial/security/simple-oauth2.md index 505b223b1..abcf6b667 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.9+" + + ```Python hl_lines="4 78" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" + + ```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.6+ 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.9+" + + ```Python hl_lines="3 79-81" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" + + ```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.6+ 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.9+" + + ```Python hl_lines="82-85" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="83-86" + {!> ../../../docs_src/security/tutorial003_an.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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.6+" + + ```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.6+ 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-72 90" - {!> ../../../docs_src/security/tutorial003.py!} + ```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.6+" + + ```Python hl_lines="59-67 70-75 95" + {!> ../../../docs_src/security/tutorial003_an.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. - ```Python hl_lines="55-64 67-70 88" + ```Python hl_lines="56-64 67-70 88" {!> ../../../docs_src/security/tutorial003_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="58-66 69-72 90" + {!> ../../../docs_src/security/tutorial003.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 5ccaf05ec..fd66c5add 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -262,22 +262,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.6+" - ```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 @@ -306,22 +306,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.6+" - ```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 +335,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.6+" - ```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 @@ -481,16 +481,16 @@ 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 3.9+" - ```Python hl_lines="9" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="7" - {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ```Python hl_lines="9" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} ``` #### Alembic Note @@ -515,16 +515,16 @@ 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 3.9+" - ```Python hl_lines="15-20" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ```Python hl_lines="13-18" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="13-18" - {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ```Python hl_lines="15-20" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} ``` !!! info @@ -540,16 +540,16 @@ 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 3.9+" - ```Python hl_lines="24 32 38 47 53" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ```Python hl_lines="22 30 36 45 51" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="22 30 36 45 51" - {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ```Python hl_lines="24 32 38 47 53" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} ``` !!! info "Technical Details" @@ -561,16 +561,16 @@ 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 3.9+" - ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" - {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ```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. @@ -654,22 +654,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.6+" ```Python - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` * `sql_app/crud.py`: @@ -680,16 +680,16 @@ For example, in a background task worker with ../../../docs_src/sql_databases/sql_app/main.py!} + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" ```Python - {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + {!> ../../../docs_src/sql_databases/sql_app/main.py!} ``` ## Check it @@ -739,16 +739,16 @@ 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 3.9+" - ```Python hl_lines="14-22" - {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} + ```Python hl_lines="12-20" + {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.6+" - ```Python hl_lines="12-20" - {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} + ```Python hl_lines="14-22" + {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} ``` !!! info diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index be07aab37..ec133a4d0 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.9+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} + ``` + +=== "Python 3.6+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an/main.py!} ``` -=== "Python 3.10 and above" +=== "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.6+ 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/mkdocs.yml b/docs/en/mkdocs.yml index 69ad29624..a5d77acbf 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -187,7 +189,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -218,6 +220,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -238,6 +242,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index e9b9f60eb..b85f0c4cf 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -34,12 +34,6 @@ -
- - - - -
diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index d1d6215b6..a89aeb21a 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -90,7 +92,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -121,6 +123,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -141,6 +145,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 7c2fe5eab..f77f82f69 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -80,7 +82,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -111,6 +113,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -131,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ 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 000000000..ace9f19f9 --- /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étaonné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/deployment/manually.md b/docs/fr/docs/deployment/manually.md new file mode 100644 index 000000000..c53e2db67 --- /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/tutorial/debugging.md b/docs/fr/docs/tutorial/debugging.md new file mode 100644 index 000000000..e58872d30 --- /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/mkdocs.yml b/docs/fr/mkdocs.yml index 7dce4b127..19182f381 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -67,7 +69,9 @@ nav: - tutorial/query-params.md - tutorial/body.md - tutorial/background-tasks.md + - tutorial/debugging.md - Guide utilisateur avancé: + - advanced/path-operation-advanced-configuration.md - advanced/additional-status-codes.md - advanced/additional-responses.md - async.md @@ -77,6 +81,7 @@ nav: - deployment/https.md - deployment/deta.md - deployment/docker.md + - deployment/manually.md - project-generation.md - alternatives.md - history-design-future.md @@ -104,7 +109,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -135,6 +140,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -155,6 +162,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index 3279099b5..c5689395f 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -80,7 +82,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -111,6 +113,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -131,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/hy/docs/index.md b/docs/hy/docs/index.md new file mode 100644 index 000000000..cc82b33cf --- /dev/null +++ b/docs/hy/docs/index.md @@ -0,0 +1,467 @@ + +{!../../../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.7+ 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.7+**. + +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/hy/mkdocs.yml b/docs/hy/mkdocs.yml new file mode 100644 index 000000000..bc64e78f2 --- /dev/null +++ b/docs/hy/mkdocs.yml @@ -0,0 +1,148 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/hy/ +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: hy +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/ + - hy: /hy/ + - 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: /hy/ + name: hy + - 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/hy/overrides/.gitignore b/docs/hy/overrides/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index abb31252f..7b7875ef7 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -80,7 +82,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -111,6 +113,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -131,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 532b5bc52..9393c3663 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -80,7 +82,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -111,6 +113,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -131,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/ja/docs/tutorial/testing.md b/docs/ja/docs/tutorial/testing.md index 56f5cabac..037e9628f 100644 --- a/docs/ja/docs/tutorial/testing.md +++ b/docs/ja/docs/tutorial/testing.md @@ -74,16 +74,16 @@ これらの *path operation* には `X-Token` ヘッダーが必要です。 -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python - {!> ../../../docs_src/app_testing/app_b/main.py!} + {!> ../../../docs_src/app_testing/app_b_py310/main.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" ```Python - {!> ../../../docs_src/app_testing/app_b_py310/main.py!} + {!> ../../../docs_src/app_testing/app_b/main.py!} ``` ### 拡張版テストファイル diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 5bbcce605..3703398af 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -124,7 +126,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -155,6 +157,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -175,6 +179,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/ko/docs/tutorial/cors.md b/docs/ko/docs/tutorial/cors.md new file mode 100644 index 000000000..39e9ea83f --- /dev/null +++ b/docs/ko/docs/tutorial/cors.md @@ -0,0 +1,84 @@ +# 교차 출처 리소스 공유 + +CORS 또는 "교차-출처 리소스 공유"란, 브라우저에서 동작하는 프론트엔드가 자바스크립트로 코드로 백엔드와 통신하고, 백엔드는 해당 프론트엔드와 다른 "출처"에 존재하는 상황을 의미합니다. + +## 출처 + +출처란 프로토콜(`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` 을 기본 포트로 간주합니다). + +그러면 브라우저는 백엔드에 HTTP `OPTIONS` 요청을 보내고, 백엔드에서 이 다른 출처(`http://localhost:8080`)와의 통신을 허가하는 적절한 헤더를 보내면, 브라우저는 프론트엔드의 자바스크립트가 백엔드에 요청을 보낼 수 있도록 합니다. + +이를 위해, 백엔드는 "허용된 출처(allowed origins)" 목록을 가지고 있어야만 합니다. + +이 경우, 프론트엔드가 제대로 동작하기 위해 `http://localhost:8080`을 목록에 포함해야 합니다. + +## 와일드카드 + +모든 출처를 허용하기 위해 목록을 `"*"` ("와일드카드")로 선언하는 것도 가능합니다. + +하지만 이것은 특정한 유형의 통신만을 허용하며, 쿠키 및 액세스 토큰과 사용되는 인증 헤더(Authoriztion header) 등이 포함된 경우와 같이 자격 증명(credentials)이 포함된 통신은 허용되지 않습니다. + +따라서 모든 작업을 의도한대로 실행하기 위해, 허용되는 출처를 명시적으로 지정하는 것이 좋습니다. + +## `CORSMiddleware` 사용 + +`CORSMiddleware` 을 사용하여 **FastAPI** 응용 프로그램의 교차 출처 리소스 공유 환경을 설정할 수 있습니다. + +* `CORSMiddleware` 임포트. +* 허용되는 출처(문자열 형식)의 리스트 생성. +* FastAPI 응용 프로그램에 "미들웨어(middleware)"로 추가. + +백엔드에서 다음의 사항을 허용할지에 대해 설정할 수도 있습니다: + +* 자격증명 (인증 헤더, 쿠키 등). +* 특정한 HTTP 메소드(`POST`, `PUT`) 또는 와일드카드 `"*"` 를 사용한 모든 HTTP 메소드. +* 특정한 HTTP 헤더 또는 와일드카드 `"*"` 를 사용한 모든 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` 입니다. + +미들웨어는 두가지 특정한 종류의 HTTP 요청에 응답합니다... + +### CORS 사전 요청 + +`Origin` 및 `Access-Control-Request-Method` 헤더와 함께 전송하는 모든 `OPTIONS` 요청입니다. + +이 경우 미들웨어는 들어오는 요청을 가로채 적절한 CORS 헤더와, 정보 제공을 위한 `200` 또는 `400` 응답으로 응답합니다. + +### 단순한 요청 + +`Origin` 헤더를 가진 모든 요청. 이 경우 미들웨어는 요청을 정상적으로 전달하지만, 적절한 CORS 헤더를 응답에 포함시킵니다. + +## 더 많은 정보 + +CORS에 대한 더 많은 정보를 알고싶다면, Mozilla CORS 문서를 참고하기 바랍니다. + +!!! note "기술적 세부 사항" + `from starlette.middleware.cors import CORSMiddleware` 역시 사용할 수 있습니다. + + **FastAPI**는 개발자인 당신의 편의를 위해 `fastapi.middleware` 에서 몇가지의 미들웨어를 제공합니다. 하지만 대부분의 미들웨어가 Stralette으로부터 직접 제공됩니다. diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 50931e134..29b684371 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -69,6 +71,7 @@ nav: - tutorial/request-files.md - tutorial/request-forms-and-files.md - tutorial/encoder.md + - tutorial/cors.md markdown_extensions: - toc: permalink: true @@ -91,7 +94,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -122,6 +125,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -142,6 +147,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index 6d46939f9..d9b1bc1b8 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -80,7 +82,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -111,6 +113,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -131,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 1cd129420..8d0d20239 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -83,7 +85,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -114,6 +116,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -134,6 +138,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/pt/docs/tutorial/body-multiple-params.md b/docs/pt/docs/tutorial/body-multiple-params.md index ac67aa47f..22f5856a6 100644 --- a/docs/pt/docs/tutorial/body-multiple-params.md +++ b/docs/pt/docs/tutorial/body-multiple-params.md @@ -8,16 +8,16 @@ 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 3.10+" - ```Python hl_lines="19-21" - {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ```Python hl_lines="17-19" + {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} ``` -=== "Python 3.10 e superiores" +=== "Python 3.6+" - ```Python hl_lines="17-19" - {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} ``` !!! nota @@ -38,16 +38,16 @@ 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 3.10+" - ```Python hl_lines="22" - {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} ``` -=== "Python 3.10 e superiores" +=== "Python 3.6+" - ```Python hl_lines="20" - {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} + ```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). @@ -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.6+" ```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,16 +137,16 @@ q: str | None = None Por exemplo: -=== "Python 3.6 e superiores" +=== "Python 3.10+" - ```Python hl_lines="27" - {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ```Python hl_lines="26" + {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} ``` -=== "Python 3.10 e superiores" +=== "Python 3.6+" - ```Python hl_lines="26" - {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} ``` !!! info "Informação" @@ -166,16 +166,16 @@ item: Item = Body(embed=True) como em: -=== "Python 3.6 e superiores" +=== "Python 3.10+" - ```Python hl_lines="17" - {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ```Python hl_lines="15" + {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} ``` -=== "Python 3.10 e superiores" +=== "Python 3.6+" - ```Python hl_lines="15" - {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} ``` Neste caso o **FastAPI** esperará um corpo como: diff --git a/docs/pt/docs/tutorial/encoder.md b/docs/pt/docs/tutorial/encoder.md new file mode 100644 index 000000000..bb4483fdc --- /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.6+" + + ```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/header-params.md b/docs/pt/docs/tutorial/header-params.md index 94ee784cd..bc8843327 100644 --- a/docs/pt/docs/tutorial/header-params.md +++ b/docs/pt/docs/tutorial/header-params.md @@ -6,16 +6,16 @@ Você pode definir parâmetros de Cabeçalho da mesma maneira que define paramê Primeiro importe `Header`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="3" - {!> ../../../docs_src/header_params/tutorial001.py!} + ```Python hl_lines="1" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="1" - {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} ``` ## Declare parâmetros de `Header` @@ -24,16 +24,16 @@ Então declare os paramêtros de cabeçalho usando a mesma estrutura que em `Pat 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 3.10+" - ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial001.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7" - {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} ``` !!! note "Detalhes Técnicos" @@ -60,16 +60,16 @@ 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 3.10+" - ```Python hl_lines="10" - {!> ../../../docs_src/header_params/tutorial002.py!} + ```Python hl_lines="8" + {!> ../../../docs_src/header_params/tutorial002_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="8" - {!> ../../../docs_src/header_params/tutorial002_py310.py!} + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} ``` !!! warning "Aviso" @@ -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.6+" - ```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/path-params-numeric-validations.md b/docs/pt/docs/tutorial/path-params-numeric-validations.md index f478fd190..ec9b74b30 100644 --- a/docs/pt/docs/tutorial/path-params-numeric-validations.md +++ b/docs/pt/docs/tutorial/path-params-numeric-validations.md @@ -6,16 +6,16 @@ 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 3.10+" - ```Python hl_lines="3" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ```Python hl_lines="1" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` -=== "Python 3.10 e superiores" +=== "Python 3.6+" - ```Python hl_lines="1" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} ``` ## Declare metadados @@ -24,16 +24,16 @@ 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 3.10+" - ```Python hl_lines="10" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` -=== "Python 3.10 e superiores" +=== "Python 3.6+" - ```Python hl_lines="8" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} ``` !!! note "Nota" diff --git a/docs/pt/docs/tutorial/query-params.md b/docs/pt/docs/tutorial/query-params.md index 189724396..3ada4fd21 100644 --- a/docs/pt/docs/tutorial/query-params.md +++ b/docs/pt/docs/tutorial/query-params.md @@ -63,16 +63,16 @@ 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 3.10+" - ```Python hl_lines="9" - {!> ../../../docs_src/query_params/tutorial002.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial002_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7" - {!> ../../../docs_src/query_params/tutorial002_py310.py!} + ```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. @@ -85,16 +85,16 @@ 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 3.10+" - ```Python hl_lines="9" - {!> ../../../docs_src/query_params/tutorial003.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial003_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7" - {!> ../../../docs_src/query_params/tutorial003_py310.py!} + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} ``` Nesse caso, se você for para: @@ -137,16 +137,16 @@ E você não precisa declarar eles em nenhuma ordem específica. Eles serão detectados pelo nome: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="8 10" - {!> ../../../docs_src/query_params/tutorial004.py!} + ```Python hl_lines="6 8" + {!> ../../../docs_src/query_params/tutorial004_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="6 8" - {!> ../../../docs_src/query_params/tutorial004_py310.py!} + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} ``` ## Parâmetros de consulta 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="10" - {!> ../../../docs_src/query_params/tutorial006.py!} + ```Python hl_lines="8" + {!> ../../../docs_src/query_params/tutorial006_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="8" - {!> ../../../docs_src/query_params/tutorial006_py310.py!} + ```Python hl_lines="10" + {!> ../../../docs_src/query_params/tutorial006.py!} ``` + Nesse caso, existem 3 parâmetros de consulta: * `needy`, um `str` obrigatório. diff --git a/docs/pt/docs/tutorial/static-files.md b/docs/pt/docs/tutorial/static-files.md new file mode 100644 index 000000000..009158fc6 --- /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 0858de062..2a8302715 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -77,9 +79,11 @@ nav: - tutorial/request-forms.md - tutorial/request-forms-and-files.md - tutorial/handling-errors.md + - tutorial/encoder.md - Segurança: - tutorial/security/index.md - tutorial/background-tasks.md + - tutorial/static-files.md - Guia de Usuário Avançado: - advanced/index.md - Implantação: @@ -115,7 +119,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -146,6 +150,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -166,6 +172,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/ru/docs/contributing.md b/docs/ru/docs/contributing.md new file mode 100644 index 000000000..cb460beb0 --- /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 -e ."[dev,doc,test]" + +---> 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/fastapi-people.md b/docs/ru/docs/fastapi-people.md new file mode 100644 index 000000000..64ae66a03 --- /dev/null +++ b/docs/ru/docs/fastapi-people.md @@ -0,0 +1,180 @@ + +# Люди, поддерживающие 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 page. 👷 + +## Рейтинг ревьюеров + +Здесь представлен **Рейтинг ревьюеров**. 🕵️ + +### Проверки переводов на другие языки + +Я знаю не очень много языков (и не очень хорошо 😅). +Итак, ревьюеры - это люди, которые могут [**подтвердить предложенный вами перевод** документации](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/ru/docs/help-fastapi.md b/docs/ru/docs/help-fastapi.md new file mode 100644 index 000000000..a69e37bd8 --- /dev/null +++ b/docs/ru/docs/help-fastapi.md @@ -0,0 +1,257 @@ +# Помочь 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}. + + Используйте этот чат только для бесед на отвлечённые темы. + +Существует также чат в Gitter, но поскольку в нем нет каналов и расширенных функций, общение в нём сложнее, потому рекомендуемой системой является Discord. + +### Не использовать чаты для вопросов + +Имейте в виду, что чаты позволяют больше "свободного общения", потому там легко задавать вопросы, которые слишком общие и на которые труднее ответить, так что Вы можете не получить нужные Вам ответы. + +В разделе "проблемы" на 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 000000000..2a5e428b1 --- /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/tutorial/background-tasks.md b/docs/ru/docs/tutorial/background-tasks.md index e608f6c8f..81efda786 100644 --- a/docs/ru/docs/tutorial/background-tasks.md +++ b/docs/ru/docs/tutorial/background-tasks.md @@ -57,16 +57,16 @@ **FastAPI** знает, что нужно сделать в каждом случае и как переиспользовать тот же объект `BackgroundTasks`, так чтобы все фоновые задачи собрались и запустились вместе в фоне: -=== "Python 3.6 и выше" +=== "Python 3.10+" - ```Python hl_lines="13 15 22 25" - {!> ../../../docs_src/background_tasks/tutorial002.py!} + ```Python hl_lines="11 13 20 23" + {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} ``` -=== "Python 3.10 и выше" +=== "Python 3.6+" - ```Python hl_lines="11 13 20 23" - {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} ``` В этом примере сообщения будут записаны в `log.txt` *после* того, как ответ сервера был отправлен. diff --git a/docs/ru/docs/tutorial/body-fields.md b/docs/ru/docs/tutorial/body-fields.md new file mode 100644 index 000000000..674b8bde4 --- /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.6+" + + ```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.6+" + + ```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/cookie-params.md b/docs/ru/docs/tutorial/cookie-params.md new file mode 100644 index 000000000..a6f2caa26 --- /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.6+" + + ```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.6+" + + ```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/mkdocs.yml b/docs/ru/mkdocs.yml index f35ee968c..4b9727872 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -62,12 +64,16 @@ nav: - fastapi-people.md - python-types.md - Учебник - руководство пользователя: + - tutorial/body-fields.md - tutorial/background-tasks.md + - tutorial/cookie-params.md - async.md - Развёртывание: - deployment/index.md - deployment/versions.md +- history-design-future.md - external-links.md +- contributing.md markdown_extensions: - toc: permalink: true @@ -90,7 +96,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -121,6 +127,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -141,6 +149,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index b07f3bc63..f24c7c503 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -80,7 +82,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -111,6 +113,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -131,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index 3332d232d..574cc5abd 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -80,7 +82,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -111,6 +113,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -131,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/ta/mkdocs.yml b/docs/ta/mkdocs.yml new file mode 100644 index 000000000..e31821e4b --- /dev/null +++ b/docs/ta/mkdocs.yml @@ -0,0 +1,148 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/ta/ +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/ + - ta: /ta/ + - 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: /ta/ + name: ta - தமிழ் + - 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/ta/overrides/.gitignore b/docs/ta/overrides/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tr/docs/tutorial/first_steps.md b/docs/tr/docs/tutorial/first_steps.md new file mode 100644 index 000000000..b39802f5d --- /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 e29d25936..19dcf2099 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,12 +56,15 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ - features.md - fastapi-people.md - python-types.md +- Tutorial - User Guide: + - tutorial/first-steps.md markdown_extensions: - toc: permalink: true @@ -83,7 +87,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -114,6 +118,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -134,6 +140,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 711328771..b8152e821 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -80,7 +82,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -111,6 +113,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -131,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/zh/docs/benchmarks.md b/docs/zh/docs/benchmarks.md index 8991c72cd..71e8d4838 100644 --- a/docs/zh/docs/benchmarks.md +++ b/docs/zh/docs/benchmarks.md @@ -1,6 +1,6 @@ # 基准测试 -第三方机构 TechEmpower 的基准测试表明在 Uvicorn 下运行的 **FastAPI** 应用程序是 可用的最快的 Python 框架之一,仅次与 Starlette 和 Uvicorn 本身 (由 FastAPI 内部使用)。(*) +第三方机构 TechEmpower 的基准测试表明在 Uvicorn 下运行的 **FastAPI** 应用程序是 可用的最快的 Python 框架之一,仅次于 Starlette 和 Uvicorn 本身 (由 FastAPI 内部使用)。(*) 但是在查看基准得分和对比时,请注意以下几点。 diff --git a/docs/zh/docs/index.md b/docs/zh/docs/index.md index 7901e9c2c..4db3ef10c 100644 --- a/docs/zh/docs/index.md +++ b/docs/zh/docs/index.md @@ -28,7 +28,7 @@ FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框 关键特性: -* **快速**:可与 **NodeJS** 和 **Go** 比肩的极高性能(归功于 Starlette 和 Pydantic)。[最快的 Python web 框架之一](#_11)。 +* **快速**:可与 **NodeJS** 和 **Go** 并肩的极高性能(归功于 Starlette 和 Pydantic)。[最快的 Python web 框架之一](#_11)。 * **高效编码**:提高功能开发速度约 200% 至 300%。* * **更少 bug**:减少约 40% 的人为(开发者)导致错误。* diff --git a/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md index 5813272ee..f404820df 100644 --- a/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md @@ -6,16 +6,16 @@ 在前面的例子中, 我们从依赖项 ("可依赖对象") 中返回了一个 `dict`: -=== "Python 3.6 以及 以上" +=== "Python 3.10+" - ```Python hl_lines="9" - {!> ../../../docs_src/dependencies/tutorial001.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` -=== "Python 3.10 以及以上" +=== "Python 3.6+" - ```Python hl_lines="7" - {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} ``` 但是后面我们在路径操作函数的参数 `commons` 中得到了一个 `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.6 以及 以上" +=== "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,16 +133,16 @@ fluffy = Cat(name="Mr Fluffy") 现在,您可以使用这个类来声明你的依赖项了。 -=== "Python 3.6 以及 以上" +=== "Python 3.10+" - ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial002.py!} + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` -=== "Python 3.10 以及 以上" +=== "Python 3.6+" - ```Python hl_lines="17" - {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} ``` **FastAPI** 调用 `CommonQueryParams` 类。这将创建该类的一个 "实例",该实例将作为参数 `commons` 被传递给你的函数。 @@ -183,16 +183,16 @@ commons = Depends(CommonQueryParams) ..就像: -=== "Python 3.6 以及 以上" +=== "Python 3.10+" - ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial003.py!} + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial003_py310.py!} ``` -=== "Python 3.10 以及 以上" +=== "Python 3.6+" - ```Python hl_lines="17" - {!> ../../../docs_src/dependencies/tutorial003_py310.py!} + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} ``` 但是声明类型是被鼓励的,因为那样你的编辑器就会知道将传递什么作为参数 `commons` ,然后它可以帮助你完成代码,类型检查,等等: @@ -227,16 +227,16 @@ commons: CommonQueryParams = Depends() 同样的例子看起来像这样: -=== "Python 3.6 以及 以上" +=== "Python 3.10+" - ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial004.py!} + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial004_py310.py!} ``` -=== "Python 3.10 以及 以上" +=== "Python 3.6+" - ```Python hl_lines="17" - {!> ../../../docs_src/dependencies/tutorial004_py310.py!} + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} ``` ... **FastAPI** 会知道怎么处理。 diff --git a/docs/zh/docs/tutorial/encoder.md b/docs/zh/docs/tutorial/encoder.md index cb813940c..76ed846ce 100644 --- a/docs/zh/docs/tutorial/encoder.md +++ b/docs/zh/docs/tutorial/encoder.md @@ -20,16 +20,16 @@ 它接收一个对象,比如Pydantic模型,并会返回一个JSON兼容的版本: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="5 22" - {!> ../../../docs_src/encoder/tutorial001.py!} + ```Python hl_lines="4 21" + {!> ../../../docs_src/encoder/tutorial001_py310.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="4 21" - {!> ../../../docs_src/encoder/tutorial001_py310.py!} + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} ``` 在这个例子中,它将Pydantic模型转换为`dict`,并将`datetime`转换为`str`。 diff --git a/docs/zh/docs/tutorial/request-files.md b/docs/zh/docs/tutorial/request-files.md index e18d6fc9f..03474907e 100644 --- a/docs/zh/docs/tutorial/request-files.md +++ b/docs/zh/docs/tutorial/request-files.md @@ -124,16 +124,16 @@ contents = myfile.file.read() 您可以通过使用标准类型注解并将 None 作为默认值的方式将一个文件参数设为可选: -=== "Python 3.6 及以上版本" +=== "Python 3.9+" - ```Python hl_lines="9 17" - {!> ../../../docs_src/request_files/tutorial001_02.py!} + ```Python hl_lines="7 14" + {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.6+" - ```Python hl_lines="7 14" - {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} ``` ## 带有额外元数据的 `UploadFile` @@ -152,16 +152,16 @@ FastAPI 支持同时上传多个文件。 上传多个文件时,要声明含 `bytes` 或 `UploadFile` 的列表(`List`): -=== "Python 3.6 及以上版本" +=== "Python 3.9+" - ```Python hl_lines="10 15" - {!> ../../../docs_src/request_files/tutorial002.py!} + ```Python hl_lines="8 13" + {!> ../../../docs_src/request_files/tutorial002_py39.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.6+" - ```Python hl_lines="8 13" - {!> ../../../docs_src/request_files/tutorial002_py39.py!} + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002.py!} ``` 接收的也是含 `bytes` 或 `UploadFile` 的列表(`list`)。 @@ -177,16 +177,16 @@ FastAPI 支持同时上传多个文件。 和之前的方式一样, 您可以为 `File()` 设置额外参数, 即使是 `UploadFile`: -=== "Python 3.6 及以上版本" +=== "Python 3.9+" - ```Python hl_lines="18" - {!> ../../../docs_src/request_files/tutorial003.py!} + ```Python hl_lines="16" + {!> ../../../docs_src/request_files/tutorial003_py39.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.6+" - ```Python hl_lines="16" - {!> ../../../docs_src/request_files/tutorial003_py39.py!} + ```Python hl_lines="18" + {!> ../../../docs_src/request_files/tutorial003.py!} ``` ## 小结 diff --git a/docs/zh/docs/tutorial/sql-databases.md b/docs/zh/docs/tutorial/sql-databases.md index 6b354c2b6..482588f94 100644 --- a/docs/zh/docs/tutorial/sql-databases.md +++ b/docs/zh/docs/tutorial/sql-databases.md @@ -246,22 +246,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.6+" - ```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 +290,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.6+" - ```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 +319,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.6+" - ```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,16 +465,16 @@ current_user.items 以非常简单的方式创建数据库表: -=== "Python 3.6 及以上版本" +=== "Python 3.9+" - ```Python hl_lines="9" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.6+" - ```Python hl_lines="7" - {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ```Python hl_lines="9" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} ``` #### Alembic 注意 @@ -499,16 +499,16 @@ current_user.items 我们的依赖项将创建一个新的 SQLAlchemy `SessionLocal`,它将在单个请求中使用,然后在请求完成后关闭它。 -=== "Python 3.6 及以上版本" +=== "Python 3.9+" - ```Python hl_lines="15-20" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ```Python hl_lines="13-18" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.6+" - ```Python hl_lines="13-18" - {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ```Python hl_lines="15-20" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} ``` !!! info @@ -524,16 +524,16 @@ current_user.items *这将为我们在路径操作函数*中提供更好的编辑器支持,因为编辑器将知道`db`参数的类型`Session`: -=== "Python 3.6 及以上版本" +=== "Python 3.9+" - ```Python hl_lines="24 32 38 47 53" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ```Python hl_lines="22 30 36 45 51" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.6+" - ```Python hl_lines="22 30 36 45 51" - {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ```Python hl_lines="24 32 38 47 53" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} ``` !!! info "技术细节" @@ -545,16 +545,16 @@ current_user.items 现在,到了最后,编写标准的**FastAPI** *路径操作*代码。 -=== "Python 3.6 及以上版本" +=== "Python 3.9+" - ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.6+" - ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" - {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} ``` 我们在依赖项中的每个请求之前利用`yield`创建数据库会话,然后关闭它。 @@ -638,22 +638,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.6+" ```Python - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` * `sql_app/crud.py`: @@ -664,16 +664,16 @@ def read_user(user_id: int, db: Session = Depends(get_db)): * `sql_app/main.py`: -=== "Python 3.6 及以上版本" +=== "Python 3.9+" ```Python - {!> ../../../docs_src/sql_databases/sql_app/main.py!} + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.6+" ```Python - {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + {!> ../../../docs_src/sql_databases/sql_app/main.py!} ``` ## 执行项目 @@ -723,16 +723,16 @@ $ uvicorn sql_app.main:app --reload 我们将添加中间件(只是一个函数)将为每个请求创建一个新的 SQLAlchemy`SessionLocal`,将其添加到请求中,然后在请求完成后关闭它。 -=== "Python 3.6 及以上版本" +=== "Python 3.9+" - ```Python hl_lines="14-22" - {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} + ```Python hl_lines="12-20" + {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.6+" - ```Python hl_lines="12-20" - {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} + ```Python hl_lines="14-22" + {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} ``` !!! info diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index f4c3c0ec1..d25881c43 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -55,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -137,7 +139,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -168,6 +170,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ @@ -188,6 +192,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs_src/additional_status_codes/tutorial001_an.py b/docs_src/additional_status_codes/tutorial001_an.py new file mode 100644 index 000000000..b5ad6a16b --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse +from typing_extensions import Annotated + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: Annotated[Union[str, None], Body()] = None, + size: Annotated[Union[int, None], Body()] = None, +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001_an_py310.py b/docs_src/additional_status_codes/tutorial001_an_py310.py new file mode 100644 index 000000000..1865074a1 --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: Annotated[str | None, Body()] = None, + size: Annotated[int | None, Body()] = None, +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001_an_py39.py b/docs_src/additional_status_codes/tutorial001_an_py39.py new file mode 100644 index 000000000..89653dd8a --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: Annotated[Union[str, None], Body()] = None, + size: Annotated[Union[int, None], Body()] = None, +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001_py310.py b/docs_src/additional_status_codes/tutorial001_py310.py new file mode 100644 index 000000000..38bfa455b --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_py310.py @@ -0,0 +1,23 @@ +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: str | None = Body(default=None), + size: int | None = Body(default=None), +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/app_testing/app_b_an/__init__.py b/docs_src/app_testing/app_b_an/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/app_testing/app_b_an/main.py b/docs_src/app_testing/app_b_an/main.py new file mode 100644 index 000000000..c63134fc9 --- /dev/null +++ b/docs_src/app_testing/app_b_an/main.py @@ -0,0 +1,39 @@ +from typing import Union + +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel +from typing_extensions import Annotated + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: Union[str, None] = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_an/test_main.py b/docs_src/app_testing/app_b_an/test_main.py new file mode 100644 index 000000000..d186b8ecb --- /dev/null +++ b/docs_src/app_testing/app_b_an/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/app_testing/app_b_an_py310/__init__.py b/docs_src/app_testing/app_b_an_py310/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/app_testing/app_b_an_py310/main.py b/docs_src/app_testing/app_b_an_py310/main.py new file mode 100644 index 000000000..48c27a0b8 --- /dev/null +++ b/docs_src/app_testing/app_b_an_py310/main.py @@ -0,0 +1,38 @@ +from typing import Annotated + +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: str | None = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_an_py310/test_main.py b/docs_src/app_testing/app_b_an_py310/test_main.py new file mode 100644 index 000000000..d186b8ecb --- /dev/null +++ b/docs_src/app_testing/app_b_an_py310/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/app_testing/app_b_an_py39/__init__.py b/docs_src/app_testing/app_b_an_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/app_testing/app_b_an_py39/main.py b/docs_src/app_testing/app_b_an_py39/main.py new file mode 100644 index 000000000..935a510b7 --- /dev/null +++ b/docs_src/app_testing/app_b_an_py39/main.py @@ -0,0 +1,38 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: Union[str, None] = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_an_py39/test_main.py b/docs_src/app_testing/app_b_an_py39/test_main.py new file mode 100644 index 000000000..d186b8ecb --- /dev/null +++ b/docs_src/app_testing/app_b_an_py39/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/app_testing/tutorial002.py b/docs_src/app_testing/tutorial002.py index b4a9c0586..71c898b3c 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 000000000..f63502b09 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_an.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import BackgroundTasks, Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: Union[str, None] = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/background_tasks/tutorial002_an_py310.py b/docs_src/background_tasks/tutorial002_an_py310.py new file mode 100644 index 000000000..1fc78fbc9 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_an_py310.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import BackgroundTasks, Depends, FastAPI + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: str | None = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/background_tasks/tutorial002_an_py39.py b/docs_src/background_tasks/tutorial002_an_py39.py new file mode 100644 index 000000000..bfdd14875 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_an_py39.py @@ -0,0 +1,26 @@ +from typing import Annotated, Union + +from fastapi import BackgroundTasks, Depends, FastAPI + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: Union[str, None] = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/bigger_applications/app_an/__init__.py b/docs_src/bigger_applications/app_an/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an/dependencies.py b/docs_src/bigger_applications/app_an/dependencies.py new file mode 100644 index 000000000..1374c54b3 --- /dev/null +++ b/docs_src/bigger_applications/app_an/dependencies.py @@ -0,0 +1,12 @@ +from fastapi import Header, HTTPException +from typing_extensions import Annotated + + +async def get_token_header(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def get_query_token(token: str): + if token != "jessica": + raise HTTPException(status_code=400, detail="No Jessica token provided") diff --git a/docs_src/bigger_applications/app_an/internal/__init__.py b/docs_src/bigger_applications/app_an/internal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an/internal/admin.py b/docs_src/bigger_applications/app_an/internal/admin.py new file mode 100644 index 000000000..99d3da86b --- /dev/null +++ b/docs_src/bigger_applications/app_an/internal/admin.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.post("/") +async def update_admin(): + return {"message": "Admin getting schwifty"} diff --git a/docs_src/bigger_applications/app_an/main.py b/docs_src/bigger_applications/app_an/main.py new file mode 100644 index 000000000..ae544a3aa --- /dev/null +++ b/docs_src/bigger_applications/app_an/main.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +from .dependencies import get_query_token, get_token_header +from .internal import admin +from .routers import items, users + +app = FastAPI(dependencies=[Depends(get_query_token)]) + + +app.include_router(users.router) +app.include_router(items.router) +app.include_router( + admin.router, + prefix="/admin", + tags=["admin"], + dependencies=[Depends(get_token_header)], + responses={418: {"description": "I'm a teapot"}}, +) + + +@app.get("/") +async def root(): + return {"message": "Hello Bigger Applications!"} diff --git a/docs_src/bigger_applications/app_an/routers/__init__.py b/docs_src/bigger_applications/app_an/routers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an/routers/items.py b/docs_src/bigger_applications/app_an/routers/items.py new file mode 100644 index 000000000..bde9ff4d5 --- /dev/null +++ b/docs_src/bigger_applications/app_an/routers/items.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, Depends, HTTPException + +from ..dependencies import get_token_header + +router = APIRouter( + prefix="/items", + tags=["items"], + dependencies=[Depends(get_token_header)], + responses={404: {"description": "Not found"}}, +) + + +fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}} + + +@router.get("/") +async def read_items(): + return fake_items_db + + +@router.get("/{item_id}") +async def read_item(item_id: str): + if item_id not in fake_items_db: + raise HTTPException(status_code=404, detail="Item not found") + return {"name": fake_items_db[item_id]["name"], "item_id": item_id} + + +@router.put( + "/{item_id}", + tags=["custom"], + responses={403: {"description": "Operation forbidden"}}, +) +async def update_item(item_id: str): + if item_id != "plumbus": + raise HTTPException( + status_code=403, detail="You can only update the item: plumbus" + ) + return {"item_id": item_id, "name": "The great Plumbus"} diff --git a/docs_src/bigger_applications/app_an/routers/users.py b/docs_src/bigger_applications/app_an/routers/users.py new file mode 100644 index 000000000..39b3d7e7c --- /dev/null +++ b/docs_src/bigger_applications/app_an/routers/users.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/users/", tags=["users"]) +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] + + +@router.get("/users/me", tags=["users"]) +async def read_user_me(): + return {"username": "fakecurrentuser"} + + +@router.get("/users/{username}", tags=["users"]) +async def read_user(username: str): + return {"username": username} diff --git a/docs_src/bigger_applications/app_an_py39/__init__.py b/docs_src/bigger_applications/app_an_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an_py39/dependencies.py b/docs_src/bigger_applications/app_an_py39/dependencies.py new file mode 100644 index 000000000..5c7612aa0 --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/dependencies.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Header, HTTPException + + +async def get_token_header(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def get_query_token(token: str): + if token != "jessica": + raise HTTPException(status_code=400, detail="No Jessica token provided") diff --git a/docs_src/bigger_applications/app_an_py39/internal/__init__.py b/docs_src/bigger_applications/app_an_py39/internal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an_py39/internal/admin.py b/docs_src/bigger_applications/app_an_py39/internal/admin.py new file mode 100644 index 000000000..99d3da86b --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/internal/admin.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.post("/") +async def update_admin(): + return {"message": "Admin getting schwifty"} diff --git a/docs_src/bigger_applications/app_an_py39/main.py b/docs_src/bigger_applications/app_an_py39/main.py new file mode 100644 index 000000000..ae544a3aa --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/main.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +from .dependencies import get_query_token, get_token_header +from .internal import admin +from .routers import items, users + +app = FastAPI(dependencies=[Depends(get_query_token)]) + + +app.include_router(users.router) +app.include_router(items.router) +app.include_router( + admin.router, + prefix="/admin", + tags=["admin"], + dependencies=[Depends(get_token_header)], + responses={418: {"description": "I'm a teapot"}}, +) + + +@app.get("/") +async def root(): + return {"message": "Hello Bigger Applications!"} diff --git a/docs_src/bigger_applications/app_an_py39/routers/__init__.py b/docs_src/bigger_applications/app_an_py39/routers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an_py39/routers/items.py b/docs_src/bigger_applications/app_an_py39/routers/items.py new file mode 100644 index 000000000..bde9ff4d5 --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/routers/items.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, Depends, HTTPException + +from ..dependencies import get_token_header + +router = APIRouter( + prefix="/items", + tags=["items"], + dependencies=[Depends(get_token_header)], + responses={404: {"description": "Not found"}}, +) + + +fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}} + + +@router.get("/") +async def read_items(): + return fake_items_db + + +@router.get("/{item_id}") +async def read_item(item_id: str): + if item_id not in fake_items_db: + raise HTTPException(status_code=404, detail="Item not found") + return {"name": fake_items_db[item_id]["name"], "item_id": item_id} + + +@router.put( + "/{item_id}", + tags=["custom"], + responses={403: {"description": "Operation forbidden"}}, +) +async def update_item(item_id: str): + if item_id != "plumbus": + raise HTTPException( + status_code=403, detail="You can only update the item: plumbus" + ) + return {"item_id": item_id, "name": "The great Plumbus"} diff --git a/docs_src/bigger_applications/app_an_py39/routers/users.py b/docs_src/bigger_applications/app_an_py39/routers/users.py new file mode 100644 index 000000000..39b3d7e7c --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/routers/users.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/users/", tags=["users"]) +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] + + +@router.get("/users/me", tags=["users"]) +async def read_user_me(): + return {"username": "fakecurrentuser"} + + +@router.get("/users/{username}", tags=["users"]) +async def read_user(username: str): + return {"username": username} diff --git a/docs_src/body_fields/tutorial001_an.py b/docs_src/body_fields/tutorial001_an.py new file mode 100644 index 000000000..15ea1b53d --- /dev/null +++ b/docs_src/body_fields/tutorial001_an.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = Field( + default=None, title="The description of the item", max_length=300 + ) + price: float = Field(gt=0, description="The price must be greater than zero") + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_fields/tutorial001_an_py310.py b/docs_src/body_fields/tutorial001_an_py310.py new file mode 100644 index 000000000..c9d99e1c2 --- /dev/null +++ b/docs_src/body_fields/tutorial001_an_py310.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = Field( + default=None, title="The description of the item", max_length=300 + ) + price: float = Field(gt=0, description="The price must be greater than zero") + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_fields/tutorial001_an_py39.py b/docs_src/body_fields/tutorial001_an_py39.py new file mode 100644 index 000000000..6ef14470c --- /dev/null +++ b/docs_src/body_fields/tutorial001_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = Field( + default=None, title="The description of the item", max_length=300 + ) + price: float = Field(gt=0, description="The price must be greater than zero") + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial001_an.py b/docs_src/body_multiple_params/tutorial001_an.py new file mode 100644 index 000000000..308eee854 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_an.py @@ -0,0 +1,28 @@ +from typing import Union + +from fastapi import FastAPI, Path +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: Union[str, None] = None, + item: Union[Item, None] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial001_an_py310.py b/docs_src/body_multiple_params/tutorial001_an_py310.py new file mode 100644 index 000000000..2d79c19c2 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_an_py310.py @@ -0,0 +1,27 @@ +from typing import Annotated + +from fastapi import FastAPI, Path +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: str | None = None, + item: Item | None = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial001_an_py39.py b/docs_src/body_multiple_params/tutorial001_an_py39.py new file mode 100644 index 000000000..1c0ac3a7b --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_an_py39.py @@ -0,0 +1,27 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Path +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: Union[str, None] = None, + item: Union[Item, None] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial003_an.py b/docs_src/body_multiple_params/tutorial003_an.py new file mode 100644 index 000000000..39ef7340a --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_an.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: Annotated[int, Body()] +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial003_an_py310.py b/docs_src/body_multiple_params/tutorial003_an_py310.py new file mode 100644 index 000000000..593ff893c --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_an_py310.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: Annotated[int, Body()] +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial003_an_py39.py b/docs_src/body_multiple_params/tutorial003_an_py39.py new file mode 100644 index 000000000..042351e0b --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_an_py39.py @@ -0,0 +1,26 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: Annotated[int, Body()] +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial004.py b/docs_src/body_multiple_params/tutorial004.py index beea7d1e3..8ce4c7a97 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 000000000..f6830f392 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_an.py @@ -0,0 +1,34 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: Annotated[int, Body(gt=0)], + q: Union[str, None] = None, +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial004_an_py310.py b/docs_src/body_multiple_params/tutorial004_an_py310.py new file mode 100644 index 000000000..cd5c66fef --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_an_py310.py @@ -0,0 +1,33 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: Annotated[int, Body(gt=0)], + q: str | None = None, +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial004_an_py39.py b/docs_src/body_multiple_params/tutorial004_an_py39.py new file mode 100644 index 000000000..567427c03 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_an_py39.py @@ -0,0 +1,33 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: Annotated[int, Body(gt=0)], + q: Union[str, None] = None, +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial004_py310.py b/docs_src/body_multiple_params/tutorial004_py310.py index 6d495d408..14acbc3b4 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 000000000..dadde80b5 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_an.py @@ -0,0 +1,20 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial005_an_py310.py b/docs_src/body_multiple_params/tutorial005_an_py310.py new file mode 100644 index 000000000..f97680ba0 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial005_an_py39.py b/docs_src/body_multiple_params/tutorial005_an_py39.py new file mode 100644 index 000000000..9a52425c1 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_an_py39.py @@ -0,0 +1,19 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/cookie_params/tutorial001_an.py b/docs_src/cookie_params/tutorial001_an.py new file mode 100644 index 000000000..6d5931229 --- /dev/null +++ b/docs_src/cookie_params/tutorial001_an.py @@ -0,0 +1,11 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: Annotated[Union[str, None], Cookie()] = None): + return {"ads_id": ads_id} diff --git a/docs_src/cookie_params/tutorial001_an_py310.py b/docs_src/cookie_params/tutorial001_an_py310.py new file mode 100644 index 000000000..69ac683fe --- /dev/null +++ b/docs_src/cookie_params/tutorial001_an_py310.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import Cookie, FastAPI + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: Annotated[str | None, Cookie()] = None): + return {"ads_id": ads_id} diff --git a/docs_src/cookie_params/tutorial001_an_py39.py b/docs_src/cookie_params/tutorial001_an_py39.py new file mode 100644 index 000000000..e18d0a332 --- /dev/null +++ b/docs_src/cookie_params/tutorial001_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated, Union + +from fastapi import Cookie, FastAPI + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: Annotated[Union[str, None], Cookie()] = None): + return {"ads_id": ads_id} diff --git a/docs_src/dependencies/tutorial001_02_an.py b/docs_src/dependencies/tutorial001_02_an.py new file mode 100644 index 000000000..455d60c82 --- /dev/null +++ b/docs_src/dependencies/tutorial001_02_an.py @@ -0,0 +1,25 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +CommonsDep = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonsDep): + return commons + + +@app.get("/users/") +async def read_users(commons: CommonsDep): + return commons diff --git a/docs_src/dependencies/tutorial001_02_an_py310.py b/docs_src/dependencies/tutorial001_02_an_py310.py new file mode 100644 index 000000000..f59637bb2 --- /dev/null +++ b/docs_src/dependencies/tutorial001_02_an_py310.py @@ -0,0 +1,22 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +CommonsDep = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonsDep): + return commons + + +@app.get("/users/") +async def read_users(commons: CommonsDep): + return commons diff --git a/docs_src/dependencies/tutorial001_02_an_py39.py b/docs_src/dependencies/tutorial001_02_an_py39.py new file mode 100644 index 000000000..df969ae9c --- /dev/null +++ b/docs_src/dependencies/tutorial001_02_an_py39.py @@ -0,0 +1,24 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +CommonsDep = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonsDep): + return commons + + +@app.get("/users/") +async def read_users(commons: CommonsDep): + return commons diff --git a/docs_src/dependencies/tutorial001_an.py b/docs_src/dependencies/tutorial001_an.py new file mode 100644 index 000000000..81e24fe86 --- /dev/null +++ b/docs_src/dependencies/tutorial001_an.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return commons diff --git a/docs_src/dependencies/tutorial001_an_py310.py b/docs_src/dependencies/tutorial001_an_py310.py new file mode 100644 index 000000000..f2e57ae7b --- /dev/null +++ b/docs_src/dependencies/tutorial001_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return commons diff --git a/docs_src/dependencies/tutorial001_an_py39.py b/docs_src/dependencies/tutorial001_an_py39.py new file mode 100644 index 000000000..5d9fe6ddf --- /dev/null +++ b/docs_src/dependencies/tutorial001_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return commons diff --git a/docs_src/dependencies/tutorial002_an.py b/docs_src/dependencies/tutorial002_an.py new file mode 100644 index 000000000..964ccf66c --- /dev/null +++ b/docs_src/dependencies/tutorial002_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial002_an_py310.py b/docs_src/dependencies/tutorial002_an_py310.py new file mode 100644 index 000000000..077726312 --- /dev/null +++ b/docs_src/dependencies/tutorial002_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial002_an_py39.py b/docs_src/dependencies/tutorial002_an_py39.py new file mode 100644 index 000000000..844a23c5a --- /dev/null +++ b/docs_src/dependencies/tutorial002_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_an.py b/docs_src/dependencies/tutorial003_an.py new file mode 100644 index 000000000..ba8e9f717 --- /dev/null +++ b/docs_src/dependencies/tutorial003_an.py @@ -0,0 +1,26 @@ +from typing import Any, Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_an_py310.py b/docs_src/dependencies/tutorial003_an_py310.py new file mode 100644 index 000000000..44116989b --- /dev/null +++ b/docs_src/dependencies/tutorial003_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated, Any + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_an_py39.py b/docs_src/dependencies/tutorial003_an_py39.py new file mode 100644 index 000000000..9e9123ad2 --- /dev/null +++ b/docs_src/dependencies/tutorial003_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Any, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_an.py b/docs_src/dependencies/tutorial004_an.py new file mode 100644 index 000000000..78881a354 --- /dev/null +++ b/docs_src/dependencies/tutorial004_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends()]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_an_py310.py b/docs_src/dependencies/tutorial004_an_py310.py new file mode 100644 index 000000000..2c009efcc --- /dev/null +++ b/docs_src/dependencies/tutorial004_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends()]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_an_py39.py b/docs_src/dependencies/tutorial004_an_py39.py new file mode 100644 index 000000000..74268626b --- /dev/null +++ b/docs_src/dependencies/tutorial004_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends()]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial005_an.py b/docs_src/dependencies/tutorial005_an.py new file mode 100644 index 000000000..6785099da --- /dev/null +++ b/docs_src/dependencies/tutorial005_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Cookie, Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +def query_extractor(q: Union[str, None] = None): + return q + + +def query_or_cookie_extractor( + q: Annotated[str, Depends(query_extractor)], + last_query: Annotated[Union[str, None], Cookie()] = None, +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query( + query_or_default: Annotated[str, Depends(query_or_cookie_extractor)] +): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial005_an_py310.py b/docs_src/dependencies/tutorial005_an_py310.py new file mode 100644 index 000000000..6c0aa0b36 --- /dev/null +++ b/docs_src/dependencies/tutorial005_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Cookie, Depends, FastAPI + +app = FastAPI() + + +def query_extractor(q: str | None = None): + return q + + +def query_or_cookie_extractor( + q: Annotated[str, Depends(query_extractor)], + last_query: Annotated[str | None, Cookie()] = None, +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query( + query_or_default: Annotated[str, Depends(query_or_cookie_extractor)] +): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial005_an_py39.py b/docs_src/dependencies/tutorial005_an_py39.py new file mode 100644 index 000000000..e8887e162 --- /dev/null +++ b/docs_src/dependencies/tutorial005_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Cookie, Depends, FastAPI + +app = FastAPI() + + +def query_extractor(q: Union[str, None] = None): + return q + + +def query_or_cookie_extractor( + q: Annotated[str, Depends(query_extractor)], + last_query: Annotated[Union[str, None], Cookie()] = None, +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query( + query_or_default: Annotated[str, Depends(query_or_cookie_extractor)] +): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial006_an.py b/docs_src/dependencies/tutorial006_an.py new file mode 100644 index 000000000..5aaea04d1 --- /dev/null +++ b/docs_src/dependencies/tutorial006_an.py @@ -0,0 +1,20 @@ +from fastapi import Depends, FastAPI, Header, HTTPException +from typing_extensions import Annotated + +app = FastAPI() + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)]) +async def read_items(): + return [{"item": "Foo"}, {"item": "Bar"}] diff --git a/docs_src/dependencies/tutorial006_an_py39.py b/docs_src/dependencies/tutorial006_an_py39.py new file mode 100644 index 000000000..11976ed6a --- /dev/null +++ b/docs_src/dependencies/tutorial006_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI, Header, HTTPException + +app = FastAPI() + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)]) +async def read_items(): + return [{"item": "Foo"}, {"item": "Bar"}] diff --git a/docs_src/dependencies/tutorial008_an.py b/docs_src/dependencies/tutorial008_an.py new file mode 100644 index 000000000..2de86f042 --- /dev/null +++ b/docs_src/dependencies/tutorial008_an.py @@ -0,0 +1,26 @@ +from fastapi import Depends +from typing_extensions import Annotated + + +async def dependency_a(): + dep_a = generate_dep_a() + try: + yield dep_a + finally: + dep_a.close() + + +async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]): + dep_b = generate_dep_b() + try: + yield dep_b + finally: + dep_b.close(dep_a) + + +async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]): + dep_c = generate_dep_c() + try: + yield dep_c + finally: + dep_c.close(dep_b) diff --git a/docs_src/dependencies/tutorial008_an_py39.py b/docs_src/dependencies/tutorial008_an_py39.py new file mode 100644 index 000000000..acc804c32 --- /dev/null +++ b/docs_src/dependencies/tutorial008_an_py39.py @@ -0,0 +1,27 @@ +from typing import Annotated + +from fastapi import Depends + + +async def dependency_a(): + dep_a = generate_dep_a() + try: + yield dep_a + finally: + dep_a.close() + + +async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]): + dep_b = generate_dep_b() + try: + yield dep_b + finally: + dep_b.close(dep_a) + + +async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]): + dep_c = generate_dep_c() + try: + yield dep_c + finally: + dep_c.close(dep_b) diff --git a/docs_src/dependencies/tutorial011_an.py b/docs_src/dependencies/tutorial011_an.py new file mode 100644 index 000000000..6c13d9033 --- /dev/null +++ b/docs_src/dependencies/tutorial011_an.py @@ -0,0 +1,22 @@ +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +class FixedContentQueryChecker: + def __init__(self, fixed_content: str): + self.fixed_content = fixed_content + + def __call__(self, q: str = ""): + if q: + return self.fixed_content in q + return False + + +checker = FixedContentQueryChecker("bar") + + +@app.get("/query-checker/") +async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]): + return {"fixed_content_in_query": fixed_content_included} diff --git a/docs_src/dependencies/tutorial011_an_py39.py b/docs_src/dependencies/tutorial011_an_py39.py new file mode 100644 index 000000000..68b7434ec --- /dev/null +++ b/docs_src/dependencies/tutorial011_an_py39.py @@ -0,0 +1,23 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +class FixedContentQueryChecker: + def __init__(self, fixed_content: str): + self.fixed_content = fixed_content + + def __call__(self, q: str = ""): + if q: + return self.fixed_content in q + return False + + +checker = FixedContentQueryChecker("bar") + + +@app.get("/query-checker/") +async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]): + return {"fixed_content_in_query": fixed_content_included} diff --git a/docs_src/dependencies/tutorial012_an.py b/docs_src/dependencies/tutorial012_an.py new file mode 100644 index 000000000..7541e6bf4 --- /dev/null +++ b/docs_src/dependencies/tutorial012_an.py @@ -0,0 +1,26 @@ +from fastapi import Depends, FastAPI, Header, HTTPException +from typing_extensions import Annotated + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)]) + + +@app.get("/items/") +async def read_items(): + return [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +@app.get("/users/") +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] diff --git a/docs_src/dependencies/tutorial012_an_py39.py b/docs_src/dependencies/tutorial012_an_py39.py new file mode 100644 index 000000000..7541e6bf4 --- /dev/null +++ b/docs_src/dependencies/tutorial012_an_py39.py @@ -0,0 +1,26 @@ +from fastapi import Depends, FastAPI, Header, HTTPException +from typing_extensions import Annotated + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)]) + + +@app.get("/items/") +async def read_items(): + return [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +@app.get("/users/") +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] diff --git a/docs_src/dependency_testing/tutorial001_an.py b/docs_src/dependency_testing/tutorial001_an.py new file mode 100644 index 000000000..4c76a87ff --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_an.py @@ -0,0 +1,60 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: Union[str, None] = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/dependency_testing/tutorial001_an_py310.py b/docs_src/dependency_testing/tutorial001_an_py310.py new file mode 100644 index 000000000..f866e7a1b --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_an_py310.py @@ -0,0 +1,57 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: str | None = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/dependency_testing/tutorial001_an_py39.py b/docs_src/dependency_testing/tutorial001_an_py39.py new file mode 100644 index 000000000..bccb0cdb1 --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_an_py39.py @@ -0,0 +1,59 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: Union[str, None] = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/dependency_testing/tutorial001_py310.py b/docs_src/dependency_testing/tutorial001_py310.py new file mode 100644 index 000000000..d1f6d4374 --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_py310.py @@ -0,0 +1,55 @@ +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: dict = Depends(common_parameters)): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: dict = Depends(common_parameters)): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: str | None = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/events/tutorial003.py b/docs_src/events/tutorial003.py new file mode 100644 index 000000000..2b650590b --- /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/extra_data_types/tutorial001_an.py b/docs_src/extra_data_types/tutorial001_an.py new file mode 100644 index 000000000..a4c074241 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_an.py @@ -0,0 +1,29 @@ +from datetime import datetime, time, timedelta +from typing import Union +from uuid import UUID + +from fastapi import Body, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: Annotated[Union[datetime, None], Body()] = None, + end_datetime: Annotated[Union[datetime, None], Body()] = None, + repeat_at: Annotated[Union[time, None], Body()] = None, + process_after: Annotated[Union[timedelta, None], Body()] = None, +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/extra_data_types/tutorial001_an_py310.py b/docs_src/extra_data_types/tutorial001_an_py310.py new file mode 100644 index 000000000..4f69c40d9 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_an_py310.py @@ -0,0 +1,28 @@ +from datetime import datetime, time, timedelta +from typing import Annotated +from uuid import UUID + +from fastapi import Body, FastAPI + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: Annotated[datetime | None, Body()] = None, + end_datetime: Annotated[datetime | None, Body()] = None, + repeat_at: Annotated[time | None, Body()] = None, + process_after: Annotated[timedelta | None, Body()] = None, +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/extra_data_types/tutorial001_an_py39.py b/docs_src/extra_data_types/tutorial001_an_py39.py new file mode 100644 index 000000000..630d36ae3 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_an_py39.py @@ -0,0 +1,28 @@ +from datetime import datetime, time, timedelta +from typing import Annotated, Union +from uuid import UUID + +from fastapi import Body, FastAPI + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: Annotated[Union[datetime, None], Body()] = None, + end_datetime: Annotated[Union[datetime, None], Body()] = None, + repeat_at: Annotated[Union[time, None], Body()] = None, + process_after: Annotated[Union[timedelta, None], Body()] = None, +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/header_params/tutorial001_an.py b/docs_src/header_params/tutorial001_an.py new file mode 100644 index 000000000..816c00086 --- /dev/null +++ b/docs_src/header_params/tutorial001_an.py @@ -0,0 +1,11 @@ +from typing import Union + +from fastapi import FastAPI, Header +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: Annotated[Union[str, None], Header()] = None): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial001_an_py310.py b/docs_src/header_params/tutorial001_an_py310.py new file mode 100644 index 000000000..4ba5e1bfb --- /dev/null +++ b/docs_src/header_params/tutorial001_an_py310.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: Annotated[str | None, Header()] = None): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial001_an_py39.py b/docs_src/header_params/tutorial001_an_py39.py new file mode 100644 index 000000000..1fbe3bb99 --- /dev/null +++ b/docs_src/header_params/tutorial001_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: Annotated[Union[str, None], Header()] = None): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial002_an.py b/docs_src/header_params/tutorial002_an.py new file mode 100644 index 000000000..65d972d46 --- /dev/null +++ b/docs_src/header_params/tutorial002_an.py @@ -0,0 +1,15 @@ +from typing import Union + +from fastapi import FastAPI, Header +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial002_an_py310.py b/docs_src/header_params/tutorial002_an_py310.py new file mode 100644 index 000000000..b340647b6 --- /dev/null +++ b/docs_src/header_params/tutorial002_an_py310.py @@ -0,0 +1,12 @@ +from typing import Annotated + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: Annotated[str | None, Header(convert_underscores=False)] = None +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial002_an_py39.py b/docs_src/header_params/tutorial002_an_py39.py new file mode 100644 index 000000000..7f6a99f9c --- /dev/null +++ b/docs_src/header_params/tutorial002_an_py39.py @@ -0,0 +1,14 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial003_an.py b/docs_src/header_params/tutorial003_an.py new file mode 100644 index 000000000..5406fd1f8 --- /dev/null +++ b/docs_src/header_params/tutorial003_an.py @@ -0,0 +1,11 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Annotated[Union[List[str], None], Header()] = None): + return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_an_py310.py b/docs_src/header_params/tutorial003_an_py310.py new file mode 100644 index 000000000..0e78cf029 --- /dev/null +++ b/docs_src/header_params/tutorial003_an_py310.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Annotated[list[str] | None, Header()] = None): + return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_an_py39.py b/docs_src/header_params/tutorial003_an_py39.py new file mode 100644 index 000000000..c1dd49961 --- /dev/null +++ b/docs_src/header_params/tutorial003_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated, List, Union + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Annotated[Union[List[str], None], Header()] = None): + return {"X-Token values": x_token} diff --git a/docs_src/path_params_numeric_validations/tutorial001_an.py b/docs_src/path_params_numeric_validations/tutorial001_an.py new file mode 100644 index 000000000..621be7b04 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_an.py @@ -0,0 +1,17 @@ +from typing import Union + +from fastapi import FastAPI, Path, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + q: Annotated[Union[str, None], Query(alias="item-query")] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial001_an_py310.py b/docs_src/path_params_numeric_validations/tutorial001_an_py310.py new file mode 100644 index 000000000..c4db263f0 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_an_py310.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + q: Annotated[str | None, Query(alias="item-query")] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial001_an_py39.py b/docs_src/path_params_numeric_validations/tutorial001_an_py39.py new file mode 100644 index 000000000..b36315a46 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_an_py39.py @@ -0,0 +1,16 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + q: Annotated[Union[str, None], Query(alias="item-query")] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial002_an.py b/docs_src/path_params_numeric_validations/tutorial002_an.py new file mode 100644 index 000000000..322f8cf0b --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial002_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + q: str, item_id: Annotated[int, Path(title="The ID of the item to get")] +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial002_an_py39.py b/docs_src/path_params_numeric_validations/tutorial002_an_py39.py new file mode 100644 index 000000000..cd882abb2 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial002_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + q: str, item_id: Annotated[int, Path(title="The ID of the item to get")] +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial003_an.py b/docs_src/path_params_numeric_validations/tutorial003_an.py new file mode 100644 index 000000000..d0fa8b3db --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial003_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial003_an_py39.py b/docs_src/path_params_numeric_validations/tutorial003_an_py39.py new file mode 100644 index 000000000..1588556e7 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial003_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial004_an.py b/docs_src/path_params_numeric_validations/tutorial004_an.py new file mode 100644 index 000000000..ffc50f6c5 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial004_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=1)], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial004_an_py39.py b/docs_src/path_params_numeric_validations/tutorial004_an_py39.py new file mode 100644 index 000000000..f67f6450e --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial004_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=1)], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial005_an.py b/docs_src/path_params_numeric_validations/tutorial005_an.py new file mode 100644 index 000000000..433c69129 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial005_an.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", gt=0, le=1000)], + q: str, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial005_an_py39.py b/docs_src/path_params_numeric_validations/tutorial005_an_py39.py new file mode 100644 index 000000000..571dd583c --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial005_an_py39.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", gt=0, le=1000)], + q: str, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial006.py b/docs_src/path_params_numeric_validations/tutorial006.py index 85bd6e8b4..0ea32694a 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 000000000..22a143623 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial006_an.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Path, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + *, + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: str, + size: Annotated[float, Query(gt=0, lt=10.5)], +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial006_an_py39.py b/docs_src/path_params_numeric_validations/tutorial006_an_py39.py new file mode 100644 index 000000000..804751893 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial006_an_py39.py @@ -0,0 +1,18 @@ +from typing import Annotated + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + *, + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: str, + size: Annotated[float, Query(gt=0, lt=10.5)], +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/python_types/tutorial013.py b/docs_src/python_types/tutorial013.py new file mode 100644 index 000000000..0ec773519 --- /dev/null +++ b/docs_src/python_types/tutorial013.py @@ -0,0 +1,5 @@ +from typing_extensions import Annotated + + +def say_hello(name: Annotated[str, "this is just metadata"]) -> str: + return f"Hello {name}" diff --git a/docs_src/python_types/tutorial013_py39.py b/docs_src/python_types/tutorial013_py39.py new file mode 100644 index 000000000..65a0eaa93 --- /dev/null +++ b/docs_src/python_types/tutorial013_py39.py @@ -0,0 +1,5 @@ +from typing import Annotated + + +def say_hello(name: Annotated[str, "this is just metadata"]) -> str: + return f"Hello {name}" diff --git a/docs_src/query_params_str_validations/tutorial002_an.py b/docs_src/query_params_str_validations/tutorial002_an.py new file mode 100644 index 000000000..cb1b38940 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial002_an.py @@ -0,0 +1,14 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(max_length=50)] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial002_an_py310.py b/docs_src/query_params_str_validations/tutorial002_an_py310.py new file mode 100644 index 000000000..66ee7451b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial002_an_py310.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query(max_length=50)] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_an.py b/docs_src/query_params_str_validations/tutorial003_an.py new file mode 100644 index 000000000..a3665f6a8 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_an.py @@ -0,0 +1,16 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(min_length=3, max_length=50)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_an_py310.py b/docs_src/query_params_str_validations/tutorial003_an_py310.py new file mode 100644 index 000000000..836af04de --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_an_py310.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[str | None, Query(min_length=3, max_length=50)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_an_py39.py b/docs_src/query_params_str_validations/tutorial003_an_py39.py new file mode 100644 index 000000000..87a426839 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(min_length=3, max_length=50)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an.py b/docs_src/query_params_str_validations/tutorial004_an.py new file mode 100644 index 000000000..5346b997b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an.py @@ -0,0 +1,18 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$") + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an_py310.py b/docs_src/query_params_str_validations/tutorial004_an_py310.py new file mode 100644 index 000000000..8fd375b3d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an_py310.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, Query(min_length=3, max_length=50, regex="^fixedquery$") + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an_py39.py b/docs_src/query_params_str_validations/tutorial004_an_py39.py new file mode 100644 index 000000000..2fd82db75 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$") + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial005_an.py b/docs_src/query_params_str_validations/tutorial005_an.py new file mode 100644 index 000000000..452d4d38d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial005_an.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial005_an_py39.py b/docs_src/query_params_str_validations/tutorial005_an_py39.py new file mode 100644 index 000000000..b1f6046b5 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial005_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006_an.py b/docs_src/query_params_str_validations/tutorial006_an.py new file mode 100644 index 000000000..559480d2b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006_an.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)]): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006_an_py39.py b/docs_src/query_params_str_validations/tutorial006_an_py39.py new file mode 100644 index 000000000..3b4a676d2 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)]): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006b_an.py b/docs_src/query_params_str_validations/tutorial006b_an.py new file mode 100644 index 000000000..ea3b02583 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006b_an.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006b_an_py39.py b/docs_src/query_params_str_validations/tutorial006b_an_py39.py new file mode 100644 index 000000000..687a9f544 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006b_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an.py b/docs_src/query_params_str_validations/tutorial006c_an.py new file mode 100644 index 000000000..10bf26a57 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_an.py @@ -0,0 +1,14 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an_py310.py b/docs_src/query_params_str_validations/tutorial006c_an_py310.py new file mode 100644 index 000000000..1ab0a7d53 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_an_py310.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an_py39.py b/docs_src/query_params_str_validations/tutorial006c_an_py39.py new file mode 100644 index 000000000..ac1273331 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006d_an.py b/docs_src/query_params_str_validations/tutorial006d_an.py new file mode 100644 index 000000000..bc8283e15 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006d_an.py @@ -0,0 +1,13 @@ +from fastapi import FastAPI, Query +from pydantic import Required +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = Required): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py39.py b/docs_src/query_params_str_validations/tutorial006d_an_py39.py new file mode 100644 index 000000000..035d9e3bd --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006d_an_py39.py @@ -0,0 +1,14 @@ +from typing import Annotated + +from fastapi import FastAPI, Query +from pydantic import Required + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = Required): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_an.py b/docs_src/query_params_str_validations/tutorial007_an.py new file mode 100644 index 000000000..3bc85cc0c --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_an.py @@ -0,0 +1,16 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(title="Query string", min_length=3)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_an_py310.py b/docs_src/query_params_str_validations/tutorial007_an_py310.py new file mode 100644 index 000000000..5933911fd --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_an_py310.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[str | None, Query(title="Query string", min_length=3)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_an_py39.py b/docs_src/query_params_str_validations/tutorial007_an_py39.py new file mode 100644 index 000000000..dafa1c5c9 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(title="Query string", min_length=3)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_an.py b/docs_src/query_params_str_validations/tutorial008_an.py new file mode 100644 index 000000000..5699f1e88 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_an.py @@ -0,0 +1,23 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_an_py310.py b/docs_src/query_params_str_validations/tutorial008_an_py310.py new file mode 100644 index 000000000..4aaadf8b4 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_an_py310.py @@ -0,0 +1,22 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, + Query( + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_an_py39.py b/docs_src/query_params_str_validations/tutorial008_an_py39.py new file mode 100644 index 000000000..1c3b36176 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_an_py39.py @@ -0,0 +1,22 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial009_an.py b/docs_src/query_params_str_validations/tutorial009_an.py new file mode 100644 index 000000000..2894e2d51 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_an.py @@ -0,0 +1,14 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(alias="item-query")] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial009_an_py310.py b/docs_src/query_params_str_validations/tutorial009_an_py310.py new file mode 100644 index 000000000..d11753362 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_an_py310.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query(alias="item-query")] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial009_an_py39.py b/docs_src/query_params_str_validations/tutorial009_an_py39.py new file mode 100644 index 000000000..70a89e613 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(alias="item-query")] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_an.py b/docs_src/query_params_str_validations/tutorial010_an.py new file mode 100644 index 000000000..8995f3f57 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_an.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + regex="^fixedquery$", + deprecated=True, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_an_py310.py b/docs_src/query_params_str_validations/tutorial010_an_py310.py new file mode 100644 index 000000000..cfa81926c --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_an_py310.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, + Query( + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + regex="^fixedquery$", + deprecated=True, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_an_py39.py b/docs_src/query_params_str_validations/tutorial010_an_py39.py new file mode 100644 index 000000000..220eaabf4 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_an_py39.py @@ -0,0 +1,26 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + regex="^fixedquery$", + deprecated=True, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial011_an.py b/docs_src/query_params_str_validations/tutorial011_an.py new file mode 100644 index 000000000..8ed699337 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_an.py @@ -0,0 +1,12 @@ +from typing import List, Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[List[str], None], Query()] = None): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_an_py310.py b/docs_src/query_params_str_validations/tutorial011_an_py310.py new file mode 100644 index 000000000..5dad4bfde --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_an_py310.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list[str] | None, Query()] = None): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_an_py39.py b/docs_src/query_params_str_validations/tutorial011_an_py39.py new file mode 100644 index 000000000..416e3990d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[list[str], None], Query()] = None): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial012_an.py b/docs_src/query_params_str_validations/tutorial012_an.py new file mode 100644 index 000000000..261af250a --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial012_an.py @@ -0,0 +1,12 @@ +from typing import List + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[List[str], Query()] = ["foo", "bar"]): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial012_an_py39.py b/docs_src/query_params_str_validations/tutorial012_an_py39.py new file mode 100644 index 000000000..9b5a9c2fb --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial012_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list[str], Query()] = ["foo", "bar"]): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial013_an.py b/docs_src/query_params_str_validations/tutorial013_an.py new file mode 100644 index 000000000..f12a25055 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial013_an.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list, Query()] = []): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial013_an_py39.py b/docs_src/query_params_str_validations/tutorial013_an_py39.py new file mode 100644 index 000000000..602734145 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial013_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list, Query()] = []): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial014_an.py b/docs_src/query_params_str_validations/tutorial014_an.py new file mode 100644 index 000000000..a9a9c4427 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_an.py @@ -0,0 +1,16 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Annotated[Union[str, None], Query(include_in_schema=False)] = None +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/query_params_str_validations/tutorial014_an_py310.py b/docs_src/query_params_str_validations/tutorial014_an_py310.py new file mode 100644 index 000000000..5fba54150 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_an_py310.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Annotated[str | None, Query(include_in_schema=False)] = None +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/query_params_str_validations/tutorial014_an_py39.py b/docs_src/query_params_str_validations/tutorial014_an_py39.py new file mode 100644 index 000000000..b07985210 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Annotated[Union[str, None], Query(include_in_schema=False)] = None +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/request_files/tutorial001_02_an.py b/docs_src/request_files/tutorial001_02_an.py new file mode 100644 index 000000000..5007fef15 --- /dev/null +++ b/docs_src/request_files/tutorial001_02_an.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import FastAPI, File, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[Union[bytes, None], File()] = None): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: Union[UploadFile, None] = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02_an_py310.py b/docs_src/request_files/tutorial001_02_an_py310.py new file mode 100644 index 000000000..9b80321b4 --- /dev/null +++ b/docs_src/request_files/tutorial001_02_an_py310.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes | None, File()] = None): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile | None = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02_an_py39.py b/docs_src/request_files/tutorial001_02_an_py39.py new file mode 100644 index 000000000..bb090ff6c --- /dev/null +++ b/docs_src/request_files/tutorial001_02_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[Union[bytes, None], File()] = None): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: Union[UploadFile, None] = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_03_an.py b/docs_src/request_files/tutorial001_03_an.py new file mode 100644 index 000000000..8a6b0a245 --- /dev/null +++ b/docs_src/request_files/tutorial001_03_an.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI, File, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File(description="A file read as bytes")]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file( + file: Annotated[UploadFile, File(description="A file read as UploadFile")], +): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_03_an_py39.py b/docs_src/request_files/tutorial001_03_an_py39.py new file mode 100644 index 000000000..93098a677 --- /dev/null +++ b/docs_src/request_files/tutorial001_03_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File(description="A file read as bytes")]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file( + file: Annotated[UploadFile, File(description="A file read as UploadFile")], +): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_an.py b/docs_src/request_files/tutorial001_an.py new file mode 100644 index 000000000..ca2f76d5c --- /dev/null +++ b/docs_src/request_files/tutorial001_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, File, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File()]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_an_py39.py b/docs_src/request_files/tutorial001_an_py39.py new file mode 100644 index 000000000..26a767221 --- /dev/null +++ b/docs_src/request_files/tutorial001_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File()]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial002_an.py b/docs_src/request_files/tutorial002_an.py new file mode 100644 index 000000000..eaa90da2b --- /dev/null +++ b/docs_src/request_files/tutorial002_an.py @@ -0,0 +1,34 @@ +from typing import List + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_files(files: Annotated[List[bytes], File()]): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files(files: List[UploadFile]): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial002_an_py39.py b/docs_src/request_files/tutorial002_an_py39.py new file mode 100644 index 000000000..db524ceab --- /dev/null +++ b/docs_src/request_files/tutorial002_an_py39.py @@ -0,0 +1,33 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files(files: Annotated[list[bytes], File()]): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files(files: list[UploadFile]): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial003_an.py b/docs_src/request_files/tutorial003_an.py new file mode 100644 index 000000000..2238e3c94 --- /dev/null +++ b/docs_src/request_files/tutorial003_an.py @@ -0,0 +1,40 @@ +from typing import List + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_files( + files: Annotated[List[bytes], File(description="Multiple files as bytes")], +): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files( + files: Annotated[ + List[UploadFile], File(description="Multiple files as UploadFile") + ], +): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial003_an_py39.py b/docs_src/request_files/tutorial003_an_py39.py new file mode 100644 index 000000000..5a8c5dab5 --- /dev/null +++ b/docs_src/request_files/tutorial003_an_py39.py @@ -0,0 +1,39 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files( + files: Annotated[list[bytes], File(description="Multiple files as bytes")], +): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files( + files: Annotated[ + list[UploadFile], File(description="Multiple files as UploadFile") + ], +): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_forms/tutorial001_an.py b/docs_src/request_forms/tutorial001_an.py new file mode 100644 index 000000000..677fbf2db --- /dev/null +++ b/docs_src/request_forms/tutorial001_an.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI, Form +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/login/") +async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]): + return {"username": username} diff --git a/docs_src/request_forms/tutorial001_an_py39.py b/docs_src/request_forms/tutorial001_an_py39.py new file mode 100644 index 000000000..8e9d2ea53 --- /dev/null +++ b/docs_src/request_forms/tutorial001_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import FastAPI, Form + +app = FastAPI() + + +@app.post("/login/") +async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]): + return {"username": username} diff --git a/docs_src/request_forms_and_files/tutorial001_an.py b/docs_src/request_forms_and_files/tutorial001_an.py new file mode 100644 index 000000000..0ea285ac8 --- /dev/null +++ b/docs_src/request_forms_and_files/tutorial001_an.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, File, Form, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file( + file: Annotated[bytes, File()], + fileb: Annotated[UploadFile, File()], + token: Annotated[str, Form()], +): + return { + "file_size": len(file), + "token": token, + "fileb_content_type": fileb.content_type, + } diff --git a/docs_src/request_forms_and_files/tutorial001_an_py39.py b/docs_src/request_forms_and_files/tutorial001_an_py39.py new file mode 100644 index 000000000..12cc43e50 --- /dev/null +++ b/docs_src/request_forms_and_files/tutorial001_an_py39.py @@ -0,0 +1,18 @@ +from typing import Annotated + +from fastapi import FastAPI, File, Form, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file( + file: Annotated[bytes, File()], + fileb: Annotated[UploadFile, File()], + token: Annotated[str, Form()], +): + return { + "file_size": len(file), + "token": token, + "fileb_content_type": fileb.content_type, + } diff --git a/docs_src/response_model/tutorial001.py b/docs_src/response_model/tutorial001.py index 0f6e03e5b..fd1c902a5 100644 --- a/docs_src/response_model/tutorial001.py +++ b/docs_src/response_model/tutorial001.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import Any, List, Union from fastapi import FastAPI from pydantic import BaseModel @@ -15,5 +15,13 @@ class Item(BaseModel): @app.post("/items/", response_model=Item) -async def create_item(item: Item): +async def create_item(item: Item) -> Any: return item + + +@app.get("/items/", response_model=List[Item]) +async def read_items() -> Any: + return [ + {"name": "Portal Gun", "price": 42.0}, + {"name": "Plumbus", "price": 32.0}, + ] diff --git a/docs_src/response_model/tutorial001_01.py b/docs_src/response_model/tutorial001_01.py new file mode 100644 index 000000000..98d30d540 --- /dev/null +++ b/docs_src/response_model/tutorial001_01.py @@ -0,0 +1,27 @@ +from typing import List, 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 + tags: List[str] = [] + + +@app.post("/items/") +async def create_item(item: Item) -> Item: + return item + + +@app.get("/items/") +async def read_items() -> List[Item]: + return [ + Item(name="Portal Gun", price=42.0), + Item(name="Plumbus", price=32.0), + ] diff --git a/docs_src/response_model/tutorial001_01_py310.py b/docs_src/response_model/tutorial001_01_py310.py new file mode 100644 index 000000000..7951c1076 --- /dev/null +++ b/docs_src/response_model/tutorial001_01_py310.py @@ -0,0 +1,25 @@ +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 + tags: list[str] = [] + + +@app.post("/items/") +async def create_item(item: Item) -> Item: + return item + + +@app.get("/items/") +async def read_items() -> list[Item]: + return [ + Item(name="Portal Gun", price=42.0), + Item(name="Plumbus", price=32.0), + ] diff --git a/docs_src/response_model/tutorial001_01_py39.py b/docs_src/response_model/tutorial001_01_py39.py new file mode 100644 index 000000000..16c78aa3f --- /dev/null +++ b/docs_src/response_model/tutorial001_01_py39.py @@ -0,0 +1,27 @@ +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 + tags: list[str] = [] + + +@app.post("/items/") +async def create_item(item: Item) -> Item: + return item + + +@app.get("/items/") +async def read_items() -> list[Item]: + return [ + Item(name="Portal Gun", price=42.0), + Item(name="Plumbus", price=32.0), + ] diff --git a/docs_src/response_model/tutorial001_py310.py b/docs_src/response_model/tutorial001_py310.py index 59efecde4..f8a2aa9fc 100644 --- a/docs_src/response_model/tutorial001_py310.py +++ b/docs_src/response_model/tutorial001_py310.py @@ -1,3 +1,5 @@ +from typing import Any + from fastapi import FastAPI from pydantic import BaseModel @@ -13,5 +15,13 @@ class Item(BaseModel): @app.post("/items/", response_model=Item) -async def create_item(item: Item): +async def create_item(item: Item) -> Any: return item + + +@app.get("/items/", response_model=list[Item]) +async def read_items() -> Any: + return [ + {"name": "Portal Gun", "price": 42.0}, + {"name": "Plumbus", "price": 32.0}, + ] diff --git a/docs_src/response_model/tutorial001_py39.py b/docs_src/response_model/tutorial001_py39.py index cdcca39d2..261e252d0 100644 --- a/docs_src/response_model/tutorial001_py39.py +++ b/docs_src/response_model/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Any, Union from fastapi import FastAPI from pydantic import BaseModel @@ -15,5 +15,13 @@ class Item(BaseModel): @app.post("/items/", response_model=Item) -async def create_item(item: Item): +async def create_item(item: Item) -> Any: return item + + +@app.get("/items/", response_model=list[Item]) +async def read_items() -> Any: + return [ + {"name": "Portal Gun", "price": 42.0}, + {"name": "Plumbus", "price": 32.0}, + ] diff --git a/docs_src/response_model/tutorial002.py b/docs_src/response_model/tutorial002.py index c68e8b138..a58668f9e 100644 --- a/docs_src/response_model/tutorial002.py +++ b/docs_src/response_model/tutorial002.py @@ -14,6 +14,6 @@ class UserIn(BaseModel): # Don't do this in production! -@app.post("/user/", response_model=UserIn) -async def create_user(user: UserIn): +@app.post("/user/") +async def create_user(user: UserIn) -> UserIn: return user diff --git a/docs_src/response_model/tutorial002_py310.py b/docs_src/response_model/tutorial002_py310.py index 29ab9c9d2..0a91a5967 100644 --- a/docs_src/response_model/tutorial002_py310.py +++ b/docs_src/response_model/tutorial002_py310.py @@ -12,6 +12,6 @@ class UserIn(BaseModel): # Don't do this in production! -@app.post("/user/", response_model=UserIn) -async def create_user(user: UserIn): +@app.post("/user/") +async def create_user(user: UserIn) -> UserIn: return user diff --git a/docs_src/response_model/tutorial003.py b/docs_src/response_model/tutorial003.py index 37e493dcb..c42dbc707 100644 --- a/docs_src/response_model/tutorial003.py +++ b/docs_src/response_model/tutorial003.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Any, Union from fastapi import FastAPI from pydantic import BaseModel, EmailStr @@ -20,5 +20,5 @@ class UserOut(BaseModel): @app.post("/user/", response_model=UserOut) -async def create_user(user: UserIn): +async def create_user(user: UserIn) -> Any: return user diff --git a/docs_src/response_model/tutorial003_01.py b/docs_src/response_model/tutorial003_01.py new file mode 100644 index 000000000..52694b551 --- /dev/null +++ b/docs_src/response_model/tutorial003_01.py @@ -0,0 +1,21 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr + +app = FastAPI() + + +class BaseUser(BaseModel): + username: str + email: EmailStr + full_name: Union[str, None] = None + + +class UserIn(BaseUser): + password: str + + +@app.post("/user/") +async def create_user(user: UserIn) -> BaseUser: + return user diff --git a/docs_src/response_model/tutorial003_01_py310.py b/docs_src/response_model/tutorial003_01_py310.py new file mode 100644 index 000000000..6ffddfd0a --- /dev/null +++ b/docs_src/response_model/tutorial003_01_py310.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr + +app = FastAPI() + + +class BaseUser(BaseModel): + username: str + email: EmailStr + full_name: str | None = None + + +class UserIn(BaseUser): + password: str + + +@app.post("/user/") +async def create_user(user: UserIn) -> BaseUser: + return user diff --git a/docs_src/response_model/tutorial003_02.py b/docs_src/response_model/tutorial003_02.py new file mode 100644 index 000000000..df6a09646 --- /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 000000000..0d4bd8de5 --- /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 000000000..b13a92692 --- /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 000000000..cee49b83e --- /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 000000000..0962061a6 --- /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 000000000..f1c0f8e12 --- /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/response_model/tutorial003_py310.py b/docs_src/response_model/tutorial003_py310.py index fc9693e3c..3703bf888 100644 --- a/docs_src/response_model/tutorial003_py310.py +++ b/docs_src/response_model/tutorial003_py310.py @@ -1,3 +1,5 @@ +from typing import Any + from fastapi import FastAPI from pydantic import BaseModel, EmailStr @@ -18,5 +20,5 @@ class UserOut(BaseModel): @app.post("/user/", response_model=UserOut) -async def create_user(user: UserIn): +async def create_user(user: UserIn) -> Any: return user diff --git a/docs_src/schema_extra_example/tutorial003_an.py b/docs_src/schema_extra_example/tutorial003_an.py new file mode 100644 index 000000000..1dec555a9 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_an.py @@ -0,0 +1,33 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Annotated[ + Item, + Body( + example={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial003_an_py310.py b/docs_src/schema_extra_example/tutorial003_an_py310.py new file mode 100644 index 000000000..9edaddfb8 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_an_py310.py @@ -0,0 +1,32 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Annotated[ + Item, + Body( + example={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial003_an_py39.py b/docs_src/schema_extra_example/tutorial003_an_py39.py new file mode 100644 index 000000000..fe08847d9 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_an_py39.py @@ -0,0 +1,32 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Annotated[ + Item, + Body( + example={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_an.py b/docs_src/schema_extra_example/tutorial004_an.py new file mode 100644 index 000000000..82c9a92ac --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_an.py @@ -0,0 +1,55 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_an_py310.py b/docs_src/schema_extra_example/tutorial004_an_py310.py new file mode 100644 index 000000000..01f1a486c --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_an_py310.py @@ -0,0 +1,54 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_an_py39.py b/docs_src/schema_extra_example/tutorial004_an_py39.py new file mode 100644 index 000000000..d50e8aa5f --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_an_py39.py @@ -0,0 +1,54 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/security/tutorial001_an.py b/docs_src/security/tutorial001_an.py new file mode 100644 index 000000000..dac915b7c --- /dev/null +++ b/docs_src/security/tutorial001_an.py @@ -0,0 +1,12 @@ +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from typing_extensions import Annotated + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +@app.get("/items/") +async def read_items(token: Annotated[str, Depends(oauth2_scheme)]): + return {"token": token} diff --git a/docs_src/security/tutorial001_an_py39.py b/docs_src/security/tutorial001_an_py39.py new file mode 100644 index 000000000..de110402e --- /dev/null +++ b/docs_src/security/tutorial001_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +@app.get("/items/") +async def read_items(token: Annotated[str, Depends(oauth2_scheme)]): + return {"token": token} diff --git a/docs_src/security/tutorial002_an.py b/docs_src/security/tutorial002_an.py new file mode 100644 index 000000000..291b3bf53 --- /dev/null +++ b/docs_src/security/tutorial002_an.py @@ -0,0 +1,33 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): + return current_user diff --git a/docs_src/security/tutorial002_an_py310.py b/docs_src/security/tutorial002_an_py310.py new file mode 100644 index 000000000..c7b761e45 --- /dev/null +++ b/docs_src/security/tutorial002_an_py310.py @@ -0,0 +1,32 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): + return current_user diff --git a/docs_src/security/tutorial002_an_py39.py b/docs_src/security/tutorial002_an_py39.py new file mode 100644 index 000000000..7ff1c470b --- /dev/null +++ b/docs_src/security/tutorial002_an_py39.py @@ -0,0 +1,32 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): + return current_user diff --git a/docs_src/security/tutorial003_an.py b/docs_src/security/tutorial003_an.py new file mode 100644 index 000000000..261cb4857 --- /dev/null +++ b/docs_src/security/tutorial003_an.py @@ -0,0 +1,95 @@ +from typing import Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel +from typing_extensions import Annotated + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user diff --git a/docs_src/security/tutorial003_an_py310.py b/docs_src/security/tutorial003_an_py310.py new file mode 100644 index 000000000..a03f4f8bf --- /dev/null +++ b/docs_src/security/tutorial003_an_py310.py @@ -0,0 +1,94 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user diff --git a/docs_src/security/tutorial003_an_py39.py b/docs_src/security/tutorial003_an_py39.py new file mode 100644 index 000000000..308dbe798 --- /dev/null +++ b/docs_src/security/tutorial003_an_py39.py @@ -0,0 +1,94 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user diff --git a/docs_src/security/tutorial004_an.py b/docs_src/security/tutorial004_an.py new file mode 100644 index 000000000..ca350343d --- /dev/null +++ b/docs_src/security/tutorial004_an.py @@ -0,0 +1,147 @@ +from datetime import datetime, timedelta +from typing import Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel +from typing_extensions import Annotated + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial004_an_py310.py b/docs_src/security/tutorial004_an_py310.py new file mode 100644 index 000000000..8bf5f3b71 --- /dev/null +++ b/docs_src/security/tutorial004_an_py310.py @@ -0,0 +1,146 @@ +from datetime import datetime, timedelta +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial004_an_py39.py b/docs_src/security/tutorial004_an_py39.py new file mode 100644 index 000000000..a634e23de --- /dev/null +++ b/docs_src/security/tutorial004_an_py39.py @@ -0,0 +1,146 @@ +from datetime import datetime, timedelta +from typing import Annotated, Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial005_an.py b/docs_src/security/tutorial005_an.py new file mode 100644 index 000000000..ec4fa1a07 --- /dev/null +++ b/docs_src/security/tutorial005_an.py @@ -0,0 +1,178 @@ +from datetime import datetime, timedelta +from typing import List, Union + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError +from typing_extensions import Annotated + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + scopes: List[str] = [] + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = "Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Security(get_current_user, scopes=["me"])] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): + return {"status": "ok"} diff --git a/docs_src/security/tutorial005_an_py310.py b/docs_src/security/tutorial005_an_py310.py new file mode 100644 index 000000000..45f3fc0bd --- /dev/null +++ b/docs_src/security/tutorial005_an_py310.py @@ -0,0 +1,177 @@ +from datetime import datetime, timedelta +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None + scopes: list[str] = [] + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = "Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Security(get_current_user, scopes=["me"])] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): + return {"status": "ok"} diff --git a/docs_src/security/tutorial005_an_py39.py b/docs_src/security/tutorial005_an_py39.py new file mode 100644 index 000000000..ecb5ed516 --- /dev/null +++ b/docs_src/security/tutorial005_an_py39.py @@ -0,0 +1,177 @@ +from datetime import datetime, timedelta +from typing import Annotated, List, Union + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + scopes: List[str] = [] + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = "Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Security(get_current_user, scopes=["me"])] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): + return {"status": "ok"} diff --git a/docs_src/security/tutorial006_an.py b/docs_src/security/tutorial006_an.py new file mode 100644 index 000000000..985e4b2ad --- /dev/null +++ b/docs_src/security/tutorial006_an.py @@ -0,0 +1,12 @@ +from fastapi import Depends, FastAPI +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from typing_extensions import Annotated + +app = FastAPI() + +security = HTTPBasic() + + +@app.get("/users/me") +def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): + return {"username": credentials.username, "password": credentials.password} diff --git a/docs_src/security/tutorial006_an_py39.py b/docs_src/security/tutorial006_an_py39.py new file mode 100644 index 000000000..03c696a4b --- /dev/null +++ b/docs_src/security/tutorial006_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.security import HTTPBasic, HTTPBasicCredentials + +app = FastAPI() + +security = HTTPBasic() + + +@app.get("/users/me") +def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): + return {"username": credentials.username, "password": credentials.password} diff --git a/docs_src/security/tutorial007_an.py b/docs_src/security/tutorial007_an.py new file mode 100644 index 000000000..5fb7c8e57 --- /dev/null +++ b/docs_src/security/tutorial007_an.py @@ -0,0 +1,36 @@ +import secrets + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from typing_extensions import Annotated + +app = FastAPI() + +security = HTTPBasic() + + +def get_current_username( + credentials: Annotated[HTTPBasicCredentials, Depends(security)] +): + current_username_bytes = credentials.username.encode("utf8") + correct_username_bytes = b"stanleyjobson" + is_correct_username = secrets.compare_digest( + current_username_bytes, correct_username_bytes + ) + current_password_bytes = credentials.password.encode("utf8") + correct_password_bytes = b"swordfish" + is_correct_password = secrets.compare_digest( + current_password_bytes, correct_password_bytes + ) + if not (is_correct_username and is_correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials.username + + +@app.get("/users/me") +def read_current_user(username: Annotated[str, Depends(get_current_username)]): + return {"username": username} diff --git a/docs_src/security/tutorial007_an_py39.py b/docs_src/security/tutorial007_an_py39.py new file mode 100644 index 000000000..17177dabf --- /dev/null +++ b/docs_src/security/tutorial007_an_py39.py @@ -0,0 +1,36 @@ +import secrets +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPBasic, HTTPBasicCredentials + +app = FastAPI() + +security = HTTPBasic() + + +def get_current_username( + credentials: Annotated[HTTPBasicCredentials, Depends(security)] +): + current_username_bytes = credentials.username.encode("utf8") + correct_username_bytes = b"stanleyjobson" + is_correct_username = secrets.compare_digest( + current_username_bytes, correct_username_bytes + ) + current_password_bytes = credentials.password.encode("utf8") + correct_password_bytes = b"swordfish" + is_correct_password = secrets.compare_digest( + current_password_bytes, correct_password_bytes + ) + if not (is_correct_username and is_correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials.username + + +@app.get("/users/me") +def read_current_user(username: Annotated[str, Depends(get_current_username)]): + return {"username": username} diff --git a/docs_src/settings/app02_an/__init__.py b/docs_src/settings/app02_an/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/app02_an/config.py b/docs_src/settings/app02_an/config.py new file mode 100644 index 000000000..9a7829135 --- /dev/null +++ b/docs_src/settings/app02_an/config.py @@ -0,0 +1,7 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 diff --git a/docs_src/settings/app02_an/main.py b/docs_src/settings/app02_an/main.py new file mode 100644 index 000000000..cb679202d --- /dev/null +++ b/docs_src/settings/app02_an/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +from .config import Settings + +app = FastAPI() + + +@lru_cache() +def get_settings(): + return Settings() + + +@app.get("/info") +async def info(settings: Annotated[Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/app02_an/test_main.py b/docs_src/settings/app02_an/test_main.py new file mode 100644 index 000000000..7a04d7e8e --- /dev/null +++ b/docs_src/settings/app02_an/test_main.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from .config import Settings +from .main import app, get_settings + +client = TestClient(app) + + +def get_settings_override(): + return Settings(admin_email="testing_admin@example.com") + + +app.dependency_overrides[get_settings] = get_settings_override + + +def test_app(): + response = client.get("/info") + data = response.json() + assert data == { + "app_name": "Awesome API", + "admin_email": "testing_admin@example.com", + "items_per_user": 50, + } diff --git a/docs_src/settings/app02_an_py39/__init__.py b/docs_src/settings/app02_an_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/app02_an_py39/config.py b/docs_src/settings/app02_an_py39/config.py new file mode 100644 index 000000000..9a7829135 --- /dev/null +++ b/docs_src/settings/app02_an_py39/config.py @@ -0,0 +1,7 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 diff --git a/docs_src/settings/app02_an_py39/main.py b/docs_src/settings/app02_an_py39/main.py new file mode 100644 index 000000000..61be74fcb --- /dev/null +++ b/docs_src/settings/app02_an_py39/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache +from typing import Annotated + +from fastapi import Depends, FastAPI + +from .config import Settings + +app = FastAPI() + + +@lru_cache() +def get_settings(): + return Settings() + + +@app.get("/info") +async def info(settings: Annotated[Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/app02_an_py39/test_main.py b/docs_src/settings/app02_an_py39/test_main.py new file mode 100644 index 000000000..7a04d7e8e --- /dev/null +++ b/docs_src/settings/app02_an_py39/test_main.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from .config import Settings +from .main import app, get_settings + +client = TestClient(app) + + +def get_settings_override(): + return Settings(admin_email="testing_admin@example.com") + + +app.dependency_overrides[get_settings] = get_settings_override + + +def test_app(): + response = client.get("/info") + data = response.json() + assert data == { + "app_name": "Awesome API", + "admin_email": "testing_admin@example.com", + "items_per_user": 50, + } diff --git a/docs_src/settings/app03_an/__init__.py b/docs_src/settings/app03_an/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/app03_an/config.py b/docs_src/settings/app03_an/config.py new file mode 100644 index 000000000..e1c3ee300 --- /dev/null +++ b/docs_src/settings/app03_an/config.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" diff --git a/docs_src/settings/app03_an/main.py b/docs_src/settings/app03_an/main.py new file mode 100644 index 000000000..c33b98f47 --- /dev/null +++ b/docs_src/settings/app03_an/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache +from typing import Annotated + +from fastapi import Depends, FastAPI + +from . import config + +app = FastAPI() + + +@lru_cache() +def get_settings(): + return config.Settings() + + +@app.get("/info") +async def info(settings: Annotated[config.Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/app03_an_py39/__init__.py b/docs_src/settings/app03_an_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/app03_an_py39/config.py b/docs_src/settings/app03_an_py39/config.py new file mode 100644 index 000000000..e1c3ee300 --- /dev/null +++ b/docs_src/settings/app03_an_py39/config.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" diff --git a/docs_src/settings/app03_an_py39/main.py b/docs_src/settings/app03_an_py39/main.py new file mode 100644 index 000000000..b89c6b6cf --- /dev/null +++ b/docs_src/settings/app03_an_py39/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +from . import config + +app = FastAPI() + + +@lru_cache() +def get_settings(): + return config.Settings() + + +@app.get("/info") +async def info(settings: Annotated[config.Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/websockets/tutorial002_an.py b/docs_src/websockets/tutorial002_an.py new file mode 100644 index 000000000..c838fbd30 --- /dev/null +++ b/docs_src/websockets/tutorial002_an.py @@ -0,0 +1,93 @@ +from typing import Union + +from fastapi import ( + Cookie, + Depends, + FastAPI, + Query, + WebSocket, + WebSocketException, + status, +) +from fastapi.responses import HTMLResponse +from typing_extensions import Annotated + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

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

WebSocket Chat

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

WebSocket Chat

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

WebSocket Chat

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

WebSocket Chat

+

Your ID:

+
+ + +
+ + + + +""" + + +class ConnectionManager: + def __init__(self): + self.active_connections: list[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def send_personal_message(self, message: str, websocket: WebSocket): + await websocket.send_text(message) + + async def broadcast(self, message: str): + for connection in self.active_connections: + await connection.send_text(message) + + +manager = ConnectionManager() + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +@app.websocket("/ws/{client_id}") +async def websocket_endpoint(websocket: WebSocket, client_id: int): + await manager.connect(websocket) + try: + while True: + data = await websocket.receive_text() + await manager.send_personal_message(f"You wrote: {data}", websocket) + await manager.broadcast(f"Client #{client_id} says: {data}") + except WebSocketDisconnect: + manager.disconnect(websocket) + await manager.broadcast(f"Client #{client_id} left the chat") diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 037d9804b..f06bb6454 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.88.0" +__version__ = "0.95.0" from starlette import status as status diff --git a/fastapi/applications.py b/fastapi/applications.py index 61d4582d2..8b3a74d3c 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -9,6 +9,7 @@ from typing import ( Optional, Sequence, Type, + TypeVar, Union, ) @@ -35,17 +36,20 @@ from starlette.applications import Starlette from starlette.datastructures import State from starlette.exceptions import HTTPException from starlette.middleware import Middleware +from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.errors import ServerErrorMiddleware from starlette.middleware.exceptions import ExceptionMiddleware 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 + +AppType = TypeVar("AppType", bound="FastAPI") class FastAPI(Starlette): def __init__( - self, + self: AppType, *, debug: bool = False, routes: Optional[List[BaseRoute]] = None, @@ -70,6 +74,7 @@ class FastAPI(Starlette): ] = None, on_startup: Optional[Sequence[Callable[[], Any]]] = None, on_shutdown: Optional[Sequence[Callable[[], Any]]] = None, + lifespan: Optional[Lifespan[AppType]] = None, terms_of_service: Optional[str] = None, contact: Optional[Dict[str, Union[str, Any]]] = None, license_info: Optional[Dict[str, Union[str, Any]]] = None, @@ -86,7 +91,7 @@ class FastAPI(Starlette): ), **extra: Any, ) -> None: - self._debug: bool = debug + self.debug = debug self.title = title self.description = description self.version = version @@ -124,6 +129,7 @@ class FastAPI(Starlette): dependency_overrides_provider=self, on_startup=on_startup, on_shutdown=on_shutdown, + lifespan=lifespan, default_response_class=default_response_class, dependencies=dependencies, callbacks=callbacks, @@ -143,7 +149,7 @@ class FastAPI(Starlette): 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: @@ -274,7 +280,7 @@ class FastAPI(Starlette): path: str, endpoint: Callable[..., Coroutine[Any, Any, Response]], *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -332,7 +338,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -435,7 +441,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -490,7 +496,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -545,7 +551,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -600,7 +606,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -655,7 +661,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -710,7 +716,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -765,7 +771,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -820,7 +826,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -870,3 +876,35 @@ 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 + + def on_event( + self, event_type: str + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + return self.router.on_event(event_type) + + def middleware( + self, middleware_type: str + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_middleware(BaseHTTPMiddleware, dispatch=func) + return func + + return decorator + + def exception_handler( + self, exc_class_or_status_code: Union[int, Type[Exception]] + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_exception_handler(exc_class_or_status_code, func) + return func + + return decorator diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 4c817d5d0..c581348c9 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -48,7 +48,7 @@ from pydantic.fields import ( Undefined, ) from pydantic.schema import get_annotation_from_field_info -from pydantic.typing import evaluate_forwardref +from pydantic.typing import evaluate_forwardref, get_args, get_origin from pydantic.utils import lenient_issubclass from starlette.background import BackgroundTasks from starlette.concurrency import run_in_threadpool @@ -56,6 +56,7 @@ from starlette.datastructures import FormData, Headers, QueryParams, UploadFile from starlette.requests import HTTPConnection, Request from starlette.responses import Response from starlette.websockets import WebSocket +from typing_extensions import Annotated sequence_shapes = { SHAPE_LIST, @@ -112,18 +113,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, ) @@ -253,7 +254,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: name=param.name, kind=param.kind, default=param.default, - annotation=get_typed_annotation(param, globalns), + annotation=get_typed_annotation(param.annotation, globalns), ) for param in signature.parameters.values() ] @@ -261,14 +262,24 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: return typed_signature -def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any: - annotation = param.annotation +def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any: if isinstance(annotation, str): annotation = ForwardRef(annotation) annotation = evaluate_forwardref(annotation, globalns, globalns) return annotation +def get_typed_return_annotation(call: Callable[..., Any]) -> Any: + signature = inspect.signature(call) + annotation = signature.return_annotation + + if annotation is inspect.Signature.empty: + return None + + globalns = getattr(call, "__globals__", {}) + return get_typed_annotation(annotation, globalns) + + def get_dependant( *, path: str, @@ -288,122 +299,199 @@ 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, BackgroundTasks): + 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 + used_default_field_info = False + depends = None + type_annotation: Any = Any + if ( + annotation is not inspect.Signature.empty + and get_origin(annotation) is Annotated # type: ignore[comparison-overlap] + ): + 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)) + ] + assert ( + len(fastapi_annotations) <= 1 + ), f"Cannot specify multiple `Annotated` FastAPI arguments for {param_name!r}" + fastapi_annotation = next(iter(fastapi_annotations), None) + if isinstance(fastapi_annotation, FieldInfo): + field_info = fastapi_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 + elif annotation is not inspect.Signature.empty: + type_annotation = 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 depends is not None and depends.dependency is None: + depends.dependency = type_annotation + + if lenient_issubclass( + type_annotation, + (Request, WebSocket, HTTPConnection, Response, BackgroundTasks, 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() + else: + field_info = params.Query(default=default_value) + used_default_field_info = True + + 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 + annotation = get_annotation_from_field_info( + annotation if annotation is not inspect.Signature.empty else Any, + 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=field_info.default, + alias=alias, + required=field_info.default in (Required, Undefined), + field_info=field_info, + ) + if used_default_field_info: + if lenient_issubclass(field.type_, UploadFile): + field.field_info = params.File(field_info.default) + elif not is_scalar_field(field=field): + field.field_info = params.Body(field_info.default) + + return type_annotation, depends, field + - return 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: @@ -686,7 +774,7 @@ async def request_body_to_args( 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: diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 1932ef065..75f054e9d 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -5,7 +5,7 @@ from pydantic.fields import Undefined def Path( # noqa: N802 - default: Any = Undefined, + default: Any = ..., *, alias: Optional[str] = None, title: Optional[str] = None, diff --git a/fastapi/params.py b/fastapi/params.py index 5395b98a3..16c5c309a 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -62,7 +62,7 @@ class Path(Param): def __init__( self, - default: Any = Undefined, + default: Any = ..., *, alias: Optional[str] = None, title: Optional[str] = None, @@ -80,9 +80,10 @@ class Path(Param): include_in_schema: bool = True, **extra: Any, ): + assert default is ..., "Path parameters cannot have a default value" self.in_ = self.in_ super().__init__( - default=..., + default=default, alias=alias, title=title, description=description, @@ -279,7 +280,7 @@ class Body(FieldInfo): class Form(Body): def __init__( self, - default: Any, + default: Any = Undefined, *, media_type: str = "application/x-www-form-urlencoded", alias: Optional[str] = None, @@ -319,7 +320,7 @@ class Form(Body): class File(Form): def __init__( self, - default: Any, + default: Any = Undefined, *, media_type: str = "multipart/form-data", alias: Optional[str] = None, diff --git a/fastapi/routing.py b/fastapi/routing.py index 9a7d88efc..06c71bffa 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -26,6 +26,7 @@ from fastapi.dependencies.utils import ( get_body_field, get_dependant, get_parameterless_sub_dependant, + get_typed_return_annotation, solve_dependencies, ) from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder @@ -41,6 +42,7 @@ from fastapi.utils import ( from pydantic import BaseModel from pydantic.error_wrappers import ErrorWrapper, ValidationError from pydantic.fields import ModelField, Undefined +from pydantic.utils import lenient_issubclass from starlette import routing from starlette.concurrency import run_in_threadpool from starlette.exceptions import HTTPException @@ -55,7 +57,7 @@ from starlette.routing import ( websocket_session, ) from starlette.status import WS_1008_POLICY_VIOLATION -from starlette.types import ASGIApp, Scope +from starlette.types import ASGIApp, Lifespan, Scope from starlette.websockets import WebSocket @@ -323,7 +325,7 @@ class APIRoute(routing.Route): path: str, endpoint: Callable[..., Any], *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -354,6 +356,12 @@ class APIRoute(routing.Route): ) -> None: self.path = path self.endpoint = endpoint + if isinstance(response_model, DefaultPlaceholder): + 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 @@ -484,6 +492,9 @@ class APIRouter(routing.Router): route_class: Type[APIRoute] = APIRoute, on_startup: Optional[Sequence[Callable[[], Any]]] = None, on_shutdown: Optional[Sequence[Callable[[], Any]]] = 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: Optional[Lifespan[Any]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, generate_unique_id_function: Callable[[APIRoute], str] = Default( @@ -496,6 +507,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 '/'" @@ -514,12 +526,31 @@ 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, endpoint: Callable[..., Any], *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -600,7 +631,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -678,6 +709,15 @@ class APIRouter(routing.Router): return decorator + def websocket_route( + self, path: str, name: Union[str, None] = None + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_websocket_route(path, func, name=name) + return func + + return decorator + def include_router( self, router: "APIRouter", @@ -795,7 +835,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -851,7 +891,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -907,7 +947,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -963,7 +1003,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1019,7 +1059,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1075,7 +1115,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1131,7 +1171,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1187,7 +1227,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1212,7 +1252,6 @@ class APIRouter(routing.Router): generate_unique_id ), ) -> Callable[[DecoratedCallable], DecoratedCallable]: - return self.api_route( path=path, response_model=response_model, @@ -1239,3 +1278,12 @@ class APIRouter(routing.Router): openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, ) + + def on_event( + self, event_type: str + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + 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 24ddbf482..61730187a 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -18,7 +18,7 @@ class APIKeyQuery(APIKeyBase): name: str, scheme_name: Optional[str] = None, description: Optional[str] = None, - auto_error: bool = True + auto_error: bool = True, ): self.model: APIKey = APIKey( **{"in": APIKeyIn.query}, name=name, description=description @@ -45,7 +45,7 @@ class APIKeyHeader(APIKeyBase): name: str, scheme_name: Optional[str] = None, description: Optional[str] = None, - auto_error: bool = True + auto_error: bool = True, ): self.model: APIKey = APIKey( **{"in": APIKeyIn.header}, name=name, description=description @@ -72,7 +72,7 @@ class APIKeyCookie(APIKeyBase): name: str, scheme_name: Optional[str] = None, description: Optional[str] = None, - auto_error: bool = True + auto_error: bool = True, ): self.model: APIKey = APIKey( **{"in": APIKeyIn.cookie}, name=name, description=description diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index eb6b4277c..dc75dc9fe 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -119,7 +119,7 @@ class OAuth2(SecurityBase): flows: Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]] = OAuthFlowsModel(), scheme_name: Optional[str] = None, description: Optional[str] = None, - auto_error: bool = True + auto_error: bool = True, ): self.model = OAuth2Model(flows=flows, description=description) self.scheme_name = scheme_name or self.__class__.__name__ diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index 393614f7c..4e65f1f6c 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -14,7 +14,7 @@ class OpenIdConnect(SecurityBase): openIdConnectUrl: str, scheme_name: Optional[str] = None, description: Optional[str] = None, - auto_error: bool = True + auto_error: bool = True, ): self.model = OpenIdConnectModel( openIdConnectUrl=openIdConnectUrl, description=description diff --git a/fastapi/utils.py b/fastapi/utils.py index b15f6a2cf..d8be53c57 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -1,4 +1,3 @@ -import functools import re import warnings from dataclasses import is_dataclass @@ -73,22 +72,26 @@ def create_response_field( 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, - ) - try: - return response_field(field_info=field_info) + return ModelField( + name=name, + type_=type_, + class_validators=class_validators, + default=default, + required=required, + model_config=model_config, + alias=alias, + field_info=field_info, + ) except RuntimeError: 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 diff --git a/pyproject.toml b/pyproject.toml index be3080ae8..6aa095a64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,8 @@ 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", @@ -39,7 +41,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette==0.22.0", + "starlette>=0.26.1,<0.27.0", "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] dynamic = ["version"] @@ -51,21 +53,21 @@ Documentation = "https://fastapi.tiangolo.com/" [project.optional-dependencies] test = [ "pytest >=7.1.3,<8.0.0", - "coverage[toml] >= 6.5.0,<7.0", + "coverage[toml] >= 6.5.0,< 8.0", "mypy ==0.982", "ruff ==0.0.138", - "black == 22.10.0", + "black == 23.1.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.41", + "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", + "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", @@ -73,7 +75,7 @@ test = [ "passlib[bcrypt] >=1.7.2,<2.0.0", # types - "types-ujson ==5.5.0", + "types-ujson ==5.7.0.1", "types-orjson ==3.6.2", ] doc = [ @@ -81,14 +83,13 @@ doc = [ "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-cli >=0.0.13,<0.0.14", "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.19.0", + "uvicorn[standard] >=0.12.0,<0.21.0", "pre-commit >=2.17.0,<3.0.0", ] all = [ @@ -155,6 +156,10 @@ source = [ "fastapi" ] context = '${CONTEXT}' +omit = [ + "docs_src/response_model/tutorial003_04.py", + "docs_src/response_model/tutorial003_04_py310.py", +] [tool.ruff] select = [ @@ -181,6 +186,12 @@ ignore = [ "docs_src/dataclasses/tutorial003.py" = ["I001"] "docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"] "docs_src/custom_request_and_route/tutorial002.py" = ["B904"] +"docs_src/dependencies/tutorial008_an.py" = ["F821"] +"docs_src/dependencies/tutorial008_an_py39.py" = ["F821"] +"docs_src/query_params_str_validations/tutorial012_an.py" = ["B006"] +"docs_src/query_params_str_validations/tutorial012_an_py39.py" = ["B006"] +"docs_src/query_params_str_validations/tutorial013_an.py" = ["B006"] +"docs_src/query_params_str_validations/tutorial013_an_py39.py" = ["B006"] [tool.ruff.isort] known-third-party = ["fastapi", "pydantic", "starlette"] diff --git a/scripts/zip-docs.sh b/scripts/zip-docs.sh index 69315f5dd..47c3b0977 100644 --- a/scripts/zip-docs.sh +++ b/scripts/zip-docs.sh @@ -1,4 +1,4 @@ -#! /usr/bin/env bash +#!/usr/bin/env bash set -x set -e diff --git a/tests/main.py b/tests/main.py index fce665704..15760c039 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_ambiguous_params.py b/tests/test_ambiguous_params.py new file mode 100644 index 000000000..42bcc27a1 --- /dev/null +++ b/tests/test_ambiguous_params.py @@ -0,0 +1,66 @@ +import pytest +from fastapi import Depends, FastAPI, Path +from fastapi.param_functions import Query +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_no_multiple_annotations(): + async def dep(): + pass # pragma: nocover + + with pytest.raises( + AssertionError, + match="Cannot specify multiple `Annotated` FastAPI arguments for 'foo'", + ): + + @app.get("/") + async def get(foo: Annotated[int, Query(min_length=1), Query()]): + pass # pragma: nocover + + 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 diff --git a/tests/test_annotated.py b/tests/test_annotated.py new file mode 100644 index 000000000..556019897 --- /dev/null +++ b/tests/test_annotated.py @@ -0,0 +1,226 @@ +import pytest +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +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) + +openapi_schema = { + "openapi": "3.0.2", + "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"}, + }, + }, + } + }, +} +foo_is_missing = { + "detail": [ + { + "loc": ["query", "foo"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} +foo_is_short = { + "detail": [ + { + "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), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_application.py b/tests/test_application.py index b7d72f9ad..a4f13e12d 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -225,36 +225,6 @@ openapi_schema = { ], } }, - "/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": { diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py index d721257d7..d8dca1ea4 100644 --- a/tests/test_params_repr.py +++ b/tests/test_params_repr.py @@ -19,8 +19,9 @@ def test_param_repr(params): assert repr(Param(params)) == "Param(" + str(params) + ")" -def test_path_repr(params): - assert repr(Path(params)) == "Path(Ellipsis)" +def test_path_repr(): + assert repr(Path()) == "Path(Ellipsis)" + assert repr(Path(...)) == "Path(Ellipsis)" def test_query_repr(params): diff --git a/tests/test_path.py b/tests/test_path.py index d1a58bc66..03b93623a 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -193,7 +193,6 @@ response_less_than_equal_3 = { ("/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"), diff --git a/tests/test_reponse_set_reponse_code_empty.py b/tests/test_reponse_set_reponse_code_empty.py index 094d54a84..50ec753a0 100644 --- a/tests/test_reponse_set_reponse_code_empty.py +++ b/tests/test_reponse_set_reponse_code_empty.py @@ -9,6 +9,7 @@ app = FastAPI() @app.delete( "/{id}", status_code=204, + response_model=None, ) async def delete_deployment( id: int, diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py new file mode 100644 index 000000000..e45364149 --- /dev/null +++ b/tests/test_response_model_as_return_annotation.py @@ -0,0 +1,1111 @@ +from typing import List, Union + +import pytest +from fastapi import FastAPI +from fastapi.exceptions import FastAPIError +from fastapi.responses import JSONResponse, Response +from fastapi.testclient import TestClient +from pydantic import BaseModel, ValidationError + + +class BaseUser(BaseModel): + name: str + + +class User(BaseUser): + surname: str + + +class DBUser(User): + password_hash: str + + +class Item(BaseModel): + name: str + price: float + + +app = FastAPI() + + +@app.get("/no_response_model-no_annotation-return_model") +def no_response_model_no_annotation_return_model(): + return User(name="John", surname="Doe") + + +@app.get("/no_response_model-no_annotation-return_dict") +def no_response_model_no_annotation_return_dict(): + return {"name": "John", "surname": "Doe"} + + +@app.get("/response_model-no_annotation-return_same_model", response_model=User) +def response_model_no_annotation_return_same_model(): + return User(name="John", surname="Doe") + + +@app.get("/response_model-no_annotation-return_exact_dict", response_model=User) +def response_model_no_annotation_return_exact_dict(): + return {"name": "John", "surname": "Doe"} + + +@app.get("/response_model-no_annotation-return_invalid_dict", response_model=User) +def response_model_no_annotation_return_invalid_dict(): + return {"name": "John"} + + +@app.get("/response_model-no_annotation-return_invalid_model", response_model=User) +def response_model_no_annotation_return_invalid_model(): + return Item(name="Foo", price=42.0) + + +@app.get( + "/response_model-no_annotation-return_dict_with_extra_data", response_model=User +) +def response_model_no_annotation_return_dict_with_extra_data(): + return {"name": "John", "surname": "Doe", "password_hash": "secret"} + + +@app.get( + "/response_model-no_annotation-return_submodel_with_extra_data", response_model=User +) +def response_model_no_annotation_return_submodel_with_extra_data(): + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get("/no_response_model-annotation-return_same_model") +def no_response_model_annotation_return_same_model() -> User: + return User(name="John", surname="Doe") + + +@app.get("/no_response_model-annotation-return_exact_dict") +def no_response_model_annotation_return_exact_dict() -> User: + return {"name": "John", "surname": "Doe"} + + +@app.get("/no_response_model-annotation-return_invalid_dict") +def no_response_model_annotation_return_invalid_dict() -> User: + return {"name": "John"} + + +@app.get("/no_response_model-annotation-return_invalid_model") +def no_response_model_annotation_return_invalid_model() -> User: + return Item(name="Foo", price=42.0) + + +@app.get("/no_response_model-annotation-return_dict_with_extra_data") +def no_response_model_annotation_return_dict_with_extra_data() -> User: + return {"name": "John", "surname": "Doe", "password_hash": "secret"} + + +@app.get("/no_response_model-annotation-return_submodel_with_extra_data") +def no_response_model_annotation_return_submodel_with_extra_data() -> User: + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get("/response_model_none-annotation-return_same_model", response_model=None) +def response_model_none_annotation_return_same_model() -> User: + return User(name="John", surname="Doe") + + +@app.get("/response_model_none-annotation-return_exact_dict", response_model=None) +def response_model_none_annotation_return_exact_dict() -> User: + return {"name": "John", "surname": "Doe"} + + +@app.get("/response_model_none-annotation-return_invalid_dict", response_model=None) +def response_model_none_annotation_return_invalid_dict() -> User: + return {"name": "John"} + + +@app.get("/response_model_none-annotation-return_invalid_model", response_model=None) +def response_model_none_annotation_return_invalid_model() -> User: + return Item(name="Foo", price=42.0) + + +@app.get( + "/response_model_none-annotation-return_dict_with_extra_data", response_model=None +) +def response_model_none_annotation_return_dict_with_extra_data() -> User: + return {"name": "John", "surname": "Doe", "password_hash": "secret"} + + +@app.get( + "/response_model_none-annotation-return_submodel_with_extra_data", + response_model=None, +) +def response_model_none_annotation_return_submodel_with_extra_data() -> User: + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get( + "/response_model_model1-annotation_model2-return_same_model", response_model=User +) +def response_model_model1_annotation_model2_return_same_model() -> Item: + return User(name="John", surname="Doe") + + +@app.get( + "/response_model_model1-annotation_model2-return_exact_dict", response_model=User +) +def response_model_model1_annotation_model2_return_exact_dict() -> Item: + return {"name": "John", "surname": "Doe"} + + +@app.get( + "/response_model_model1-annotation_model2-return_invalid_dict", response_model=User +) +def response_model_model1_annotation_model2_return_invalid_dict() -> Item: + return {"name": "John"} + + +@app.get( + "/response_model_model1-annotation_model2-return_invalid_model", response_model=User +) +def response_model_model1_annotation_model2_return_invalid_model() -> Item: + return Item(name="Foo", price=42.0) + + +@app.get( + "/response_model_model1-annotation_model2-return_dict_with_extra_data", + response_model=User, +) +def response_model_model1_annotation_model2_return_dict_with_extra_data() -> Item: + return {"name": "John", "surname": "Doe", "password_hash": "secret"} + + +@app.get( + "/response_model_model1-annotation_model2-return_submodel_with_extra_data", + response_model=User, +) +def response_model_model1_annotation_model2_return_submodel_with_extra_data() -> Item: + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get( + "/response_model_filtering_model-annotation_submodel-return_submodel", + response_model=User, +) +def response_model_filtering_model_annotation_submodel_return_submodel() -> DBUser: + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get("/response_model_list_of_model-no_annotation", response_model=List[User]) +def response_model_list_of_model_no_annotation(): + return [ + DBUser(name="John", surname="Doe", password_hash="secret"), + DBUser(name="Jane", surname="Does", password_hash="secret2"), + ] + + +@app.get("/no_response_model-annotation_list_of_model") +def no_response_model_annotation_list_of_model() -> List[User]: + return [ + DBUser(name="John", surname="Doe", password_hash="secret"), + DBUser(name="Jane", surname="Does", password_hash="secret2"), + ] + + +@app.get("/no_response_model-annotation_forward_ref_list_of_model") +def no_response_model_annotation_forward_ref_list_of_model() -> "List[User]": + return [ + DBUser(name="John", surname="Doe", password_hash="secret"), + DBUser(name="Jane", surname="Does", password_hash="secret2"), + ] + + +@app.get( + "/response_model_union-no_annotation-return_model1", + response_model=Union[User, Item], +) +def response_model_union_no_annotation_return_model1(): + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get( + "/response_model_union-no_annotation-return_model2", + response_model=Union[User, Item], +) +def response_model_union_no_annotation_return_model2(): + return Item(name="Foo", price=42.0) + + +@app.get("/no_response_model-annotation_union-return_model1") +def no_response_model_annotation_union_return_model1() -> Union[User, Item]: + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get("/no_response_model-annotation_union-return_model2") +def no_response_model_annotation_union_return_model2() -> Union[User, Item]: + return Item(name="Foo", price=42.0) + + +@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"}) + + +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"}, + ], + } + } + }, + } + }, + } + }, + "/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"}, + }, + }, + } + }, +} + + +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 + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_no_response_model_no_annotation_return_dict(): + response = client.get("/no_response_model-no_annotation-return_dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_no_annotation_return_same_model(): + response = client.get("/response_model-no_annotation-return_same_model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_no_annotation_return_exact_dict(): + response = client.get("/response_model-no_annotation-return_exact_dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_no_annotation_return_invalid_dict(): + with pytest.raises(ValidationError): + client.get("/response_model-no_annotation-return_invalid_dict") + + +def test_response_model_no_annotation_return_invalid_model(): + with pytest.raises(ValidationError): + client.get("/response_model-no_annotation-return_invalid_model") + + +def test_response_model_no_annotation_return_dict_with_extra_data(): + response = client.get("/response_model-no_annotation-return_dict_with_extra_data") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_no_annotation_return_submodel_with_extra_data(): + response = client.get( + "/response_model-no_annotation-return_submodel_with_extra_data" + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_no_response_model_annotation_return_same_model(): + response = client.get("/no_response_model-annotation-return_same_model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_no_response_model_annotation_return_exact_dict(): + response = client.get("/no_response_model-annotation-return_exact_dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_no_response_model_annotation_return_invalid_dict(): + with pytest.raises(ValidationError): + client.get("/no_response_model-annotation-return_invalid_dict") + + +def test_no_response_model_annotation_return_invalid_model(): + with pytest.raises(ValidationError): + client.get("/no_response_model-annotation-return_invalid_model") + + +def test_no_response_model_annotation_return_dict_with_extra_data(): + response = client.get("/no_response_model-annotation-return_dict_with_extra_data") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_no_response_model_annotation_return_submodel_with_extra_data(): + response = client.get( + "/no_response_model-annotation-return_submodel_with_extra_data" + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_none_annotation_return_same_model(): + response = client.get("/response_model_none-annotation-return_same_model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_none_annotation_return_exact_dict(): + response = client.get("/response_model_none-annotation-return_exact_dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_none_annotation_return_invalid_dict(): + response = client.get("/response_model_none-annotation-return_invalid_dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John"} + + +def test_response_model_none_annotation_return_invalid_model(): + response = client.get("/response_model_none-annotation-return_invalid_model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "price": 42.0} + + +def test_response_model_none_annotation_return_dict_with_extra_data(): + response = client.get("/response_model_none-annotation-return_dict_with_extra_data") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "John", + "surname": "Doe", + "password_hash": "secret", + } + + +def test_response_model_none_annotation_return_submodel_with_extra_data(): + response = client.get( + "/response_model_none-annotation-return_submodel_with_extra_data" + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "John", + "surname": "Doe", + "password_hash": "secret", + } + + +def test_response_model_model1_annotation_model2_return_same_model(): + response = client.get("/response_model_model1-annotation_model2-return_same_model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_model1_annotation_model2_return_exact_dict(): + response = client.get("/response_model_model1-annotation_model2-return_exact_dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_model1_annotation_model2_return_invalid_dict(): + with pytest.raises(ValidationError): + client.get("/response_model_model1-annotation_model2-return_invalid_dict") + + +def test_response_model_model1_annotation_model2_return_invalid_model(): + with pytest.raises(ValidationError): + client.get("/response_model_model1-annotation_model2-return_invalid_model") + + +def test_response_model_model1_annotation_model2_return_dict_with_extra_data(): + response = client.get( + "/response_model_model1-annotation_model2-return_dict_with_extra_data" + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_model1_annotation_model2_return_submodel_with_extra_data(): + response = client.get( + "/response_model_model1-annotation_model2-return_submodel_with_extra_data" + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_filtering_model_annotation_submodel_return_submodel(): + response = client.get( + "/response_model_filtering_model-annotation_submodel-return_submodel" + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_list_of_model_no_annotation(): + response = client.get("/response_model_list_of_model-no_annotation") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "John", "surname": "Doe"}, + {"name": "Jane", "surname": "Does"}, + ] + + +def test_no_response_model_annotation_list_of_model(): + response = client.get("/no_response_model-annotation_list_of_model") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "John", "surname": "Doe"}, + {"name": "Jane", "surname": "Does"}, + ] + + +def test_no_response_model_annotation_forward_ref_list_of_model(): + response = client.get("/no_response_model-annotation_forward_ref_list_of_model") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "John", "surname": "Doe"}, + {"name": "Jane", "surname": "Does"}, + ] + + +def test_response_model_union_no_annotation_return_model1(): + response = client.get("/response_model_union-no_annotation-return_model1") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_union_no_annotation_return_model2(): + response = client.get("/response_model_union-no_annotation-return_model2") + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "price": 42.0} + + +def test_no_response_model_annotation_union_return_model1(): + response = client.get("/no_response_model-annotation_union-return_model1") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +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] diff --git a/tests/test_route_scope.py b/tests/test_route_scope.py index a188e9a5f..2021c828f 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 5ff1fdf9f..ba6b76382 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,49 @@ class State(BaseModel): sub_router_shutdown: bool = False -state = State() - -app = FastAPI() - - -@app.on_event("startup") -def app_startup(): - state.app_startup = True - - -@app.on_event("shutdown") -def app_shutdown(): - state.app_shutdown = True - +@pytest.fixture +def state() -> State: + return State() -router = APIRouter() +def test_router_events(state: State) -> None: + app = FastAPI() -@router.on_event("startup") -def router_startup(): - state.router_startup = True + @app.get("/") + def main() -> Dict[str, str]: + return {"message": "Hello World"} + @app.on_event("startup") + def app_startup() -> None: + state.app_startup = True -@router.on_event("shutdown") -def router_shutdown(): - state.router_shutdown = True + @app.on_event("shutdown") + def app_shutdown() -> None: + state.app_shutdown = True + router = APIRouter() -sub_router = APIRouter() + @router.on_event("startup") + def router_startup() -> None: + state.router_startup = True + @router.on_event("shutdown") + def router_shutdown() -> None: + state.router_shutdown = True -@sub_router.on_event("startup") -def sub_router_startup(): - state.sub_router_startup = True + sub_router = APIRouter() + @sub_router.on_event("startup") + def sub_router_startup() -> None: + state.sub_router_startup = True -@sub_router.on_event("shutdown") -def sub_router_shutdown(): - state.sub_router_shutdown = True + @sub_router.on_event("shutdown") + def sub_router_shutdown() -> None: + state.sub_router_shutdown = True + router.include_router(sub_router) + app.include_router(router) -@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 +81,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_tutorial/test_additional_status_codes/test_tutorial001_an.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an.py new file mode 100644 index 000000000..2cb2bb993 --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an.py @@ -0,0 +1,17 @@ +from fastapi.testclient import TestClient + +from docs_src.additional_status_codes.tutorial001_an import app + +client = TestClient(app) + + +def test_update(): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +def test_create(): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py new file mode 100644 index 000000000..c7660a392 --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py @@ -0,0 +1,26 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.additional_status_codes.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_update(client: TestClient): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +@needs_py310 +def test_create(client: TestClient): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py new file mode 100644 index 000000000..303c5dbae --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py @@ -0,0 +1,26 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.additional_status_codes.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_update(client: TestClient): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +@needs_py39 +def test_create(client: TestClient): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py new file mode 100644 index 000000000..02f2e188c --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py @@ -0,0 +1,26 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.additional_status_codes.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_update(client: TestClient): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +@needs_py310 +def test_create(client: TestClient): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py new file mode 100644 index 000000000..af682ecff --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py @@ -0,0 +1,19 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from docs_src.background_tasks.tutorial002_an import app + +client = TestClient(app) + + +def test(): + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py new file mode 100644 index 000000000..067b2787e --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@needs_py310 +def test(): + from docs_src.background_tasks.tutorial002_an_py310 import app + + client = TestClient(app) + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py new file mode 100644 index 000000000..06b5a2f57 --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@needs_py39 +def test(): + from docs_src.background_tasks.tutorial002_an_py39 import app + + client = TestClient(app) + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an.py b/tests/test_tutorial/test_bigger_applications/test_main_an.py new file mode 100644 index 000000000..4b84a31b5 --- /dev/null +++ b/tests/test_tutorial/test_bigger_applications/test_main_an.py @@ -0,0 +1,491 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.bigger_applications.app_an.main import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +no_jessica = { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + + +@pytest.mark.parametrize( + "path,expected_status,expected_response,headers", + [ + ( + "/users?token=jessica", + 200, + [{"username": "Rick"}, {"username": "Morty"}], + {}, + ), + ("/users", 422, no_jessica, {}), + ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), + ("/users/foo", 422, no_jessica, {}), + ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), + ("/users/me", 422, no_jessica, {}), + ( + "/users?token=monica", + 400, + {"detail": "No Jessica token provided"}, + {}, + ), + ( + "/items?token=jessica", + 200, + {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, + {"X-Token": "fake-super-secret-token"}, + ), + ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), + ( + "/items/plumbus?token=jessica", + 200, + {"name": "Plumbus", "item_id": "plumbus"}, + {"X-Token": "fake-super-secret-token"}, + ), + ( + "/items/bar?token=jessica", + 404, + {"detail": "Item not found"}, + {"X-Token": "fake-super-secret-token"}, + ), + ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), + ( + "/items?token=jessica", + 400, + {"detail": "X-Token header invalid"}, + {"X-Token": "invalid"}, + ), + ( + "/items/bar?token=jessica", + 400, + {"detail": "X-Token header invalid"}, + {"X-Token": "invalid"}, + ), + ( + "/items?token=jessica", + 422, + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + }, + {}, + ), + ( + "/items/plumbus?token=jessica", + 422, + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + }, + {}, + ), + ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), + ("/", 422, no_jessica, {}), + ("/openapi.json", 200, openapi_schema, {}), + ], +) +def test_get_path(path, expected_status, expected_response, headers): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_put_no_header(): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +def test_put_invalid_header(): + response = client.put("/items/foo", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_put(): + response = client.put( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} + + +def test_put_forbidden(): + response = client.put( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 403, response.text + assert response.json() == {"detail": "You can only update the item: plumbus"} + + +def test_admin(): + response = client.post( + "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin getting schwifty"} + + +def test_admin_invalid_header(): + response = client.post("/admin/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py new file mode 100644 index 000000000..1caf5bd49 --- /dev/null +++ b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py @@ -0,0 +1,506 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +no_jessica = { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.bigger_applications.app_an_py39.main import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,expected_status,expected_response,headers", + [ + ( + "/users?token=jessica", + 200, + [{"username": "Rick"}, {"username": "Morty"}], + {}, + ), + ("/users", 422, no_jessica, {}), + ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), + ("/users/foo", 422, no_jessica, {}), + ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), + ("/users/me", 422, no_jessica, {}), + ( + "/users?token=monica", + 400, + {"detail": "No Jessica token provided"}, + {}, + ), + ( + "/items?token=jessica", + 200, + {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, + {"X-Token": "fake-super-secret-token"}, + ), + ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), + ( + "/items/plumbus?token=jessica", + 200, + {"name": "Plumbus", "item_id": "plumbus"}, + {"X-Token": "fake-super-secret-token"}, + ), + ( + "/items/bar?token=jessica", + 404, + {"detail": "Item not found"}, + {"X-Token": "fake-super-secret-token"}, + ), + ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), + ( + "/items?token=jessica", + 400, + {"detail": "X-Token header invalid"}, + {"X-Token": "invalid"}, + ), + ( + "/items/bar?token=jessica", + 400, + {"detail": "X-Token header invalid"}, + {"X-Token": "invalid"}, + ), + ( + "/items?token=jessica", + 422, + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + }, + {}, + ), + ( + "/items/plumbus?token=jessica", + 422, + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + }, + {}, + ), + ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), + ("/", 422, no_jessica, {}), + ("/openapi.json", 200, openapi_schema, {}), + ], +) +def test_get_path( + path, expected_status, expected_response, headers, client: TestClient +): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py39 +def test_put_no_header(client: TestClient): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +@needs_py39 +def test_put_invalid_header(client: TestClient): + response = client.put("/items/foo", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_put(client: TestClient): + response = client.put( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} + + +@needs_py39 +def test_put_forbidden(client: TestClient): + response = client.put( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 403, response.text + assert response.json() == {"detail": "You can only update the item: plumbus"} + + +@needs_py39 +def test_admin(client: TestClient): + response = client.post( + "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin getting schwifty"} + + +@needs_py39 +def test_admin_invalid_header(client: TestClient): + response = client.post("/admin/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py new file mode 100644 index 000000000..879769147 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py @@ -0,0 +1,169 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.body_fields.tutorial001_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +price_not_greater = { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] +} + + +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + {"item": {"name": "Foo", "price": 3.0}}, + 200, + { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + }, + ), + ( + "/items/6", + { + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + 200, + { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + }, + ), + ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), + ], +) +def test(path, body, expected_status, expected_response): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py new file mode 100644 index 000000000..0cd57a187 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py @@ -0,0 +1,176 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +price_not_greater = { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + {"item": {"name": "Foo", "price": 3.0}}, + 200, + { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + }, + ), + ( + "/items/6", + { + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + 200, + { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + }, + ), + ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), + ], +) +def test(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py new file mode 100644 index 000000000..26ff26f50 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py @@ -0,0 +1,176 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +price_not_greater = { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] +} + + +@needs_py39 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + {"item": {"name": "Foo", "price": 3.0}}, + 200, + { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + }, + ), + ( + "/items/6", + { + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + 200, + { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + }, + ), + ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), + ], +) +def test(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py new file mode 100644 index 000000000..94ba8593a --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py @@ -0,0 +1,147 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.body_multiple_params.tutorial001_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "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"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +item_id_not_int = { + "detail": [ + { + "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, + }, + "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 diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py new file mode 100644 index 000000000..cd378ec9c --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py @@ -0,0 +1,155 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +item_id_not_int = { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5?q=bar", + {"name": "Foo", "price": 50.5}, + 200, + { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + }, + ), + ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), + ("/items/5", None, 200, {"item_id": 5}), + ("/items/foo", None, 422, item_id_not_int), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_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 000000000..b8fe1baaf --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py @@ -0,0 +1,155 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +item_id_not_int = { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] +} + + +@needs_py39 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5?q=bar", + {"name": "Foo", "price": 50.5}, + 200, + { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + }, + ), + ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), + ("/items/5", None, 200, {"item_id": 5}), + ("/items/foo", None, 422, item_id_not_int), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py new file mode 100644 index 000000000..788db8b30 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py @@ -0,0 +1,198 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.body_multiple_params.tutorial003_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + { + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + 200, + { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + }, + ), + ( + "/items/5", + None, + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ( + "/items/5", + [], + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ], +) +def test_post_body(path, body, expected_status, expected_response): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py new file mode 100644 index 000000000..9003016cd --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py @@ -0,0 +1,206 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + { + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + 200, + { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + }, + ), + ( + "/items/5", + None, + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ( + "/items/5", + [], + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py new file mode 100644 index 000000000..bc014a441 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py @@ -0,0 +1,206 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py39 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + { + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + 200, + { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + }, + ), + ( + "/items/5", + None, + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ( + "/items/5", + [], + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py new file mode 100644 index 000000000..fb60ea993 --- /dev/null +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py @@ -0,0 +1,92 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.cookie_params.tutorial001_an 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"}), + ( + "/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 diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py new file mode 100644 index 000000000..308886085 --- /dev/null +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py @@ -0,0 +1,95 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@needs_py310 +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), + ], +) +def test(path, cookies, expected_status, expected_response): + from docs_src.cookie_params.tutorial001_an_py310 import app + + client = TestClient(app, cookies=cookies) + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py new file mode 100644 index 000000000..bbfe5ff9a --- /dev/null +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py @@ -0,0 +1,95 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@needs_py39 +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), + ], +) +def test(path, cookies, expected_status, expected_response): + from docs_src.cookie_params.tutorial001_an_py39 import app + + client = TestClient(app, cookies=cookies) + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dataclasses/__init__.py b/tests/test_tutorial/test_dataclasses/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 34aeb0be5..f5597e30c 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -54,7 +54,7 @@ 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 + # Ref: https://github.com/pydantic/pydantic/pull/2557 data = response.json() alternative_data1 = deepcopy(data) alternative_data2 = deepcopy(data) diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py new file mode 100644 index 000000000..13960addc --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py @@ -0,0 +1,149 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial001_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py new file mode 100644 index 000000000..4b093af0d --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py @@ -0,0 +1,157 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py new file mode 100644 index 000000000..6059924cc --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py @@ -0,0 +1,157 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py new file mode 100644 index 000000000..ef6199b04 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py @@ -0,0 +1,144 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial004_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ( + "/items", + 200, + { + "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 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 000000000..e9736780c --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py @@ -0,0 +1,152 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial004_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@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 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 000000000..2b346f3b2 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py @@ -0,0 +1,152 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial004_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ( + "/items", + 200, + { + "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 diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py new file mode 100644 index 000000000..f33b67d58 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py @@ -0,0 +1,128 @@ +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial006_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_no_headers(): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +def test_get_invalid_one_header(): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_get_invalid_second_header(): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +def test_get_valid_headers(): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py new file mode 100644 index 000000000..171e39a96 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py @@ -0,0 +1,140 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial006_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_get_no_headers(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +@needs_py39 +def test_get_invalid_one_header(client: TestClient): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_get_invalid_second_header(client: TestClient): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +@needs_py39 +def test_get_valid_headers(client: TestClient): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py new file mode 100644 index 000000000..0a6908f72 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py @@ -0,0 +1,209 @@ +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial012_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_no_headers_items(): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +def test_get_no_headers_users(): + response = client.get("/users/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +def test_get_invalid_one_header_items(): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_get_invalid_one_users(): + response = client.get("/users/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_get_invalid_second_header_items(): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +def test_get_invalid_second_header_users(): + response = client.get( + "/users/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +def test_get_valid_headers_items(): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +def test_get_valid_headers_users(): + response = client.get( + "/users/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py new file mode 100644 index 000000000..25f54f4c9 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py @@ -0,0 +1,225 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial012_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_get_no_headers_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +@needs_py39 +def test_get_no_headers_users(client: TestClient): + response = client.get("/users/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +@needs_py39 +def test_get_invalid_one_header_items(client: TestClient): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_get_invalid_one_users(client: TestClient): + response = client.get("/users/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_get_invalid_second_header_items(client: TestClient): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +@needs_py39 +def test_get_invalid_second_header_users(client: TestClient): + response = client.get( + "/users/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +@needs_py39 +def test_get_valid_headers_items(client: TestClient): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +@needs_py39 +def test_get_valid_headers_users(client: TestClient): + response = client.get( + "/users/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] diff --git a/tests/test_tutorial/test_events/test_tutorial003.py b/tests/test_tutorial/test_events/test_tutorial003.py new file mode 100644 index 000000000..56b493954 --- /dev/null +++ b/tests/test_tutorial/test_events/test_tutorial003.py @@ -0,0 +1,86 @@ +from fastapi.testclient import TestClient + +from docs_src.events.tutorial003 import ( + app, + fake_answer_to_everything_ml_model, + ml_models, +) + +openapi_schema = { + "openapi": "3.0.2", + "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"}, + }, + }, + } + }, +} + + +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("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + 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" diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py new file mode 100644 index 000000000..d5be16dfb --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py @@ -0,0 +1,138 @@ +from fastapi.testclient import TestClient + +from docs_src.extra_data_types.tutorial001_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_extra_types(): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py new file mode 100644 index 000000000..80806b694 --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py @@ -0,0 +1,146 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_data_types.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_extra_types(client: TestClient): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py new file mode 100644 index 000000000..5c7d43394 --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py @@ -0,0 +1,146 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_data_types.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_extra_types(client: TestClient): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an.py b/tests/test_tutorial/test_header_params/test_tutorial001_an.py new file mode 100644 index 000000000..3c155f786 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an.py @@ -0,0 +1,88 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial001_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "User-Agent", "type": "string"}, + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"User-Agent": "testclient"}), + ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), + ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py new file mode 100644 index 000000000..1f86f2a5d --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py @@ -0,0 +1,94 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "User-Agent", "type": "string"}, + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"User-Agent": "testclient"}), + ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), + ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py new file mode 100644 index 000000000..b23398287 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -0,0 +1,99 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial002 import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an.py b/tests/test_tutorial/test_header_params/test_tutorial002_an.py new file mode 100644 index 000000000..77d236e09 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an.py @@ -0,0 +1,99 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial002_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py new file mode 100644 index 000000000..49b0ef462 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial002_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py new file mode 100644 index 000000000..13aaabeb8 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial002_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py new file mode 100644 index 000000000..6cae3d338 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial002_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py new file mode 100644 index 000000000..99dd9e25f --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -0,0 +1,98 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial003 import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an.py b/tests/test_tutorial/test_header_params/test_tutorial003_an.py new file mode 100644 index 000000000..4477da7a8 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an.py @@ -0,0 +1,98 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial003_an import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py new file mode 100644 index 000000000..b52304a2b --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial003_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py new file mode 100644 index 000000000..dffdd1622 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial003_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py new file mode 100644 index 000000000..64ef7b22a --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial003_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py similarity index 100% rename from tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py rename to tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py new file mode 100644 index 000000000..b2b9b5018 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py @@ -0,0 +1,122 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial010_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +regex_error = { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] +} + + +@pytest.mark.parametrize( + "q_name,q,expected_status,expected_response", + [ + (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ( + "item-query", + "fixedquery", + 200, + {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, + ), + ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ("item-query", "nonregexquery", 422, regex_error), + ], +) +def test_query_params_str_validations(q_name, q, expected_status, expected_response): + url = "/items/" + if q_name and q: + url = f"{url}?{q_name}={q}" + response = client.get(url) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py new file mode 100644 index 000000000..edbe4d009 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py @@ -0,0 +1,132 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +regex_error = { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "q_name,q,expected_status,expected_response", + [ + (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ( + "item-query", + "fixedquery", + 200, + {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, + ), + ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ("item-query", "nonregexquery", 422, regex_error), + ], +) +def test_query_params_str_validations( + q_name, q, expected_status, expected_response, client: TestClient +): + url = "/items/" + if q_name and q: + url = f"{url}?{q_name}={q}" + response = client.get(url) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py new file mode 100644 index 000000000..f51e90247 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py @@ -0,0 +1,132 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +regex_error = { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] +} + + +@needs_py39 +@pytest.mark.parametrize( + "q_name,q,expected_status,expected_response", + [ + (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ( + "item-query", + "fixedquery", + 200, + {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, + ), + ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ("item-query", "nonregexquery", 422, regex_error), + ], +) +def test_query_params_str_validations( + q_name, q, expected_status, expected_response, client: TestClient +): + url = "/items/" + if q_name and q: + url = f"{url}?{q_name}={q}" + response = client.get(url) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py similarity index 100% rename from tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py rename to tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py new file mode 100644 index 000000000..25a11f2ca --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py @@ -0,0 +1,95 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial011_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_multi_query_values(): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +def test_query_no_values(): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py new file mode 100644 index 000000000..99aaf3948 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial011_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py310 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py new file mode 100644 index 000000000..902add851 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial011_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py new file mode 100644 index 000000000..e57a2178f --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py @@ -0,0 +1,96 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial012_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_default_query_values(): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +def test_multi_query_values(): + url = "/items/?q=baz&q=foobar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["baz", "foobar"]} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py new file mode 100644 index 000000000..140b74790 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial012_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_default_query_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=baz&q=foobar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["baz", "foobar"]} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py new file mode 100644 index 000000000..fc684b557 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py @@ -0,0 +1,96 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial013_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_multi_query_values(): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +def test_query_no_values(): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": []} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py new file mode 100644 index 000000000..9d3f255e0 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial013_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": []} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py new file mode 100644 index 000000000..ba5bf7c50 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py @@ -0,0 +1,82 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial014_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_hidden_query(): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +def test_no_hidden_query(): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py new file mode 100644 index 000000000..69e176bab --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py @@ -0,0 +1,91 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial014_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_hidden_query(client: TestClient): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +@needs_py310 +def test_no_hidden_query(client: TestClient): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py new file mode 100644 index 000000000..2adfddfef --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py @@ -0,0 +1,91 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial014_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_hidden_query(client: TestClient): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +@needs_py310 +def test_no_hidden_query(client: TestClient): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py new file mode 100644 index 000000000..50b05fa4b --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py @@ -0,0 +1,157 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_02_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +def test_post_uploadfile_no_body(): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py new file mode 100644 index 000000000..a5796b74c --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py @@ -0,0 +1,169 @@ +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_02_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +@needs_py310 +def test_post_uploadfile_no_body(client: TestClient): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +@needs_py310 +def test_post_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py310 +def test_post_upload_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py new file mode 100644 index 000000000..57175f736 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py @@ -0,0 +1,169 @@ +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_02_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +@needs_py39 +def test_post_uploadfile_no_body(client: TestClient): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +@needs_py39 +def test_post_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py39 +def test_post_upload_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py new file mode 100644 index 000000000..e83fc68bb --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py @@ -0,0 +1,159 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_03_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py new file mode 100644 index 000000000..7808262a7 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py @@ -0,0 +1,167 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_03_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_post_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py39 +def test_post_upload_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_an.py new file mode 100644 index 000000000..739f04b43 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an.py @@ -0,0 +1,184 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_required + + +def test_post_body_json(): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_large_file(tmp_path): + default_pydantic_max_size = 2**16 + path = tmp_path / "test.txt" + path.write_bytes(b"x" * (default_pydantic_max_size + 1)) + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": default_pydantic_max_size + 1} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py new file mode 100644 index 000000000..091a9362b --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py @@ -0,0 +1,194 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_required + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +@needs_py39 +def test_post_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py39 +def test_post_large_file(tmp_path, client: TestClient): + default_pydantic_max_size = 2**16 + path = tmp_path / "test.txt" + path.write_bytes(b"x" * (default_pydantic_max_size + 1)) + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": default_pydantic_max_size + 1} + + +@needs_py39 +def test_post_upload_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an.py b/tests/test_tutorial/test_request_files/test_tutorial002_an.py new file mode 100644 index 000000000..8a99c657c --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an.py @@ -0,0 +1,215 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial002_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfiles/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + } + }, + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Main", + "operationId": "main__get", + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_required + + +def test_post_body_json(): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +def test_post_files(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +def test_get_root(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +@needs_py39 +def test_post_upload_file(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +@needs_py39 +def test_get_root(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +def test_get_root(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +@needs_py39 +def test_post_upload_file(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +@needs_py39 +def test_get_root(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 422, response.text + assert response.json() == token_required + + +def test_post_files_and_token(tmp_path): + patha = tmp_path / "test.txt" + pathb = tmp_path / "testb.txt" + patha.write_text("") + pathb.write_text("") + + client = TestClient(app) + with patha.open("rb") as filea, pathb.open("rb") as fileb: + response = client.post( + "/files/", + data={"token": "foo"}, + files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "file_size": 14, + "token": "foo", + "fileb_content_type": "text/plain", + } diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py new file mode 100644 index 000000000..fa2ebc77d --- /dev/null +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py @@ -0,0 +1,211 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file", "fileb", "token"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"}, + "fileb": {"title": "Fileb", "type": "string", "format": "binary"}, + "token": {"title": "Token", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_forms_and_files.tutorial001_an_py39 import app + + return app + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + +token_required = { + "detail": [ + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + +# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]} + +file_and_token_required = { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_and_token_required + + +@needs_py39 +def test_post_form_no_file(client: TestClient): + response = client.post("/files/", data={"token": "foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) + assert response.status_code == 422, response.text + assert response.json() == file_and_token_required + + +@needs_py39 +def test_post_file_no_token(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 422, response.text + assert response.json() == token_required + + +@needs_py39 +def test_post_files_and_token(tmp_path, app: FastAPI): + patha = tmp_path / "test.txt" + pathb = tmp_path / "testb.txt" + patha.write_text("") + pathb.write_text("") + + client = TestClient(app) + with patha.open("rb") as filea, pathb.open("rb") as fileb: + response = client.post( + "/files/", + data={"token": "foo"}, + files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "file_size": 14, + "token": "foo", + "fileb_content_type": "text/plain", + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py new file mode 100644 index 000000000..39a4734ed --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -0,0 +1,120 @@ +from fastapi.testclient import TestClient + +from docs_src.response_model.tutorial003_01 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "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": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string", "format": "email"}, + "full_name": {"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": {"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"}, + }, + }, + } + }, +} + + +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( + "/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", + } 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 000000000..3a04db6bc --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py @@ -0,0 +1,129 @@ +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": { + "/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": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string", "format": "email"}, + "full_name": {"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": {"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"}, + }, + }, + } + }, +} + + +@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_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( + "/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", + } 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 000000000..d933f871c --- /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) + +openapi_schema = { + "openapi": "3.0.2", + "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"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +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" 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 000000000..398eb4765 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_03.py @@ -0,0 +1,36 @@ +from fastapi.testclient import TestClient + +from docs_src.response_model.tutorial003_03 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "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": {}}}, + } + }, + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +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" 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 000000000..4aa80145a --- /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 000000000..b876facc8 --- /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 000000000..27896d490 --- /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) + +openapi_schema = { + "openapi": "3.0.2", + "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"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +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" 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 000000000..bf36c906b --- /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 + +openapi_schema = { + "openapi": "3.0.2", + "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"}, + }, + }, + } + }, +} + + +@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_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_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" diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py new file mode 100644 index 000000000..7694669ce --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py @@ -0,0 +1,134 @@ +from fastapi.testclient import TestClient + +from docs_src.schema_extra_example.tutorial004_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +def test_post_body_example(): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py new file mode 100644 index 000000000..c81fbcf52 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py @@ -0,0 +1,143 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial004_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py310 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py new file mode 100644 index 000000000..395c27b0e --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py @@ -0,0 +1,143 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial004_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py39 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 diff --git a/tests/test_tutorial/test_security/test_tutorial001_an.py b/tests/test_tutorial/test_security/test_tutorial001_an.py new file mode 100644 index 000000000..fdcb9bfb8 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial001_an.py @@ -0,0 +1,59 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial001_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_no_token(): + response = client.get("/items") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(): + response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + +def test_incorrect_token(): + response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial001_an_py39.py b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py new file mode 100644 index 000000000..4f8947b90 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py @@ -0,0 +1,70 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_no_token(client: TestClient): + response = client.get("/items") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_token(client: TestClient): + response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + +@needs_py39 +def test_incorrect_token(client: TestClient): + response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial003_an.py b/tests/test_tutorial/test_security/test_tutorial003_an.py new file mode 100644 index 000000000..b1b9c0fc1 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial003_an.py @@ -0,0 +1,176 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial003_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_login(): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} + + +def test_login_incorrect_password(): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_login_incorrect_username(): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_no_token(): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(): + response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + } + + +def test_incorrect_token(): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_incorrect_token_type(): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_inactive_user(): + response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py new file mode 100644 index 000000000..486f0c4ce --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py @@ -0,0 +1,192 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial003_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} + + +@needs_py310 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + } + + +@needs_py310 +def test_incorrect_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_inactive_user(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py new file mode 100644 index 000000000..b6709e2fb --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py @@ -0,0 +1,192 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial003_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} + + +@needs_py39 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + } + + +@needs_py39 +def test_incorrect_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_inactive_user(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} diff --git a/tests/test_tutorial/test_security/test_tutorial005_an.py b/tests/test_tutorial/test_security/test_tutorial005_an.py new file mode 100644 index 000000000..b6c2708f0 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005_an.py @@ -0,0 +1,347 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial005_an import ( + app, + create_access_token, + fake_users_db, + get_password_hash, + verify_password, +) + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + "disabled": {"title": "Disabled", "type": "boolean"}, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def get_access_token(username="johndoe", password="secret", scope=None): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +def test_login(): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +def test_login_incorrect_password(): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_login_incorrect_username(): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_no_token(): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(): + access_token = get_access_token(scope="me") + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +def test_incorrect_token(): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_incorrect_token_type(): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_verify_password(): + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +def test_get_password_hash(): + assert get_password_hash("secretalice") + + +def test_create_access_token(): + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +def test_token_no_sub(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_no_username(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_no_scope(): + access_token = get_access_token() + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not enough permissions"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_inexistent_user(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_inactive_user(): + access_token = get_access_token( + username="alice", password="secretalice", scope="me" + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +def test_read_items(): + access_token = get_access_token(scope="me items") + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +def test_read_system_status(): + access_token = get_access_token() + response = client.get( + "/status/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +def test_read_system_status_no_token(): + response = client.get("/status/") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py new file mode 100644 index 000000000..15a9445b9 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py @@ -0,0 +1,375 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + "disabled": {"title": "Disabled", "type": "boolean"}, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial005_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def get_access_token( + *, username="johndoe", password="secret", scope=None, client: TestClient +): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +@needs_py310 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +@needs_py310 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_token(client: TestClient): + access_token = get_access_token(scope="me", client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +@needs_py310 +def test_incorrect_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_verify_password(): + from docs_src.security.tutorial005_an_py310 import fake_users_db, verify_password + + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +@needs_py310 +def test_get_password_hash(): + from docs_src.security.tutorial005_an_py310 import get_password_hash + + assert get_password_hash("secretalice") + + +@needs_py310 +def test_create_access_token(): + from docs_src.security.tutorial005_an_py310 import create_access_token + + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +@needs_py310 +def test_token_no_sub(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_no_username(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_no_scope(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not enough permissions"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_inexistent_user(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_inactive_user(client: TestClient): + access_token = get_access_token( + username="alice", password="secretalice", scope="me", client=client + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +@needs_py310 +def test_read_items(client: TestClient): + access_token = get_access_token(scope="me items", client=client) + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +@needs_py310 +def test_read_system_status(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/status/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +@needs_py310 +def test_read_system_status_no_token(client: TestClient): + response = client.get("/status/") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py new file mode 100644 index 000000000..989424dd3 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py @@ -0,0 +1,375 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + "disabled": {"title": "Disabled", "type": "boolean"}, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial005_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def get_access_token( + *, username="johndoe", password="secret", scope=None, client: TestClient +): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +@needs_py39 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +@needs_py39 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_token(client: TestClient): + access_token = get_access_token(scope="me", client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +@needs_py39 +def test_incorrect_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_verify_password(): + from docs_src.security.tutorial005_an_py39 import fake_users_db, verify_password + + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +@needs_py39 +def test_get_password_hash(): + from docs_src.security.tutorial005_an_py39 import get_password_hash + + assert get_password_hash("secretalice") + + +@needs_py39 +def test_create_access_token(): + from docs_src.security.tutorial005_an_py39 import create_access_token + + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +@needs_py39 +def test_token_no_sub(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_no_username(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_no_scope(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not enough permissions"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_inexistent_user(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_inactive_user(client: TestClient): + access_token = get_access_token( + username="alice", password="secretalice", scope="me", client=client + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +@needs_py39 +def test_read_items(client: TestClient): + access_token = get_access_token(scope="me items", client=client) + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +@needs_py39 +def test_read_system_status(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/status/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +@needs_py39 +def test_read_system_status_no_token(client: TestClient): + response = client.get("/status/") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial006_an.py b/tests/test_tutorial/test_security/test_tutorial006_an.py new file mode 100644 index 000000000..1d1668fec --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial006_an.py @@ -0,0 +1,67 @@ +from base64 import b64encode + +from fastapi.testclient import TestClient + +from docs_src.security.tutorial006_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_security_http_basic(): + response = client.get("/users/me", auth=("john", "secret")) + assert response.status_code == 200, response.text + assert response.json() == {"username": "john", "password": "secret"} + + +def test_security_http_basic_no_credentials(): + response = client.get("/users/me") + assert response.json() == {"detail": "Not authenticated"} + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + + +def test_security_http_basic_invalid_credentials(): + response = client.get( + "/users/me", headers={"Authorization": "Basic notabase64token"} + ) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_security_http_basic_non_basic_credentials(): + payload = b64encode(b"johnsecret").decode("ascii") + auth_header = f"Basic {payload}" + response = client.get("/users/me", headers={"Authorization": auth_header}) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} diff --git a/tests/test_tutorial/test_security/test_tutorial006_an_py39.py b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py new file mode 100644 index 000000000..b72b5d864 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py @@ -0,0 +1,79 @@ +from base64 import b64encode + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial006_an import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_security_http_basic(client: TestClient): + response = client.get("/users/me", auth=("john", "secret")) + assert response.status_code == 200, response.text + assert response.json() == {"username": "john", "password": "secret"} + + +@needs_py39 +def test_security_http_basic_no_credentials(client: TestClient): + response = client.get("/users/me") + assert response.json() == {"detail": "Not authenticated"} + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + + +@needs_py39 +def test_security_http_basic_invalid_credentials(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Basic notabase64token"} + ) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} + + +@needs_py39 +def test_security_http_basic_non_basic_credentials(client: TestClient): + payload = b64encode(b"johnsecret").decode("ascii") + auth_header = f"Basic {payload}" + response = client.get("/users/me", headers={"Authorization": auth_header}) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} diff --git a/tests/test_tutorial/test_testing/test_main_b_an.py b/tests/test_tutorial/test_testing/test_main_b_an.py new file mode 100644 index 000000000..b64c5f710 --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main_b_an.py @@ -0,0 +1,10 @@ +from docs_src.app_testing.app_b_an import test_main + + +def test_app(): + test_main.test_create_existing_item() + test_main.test_create_item() + test_main.test_create_item_bad_token() + test_main.test_read_inexistent_item() + test_main.test_read_item() + test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_main_b_an_py310.py b/tests/test_tutorial/test_testing/test_main_b_an_py310.py new file mode 100644 index 000000000..194700b6d --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main_b_an_py310.py @@ -0,0 +1,13 @@ +from ...utils import needs_py310 + + +@needs_py310 +def test_app(): + from docs_src.app_testing.app_b_an_py310 import test_main + + test_main.test_create_existing_item() + test_main.test_create_item() + test_main.test_create_item_bad_token() + test_main.test_read_inexistent_item() + test_main.test_read_item() + test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_main_b_an_py39.py b/tests/test_tutorial/test_testing/test_main_b_an_py39.py new file mode 100644 index 000000000..2f8a13623 --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main_b_an_py39.py @@ -0,0 +1,13 @@ +from ...utils import needs_py39 + + +@needs_py39 +def test_app(): + from docs_src.app_testing.app_b_an_py39 import test_main + + test_main.test_create_existing_item() + test_main.test_create_item() + test_main.test_create_item_bad_token() + test_main.test_read_inexistent_item() + test_main.test_read_item() + test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py new file mode 100644 index 000000000..fc1f9149a --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py @@ -0,0 +1,56 @@ +from docs_src.dependency_testing.tutorial001_an import ( + app, + client, + test_override_in_items, + test_override_in_items_with_params, + test_override_in_items_with_q, +) + + +def test_override_in_items_run(): + test_override_in_items() + + +def test_override_in_items_with_q_run(): + test_override_in_items_with_q() + + +def test_override_in_items_with_params_run(): + test_override_in_items_with_params() + + +def test_override_in_users(): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_users_with_q(): + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_users_with_params(): + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_normal_app(): + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py new file mode 100644 index 000000000..a3d27f47f --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py @@ -0,0 +1,75 @@ +from ...utils import needs_py310 + + +@needs_py310 +def test_override_in_items_run(): + from docs_src.dependency_testing.tutorial001_an_py310 import test_override_in_items + + test_override_in_items() + + +@needs_py310 +def test_override_in_items_with_q_run(): + from docs_src.dependency_testing.tutorial001_an_py310 import ( + test_override_in_items_with_q, + ) + + test_override_in_items_with_q() + + +@needs_py310 +def test_override_in_items_with_params_run(): + from docs_src.dependency_testing.tutorial001_an_py310 import ( + test_override_in_items_with_params, + ) + + test_override_in_items_with_params() + + +@needs_py310 +def test_override_in_users(): + from docs_src.dependency_testing.tutorial001_an_py310 import client + + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_q(): + from docs_src.dependency_testing.tutorial001_an_py310 import client + + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_params(): + from docs_src.dependency_testing.tutorial001_an_py310 import client + + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_normal_app(): + from docs_src.dependency_testing.tutorial001_an_py310 import app, client + + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py new file mode 100644 index 000000000..f03ed5e07 --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py @@ -0,0 +1,75 @@ +from ...utils import needs_py39 + + +@needs_py39 +def test_override_in_items_run(): + from docs_src.dependency_testing.tutorial001_an_py39 import test_override_in_items + + test_override_in_items() + + +@needs_py39 +def test_override_in_items_with_q_run(): + from docs_src.dependency_testing.tutorial001_an_py39 import ( + test_override_in_items_with_q, + ) + + test_override_in_items_with_q() + + +@needs_py39 +def test_override_in_items_with_params_run(): + from docs_src.dependency_testing.tutorial001_an_py39 import ( + test_override_in_items_with_params, + ) + + test_override_in_items_with_params() + + +@needs_py39 +def test_override_in_users(): + from docs_src.dependency_testing.tutorial001_an_py39 import client + + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +@needs_py39 +def test_override_in_users_with_q(): + from docs_src.dependency_testing.tutorial001_an_py39 import client + + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py39 +def test_override_in_users_with_params(): + from docs_src.dependency_testing.tutorial001_an_py39 import client + + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py39 +def test_normal_app(): + from docs_src.dependency_testing.tutorial001_an_py39 import app, client + + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py new file mode 100644 index 000000000..776b916ff --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py @@ -0,0 +1,75 @@ +from ...utils import needs_py310 + + +@needs_py310 +def test_override_in_items_run(): + from docs_src.dependency_testing.tutorial001_py310 import test_override_in_items + + test_override_in_items() + + +@needs_py310 +def test_override_in_items_with_q_run(): + from docs_src.dependency_testing.tutorial001_py310 import ( + test_override_in_items_with_q, + ) + + test_override_in_items_with_q() + + +@needs_py310 +def test_override_in_items_with_params_run(): + from docs_src.dependency_testing.tutorial001_py310 import ( + test_override_in_items_with_params, + ) + + test_override_in_items_with_params() + + +@needs_py310 +def test_override_in_users(): + from docs_src.dependency_testing.tutorial001_py310 import client + + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_q(): + from docs_src.dependency_testing.tutorial001_py310 import client + + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_params(): + from docs_src.dependency_testing.tutorial001_py310 import client + + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_normal_app(): + from docs_src.dependency_testing.tutorial001_py310 import app, client + + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an.py b/tests/test_tutorial/test_websockets/test_tutorial002_an.py new file mode 100644 index 000000000..ec78d70d3 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_an.py @@ -0,0 +1,88 @@ +import pytest +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from docs_src.websockets.tutorial002_an import app + + +def test_main(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"" in response.content + + +def test_websocket_with_cookie(): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +def test_websocket_with_header(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +def test_websocket_with_header_and_query(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +def test_websocket_no_credentials(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +def test_websocket_invalid_data(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py b/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py new file mode 100644 index 000000000..23b4bcb78 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from ...utils import needs_py310 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial002_an_py310 import app + + return app + + +@needs_py310 +def test_main(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"" in response.content + + +@needs_py310 +def test_websocket_with_cookie(app: FastAPI): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +@needs_py310 +def test_websocket_with_header(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +@needs_py310 +def test_websocket_with_header_and_query(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +@needs_py310 +def test_websocket_no_credentials(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +@needs_py310 +def test_websocket_invalid_data(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py b/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py new file mode 100644 index 000000000..2d77f05b3 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from ...utils import needs_py39 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial002_an_py39 import app + + return app + + +@needs_py39 +def test_main(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"" in response.content + + +@needs_py39 +def test_websocket_with_cookie(app: FastAPI): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +@needs_py39 +def test_websocket_with_header(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +@needs_py39 +def test_websocket_with_header_and_query(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +@needs_py39 +def test_websocket_no_credentials(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +@needs_py39 +def test_websocket_invalid_data(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_py310.py b/tests/test_tutorial/test_websockets/test_tutorial002_py310.py new file mode 100644 index 000000000..03bc27bdf --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_py310.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from ...utils import needs_py310 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial002_py310 import app + + return app + + +@needs_py310 +def test_main(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"" in response.content + + +@needs_py310 +def test_websocket_with_cookie(app: FastAPI): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +@needs_py310 +def test_websocket_with_header(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +@needs_py310 +def test_websocket_with_header_and_query(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +@needs_py310 +def test_websocket_no_credentials(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +@needs_py310 +def test_websocket_invalid_data(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial003_py39.py b/tests/test_tutorial/test_websockets/test_tutorial003_py39.py new file mode 100644 index 000000000..06c4a9279 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial003_py39.py @@ -0,0 +1,50 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial003_py39 import app + + return app + + +@pytest.fixture(name="html") +def get_html(): + from docs_src.websockets.tutorial003_py39 import html + + return html + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + + return client + + +@needs_py39 +def test_get(client: TestClient, html: str): + response = client.get("/") + assert response.text == html + + +@needs_py39 +def test_websocket_handle_disconnection(client: TestClient): + with client.websocket_connect("/ws/1234") as connection, client.websocket_connect( + "/ws/5678" + ) as connection_two: + connection.send_text("Hello from 1234") + data1 = connection.receive_text() + assert data1 == "You wrote: Hello from 1234" + data2 = connection_two.receive_text() + client1_says = "Client #1234 says: Hello from 1234" + assert data2 == client1_says + data1 = connection.receive_text() + assert data1 == client1_says + connection_two.close() + data1 = connection.receive_text() + assert data1 == "Client #5678 left the chat" diff --git a/tests/test_typing_python39.py b/tests/test_typing_python39.py index b1ea635e7..26775efd4 100644 --- a/tests/test_typing_python39.py +++ b/tests/test_typing_python39.py @@ -1,10 +1,10 @@ from fastapi import FastAPI from fastapi.testclient import TestClient -from .utils import needs_py39 +from .utils import needs_py310 -@needs_py39 +@needs_py310 def test_typing(): types = { list[int]: [1, 2, 3],