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._"
+
+
+ FastAPI framework, high performance, easy to learn, fast to code, ready for production +
+ + +--- + +**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 -%} +async def
...uvicorn main:app --reload
...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/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/encoder.md b/docs/pt/docs/tutorial/encoder.md
new file mode 100644
index 000000000..bb04e9ca2
--- /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.6 e acima"
+
+ ```Python hl_lines="5 22"
+ {!> ../../../docs_src/encoder/tutorial001.py!}
+ ```
+
+=== "Python 3.10 e acima"
+
+ ```Python hl_lines="4 21"
+ {!> ../../../docs_src/encoder/tutorial001_py310.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/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`.
+
++ +**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/body-fields.md b/docs/ru/docs/tutorial/body-fields.md new file mode 100644 index 000000000..e8507c171 --- /dev/null +++ b/docs/ru/docs/tutorial/body-fields.md @@ -0,0 +1,69 @@ +# Body - Поля + +Таким же способом, как вы объявляете дополнительную валидацию и метаданные в параметрах *функции обработки пути* с помощью функций `Query`, `Path` и `Body`, вы можете объявлять валидацию и метаданные внутри Pydantic моделей, используя функцию `Field` из Pydantic. + +## Импорт `Field` + +Сначала вы должны импортировать его: + +=== "Python 3.6 и выше" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="2" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +!!! warning "Внимание" + Обратите внимание, что функция `Field` импортируется непосредственно из `pydantic`, а не из `fastapi`, как все остальные функции (`Query`, `Path`, `Body` и т.д.). + +## Объявление атрибутов модели + +Вы можете использовать функцию `Field` с атрибутами модели: + +=== "Python 3.6 и выше" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="9-12" + {!> ../../../docs_src/body_fields/tutorial001_py310.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..75e9d9064 --- /dev/null +++ b/docs/ru/docs/tutorial/cookie-params.md @@ -0,0 +1,49 @@ +# Параметры Cookie + +Вы можете задать параметры Cookie таким же способом, как `Query` и `Path` параметры. + +## Импорт `Cookie` + +Сначала импортируйте `Cookie`: + +=== "Python 3.6 и выше" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="1" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +## Объявление параметров `Cookie` + +Затем объявляйте параметры cookie, используя ту же структуру, что и с `Path` и `Query`. + +Первое значение - это значение по умолчанию, вы можете передать все дополнительные параметры проверки или аннотации: + +=== "Python 3.6 и выше" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="7" + {!> ../../../docs_src/cookie_params/tutorial001_py310.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: + +
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/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/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/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_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/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/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/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/fastapi/__init__.py b/fastapi/__init__.py
index 037d9804b..05da7b759 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.94.1"
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..a982b071a 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -253,7 +253,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 +261,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,
@@ -686,7 +696,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/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 986038891..4fd4926e1 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..391c47d81 100644
--- a/fastapi/utils.py
+++ b/fastapi/utils.py
@@ -88,7 +88,13 @@ def create_response_field(
return response_field(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..b8d006359 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 = [
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/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_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_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_request_files/test_tutorial002_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py
index de4127057..633795dee 100644
--- a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py
+++ b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py
@@ -150,7 +150,6 @@ def get_app():
@pytest.fixture(name="client")
def get_client(app: FastAPI):
-
client = TestClient(app)
return client
diff --git a/tests/test_tutorial/test_request_files/test_tutorial003_py39.py b/tests/test_tutorial/test_request_files/test_tutorial003_py39.py
index 56aeb54cd..474da8ba0 100644
--- a/tests/test_tutorial/test_request_files/test_tutorial003_py39.py
+++ b/tests/test_tutorial/test_request_files/test_tutorial003_py39.py
@@ -152,7 +152,6 @@ def get_app():
@pytest.fixture(name="client")
def get_client(app: FastAPI):
-
client = TestClient(app)
return client
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"