committed by
GitHub
233 changed files with 4879 additions and 26536 deletions
@ -1,7 +0,0 @@ |
|||||
FROM python:3.9 |
|
||||
|
|
||||
RUN pip install httpx PyGithub "pydantic==1.5.1" "pyyaml>=5.3.1,<6.0.0" |
|
||||
|
|
||||
COPY ./app /app |
|
||||
|
|
||||
CMD ["python", "/app/main.py"] |
|
@ -1,10 +0,0 @@ |
|||||
name: "Notify Translations" |
|
||||
description: "Notify in the issue for a translation when there's a new PR available" |
|
||||
author: "Sebastián Ramírez <[email protected]>" |
|
||||
inputs: |
|
||||
token: |
|
||||
description: 'Token, to read the GitHub API. Can be passed in using {{ secrets.GITHUB_TOKEN }}' |
|
||||
required: true |
|
||||
runs: |
|
||||
using: 'docker' |
|
||||
image: 'Dockerfile' |
|
@ -1,7 +0,0 @@ |
|||||
FROM python:3.9 |
|
||||
|
|
||||
RUN pip install httpx PyGithub "pydantic==2.0.2" pydantic-settings "pyyaml>=5.3.1,<6.0.0" |
|
||||
|
|
||||
COPY ./app /app |
|
||||
|
|
||||
CMD ["python", "/app/main.py"] |
|
@ -1,10 +0,0 @@ |
|||||
name: "Generate FastAPI People" |
|
||||
description: "Generate the data for the FastAPI People page" |
|
||||
author: "Sebastián Ramírez <[email protected]>" |
|
||||
inputs: |
|
||||
token: |
|
||||
description: 'User token, to read the GitHub API. Can be passed in using {{ secrets.FASTAPI_PEOPLE }}' |
|
||||
required: true |
|
||||
runs: |
|
||||
using: 'docker' |
|
||||
image: 'Dockerfile' |
|
@ -1,682 +0,0 @@ |
|||||
import logging |
|
||||
import subprocess |
|
||||
import sys |
|
||||
from collections import Counter, defaultdict |
|
||||
from datetime import datetime, timedelta, timezone |
|
||||
from pathlib import Path |
|
||||
from typing import Any, Container, DefaultDict, Dict, List, Set, Union |
|
||||
|
|
||||
import httpx |
|
||||
import yaml |
|
||||
from github import Github |
|
||||
from pydantic import BaseModel, SecretStr |
|
||||
from pydantic_settings import BaseSettings |
|
||||
|
|
||||
github_graphql_url = "https://api.github.com/graphql" |
|
||||
questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0" |
|
||||
|
|
||||
discussions_query = """ |
|
||||
query Q($after: String, $category_id: ID) { |
|
||||
repository(name: "fastapi", owner: "fastapi") { |
|
||||
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 |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
""" |
|
||||
|
|
||||
|
|
||||
prs_query = """ |
|
||||
query Q($after: String) { |
|
||||
repository(name: "fastapi", owner: "fastapi") { |
|
||||
pullRequests(first: 100, after: $after) { |
|
||||
edges { |
|
||||
cursor |
|
||||
node { |
|
||||
number |
|
||||
labels(first: 100) { |
|
||||
nodes { |
|
||||
name |
|
||||
} |
|
||||
} |
|
||||
author { |
|
||||
login |
|
||||
avatarUrl |
|
||||
url |
|
||||
} |
|
||||
title |
|
||||
createdAt |
|
||||
state |
|
||||
comments(first: 100) { |
|
||||
nodes { |
|
||||
createdAt |
|
||||
author { |
|
||||
login |
|
||||
avatarUrl |
|
||||
url |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
reviews(first:100) { |
|
||||
nodes { |
|
||||
author { |
|
||||
login |
|
||||
avatarUrl |
|
||||
url |
|
||||
} |
|
||||
state |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
""" |
|
||||
|
|
||||
sponsors_query = """ |
|
||||
query Q($after: String) { |
|
||||
user(login: "fastapi") { |
|
||||
sponsorshipsAsMaintainer(first: 100, after: $after) { |
|
||||
edges { |
|
||||
cursor |
|
||||
node { |
|
||||
sponsorEntity { |
|
||||
... on Organization { |
|
||||
login |
|
||||
avatarUrl |
|
||||
url |
|
||||
} |
|
||||
... on User { |
|
||||
login |
|
||||
avatarUrl |
|
||||
url |
|
||||
} |
|
||||
} |
|
||||
tier { |
|
||||
name |
|
||||
monthlyPriceInDollars |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
""" |
|
||||
|
|
||||
|
|
||||
class Author(BaseModel): |
|
||||
login: str |
|
||||
avatarUrl: str |
|
||||
url: str |
|
||||
|
|
||||
|
|
||||
# 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 DiscussionsNode(BaseModel): |
|
||||
number: int |
|
||||
author: Union[Author, None] = None |
|
||||
title: str |
|
||||
createdAt: datetime |
|
||||
comments: DiscussionsComments |
|
||||
|
|
||||
|
|
||||
class DiscussionsEdge(BaseModel): |
|
||||
cursor: str |
|
||||
node: DiscussionsNode |
|
||||
|
|
||||
|
|
||||
class Discussions(BaseModel): |
|
||||
edges: List[DiscussionsEdge] |
|
||||
|
|
||||
|
|
||||
class DiscussionsRepository(BaseModel): |
|
||||
discussions: Discussions |
|
||||
|
|
||||
|
|
||||
class DiscussionsResponseData(BaseModel): |
|
||||
repository: DiscussionsRepository |
|
||||
|
|
||||
|
|
||||
class DiscussionsResponse(BaseModel): |
|
||||
data: DiscussionsResponseData |
|
||||
|
|
||||
|
|
||||
# PRs |
|
||||
|
|
||||
|
|
||||
class LabelNode(BaseModel): |
|
||||
name: str |
|
||||
|
|
||||
|
|
||||
class Labels(BaseModel): |
|
||||
nodes: List[LabelNode] |
|
||||
|
|
||||
|
|
||||
class ReviewNode(BaseModel): |
|
||||
author: Union[Author, None] = None |
|
||||
state: str |
|
||||
|
|
||||
|
|
||||
class Reviews(BaseModel): |
|
||||
nodes: List[ReviewNode] |
|
||||
|
|
||||
|
|
||||
class PullRequestNode(BaseModel): |
|
||||
number: int |
|
||||
labels: Labels |
|
||||
author: Union[Author, None] = None |
|
||||
title: str |
|
||||
createdAt: datetime |
|
||||
state: str |
|
||||
comments: Comments |
|
||||
reviews: Reviews |
|
||||
|
|
||||
|
|
||||
class PullRequestEdge(BaseModel): |
|
||||
cursor: str |
|
||||
node: PullRequestNode |
|
||||
|
|
||||
|
|
||||
class PullRequests(BaseModel): |
|
||||
edges: List[PullRequestEdge] |
|
||||
|
|
||||
|
|
||||
class PRsRepository(BaseModel): |
|
||||
pullRequests: PullRequests |
|
||||
|
|
||||
|
|
||||
class PRsResponseData(BaseModel): |
|
||||
repository: PRsRepository |
|
||||
|
|
||||
|
|
||||
class PRsResponse(BaseModel): |
|
||||
data: PRsResponseData |
|
||||
|
|
||||
|
|
||||
# Sponsors |
|
||||
|
|
||||
|
|
||||
class SponsorEntity(BaseModel): |
|
||||
login: str |
|
||||
avatarUrl: str |
|
||||
url: str |
|
||||
|
|
||||
|
|
||||
class Tier(BaseModel): |
|
||||
name: str |
|
||||
monthlyPriceInDollars: float |
|
||||
|
|
||||
|
|
||||
class SponsorshipAsMaintainerNode(BaseModel): |
|
||||
sponsorEntity: SponsorEntity |
|
||||
tier: Tier |
|
||||
|
|
||||
|
|
||||
class SponsorshipAsMaintainerEdge(BaseModel): |
|
||||
cursor: str |
|
||||
node: SponsorshipAsMaintainerNode |
|
||||
|
|
||||
|
|
||||
class SponsorshipAsMaintainer(BaseModel): |
|
||||
edges: List[SponsorshipAsMaintainerEdge] |
|
||||
|
|
||||
|
|
||||
class SponsorsUser(BaseModel): |
|
||||
sponsorshipsAsMaintainer: SponsorshipAsMaintainer |
|
||||
|
|
||||
|
|
||||
class SponsorsResponseData(BaseModel): |
|
||||
user: SponsorsUser |
|
||||
|
|
||||
|
|
||||
class SponsorsResponse(BaseModel): |
|
||||
data: SponsorsResponseData |
|
||||
|
|
||||
|
|
||||
class Settings(BaseSettings): |
|
||||
input_token: SecretStr |
|
||||
github_repository: str |
|
||||
httpx_timeout: int = 30 |
|
||||
|
|
||||
|
|
||||
def get_graphql_response( |
|
||||
*, |
|
||||
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()}"} |
|
||||
# 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, |
|
||||
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(data["errors"]) |
|
||||
logging.error(response.text) |
|
||||
raise RuntimeError(response.text) |
|
||||
return data |
|
||||
|
|
||||
|
|
||||
def get_graphql_question_discussion_edges( |
|
||||
*, |
|
||||
settings: Settings, |
|
||||
after: Union[str, None] = None, |
|
||||
): |
|
||||
data = get_graphql_response( |
|
||||
settings=settings, |
|
||||
query=discussions_query, |
|
||||
after=after, |
|
||||
category_id=questions_category_id, |
|
||||
) |
|
||||
graphql_response = DiscussionsResponse.model_validate(data) |
|
||||
return graphql_response.data.repository.discussions.edges |
|
||||
|
|
||||
|
|
||||
def get_graphql_pr_edges(*, settings: Settings, after: Union[str, None] = None): |
|
||||
data = get_graphql_response(settings=settings, query=prs_query, after=after) |
|
||||
graphql_response = PRsResponse.model_validate(data) |
|
||||
return graphql_response.data.repository.pullRequests.edges |
|
||||
|
|
||||
|
|
||||
def get_graphql_sponsor_edges(*, settings: Settings, after: Union[str, None] = None): |
|
||||
data = get_graphql_response(settings=settings, query=sponsors_query, after=after) |
|
||||
graphql_response = SponsorsResponse.model_validate(data) |
|
||||
return graphql_response.data.user.sponsorshipsAsMaintainer.edges |
|
||||
|
|
||||
|
|
||||
class DiscussionExpertsResults(BaseModel): |
|
||||
commenters: Counter |
|
||||
last_month_commenters: Counter |
|
||||
three_months_commenters: Counter |
|
||||
six_months_commenters: Counter |
|
||||
one_year_commenters: Counter |
|
||||
authors: Dict[str, Author] |
|
||||
|
|
||||
|
|
||||
def get_discussion_nodes(settings: Settings) -> List[DiscussionsNode]: |
|
||||
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 |
|
||||
) |
|
||||
return discussion_nodes |
|
||||
|
|
||||
|
|
||||
def get_discussions_experts( |
|
||||
discussion_nodes: List[DiscussionsNode], |
|
||||
) -> DiscussionExpertsResults: |
|
||||
commenters = Counter() |
|
||||
last_month_commenters = Counter() |
|
||||
three_months_commenters = Counter() |
|
||||
six_months_commenters = Counter() |
|
||||
one_year_commenters = Counter() |
|
||||
authors: Dict[str, Author] = {} |
|
||||
|
|
||||
now = datetime.now(tz=timezone.utc) |
|
||||
one_month_ago = now - timedelta(days=30) |
|
||||
three_months_ago = now - timedelta(days=90) |
|
||||
six_months_ago = now - timedelta(days=180) |
|
||||
one_year_ago = now - timedelta(days=365) |
|
||||
|
|
||||
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: dict[str, datetime] = {} |
|
||||
for comment in discussion.comments.nodes: |
|
||||
if comment.author: |
|
||||
authors[comment.author.login] = comment.author |
|
||||
if comment.author.login != discussion_author_name: |
|
||||
author_time = discussion_commentors.get( |
|
||||
comment.author.login, comment.createdAt |
|
||||
) |
|
||||
discussion_commentors[comment.author.login] = max( |
|
||||
author_time, comment.createdAt |
|
||||
) |
|
||||
for reply in comment.replies.nodes: |
|
||||
if reply.author: |
|
||||
authors[reply.author.login] = reply.author |
|
||||
if reply.author.login != discussion_author_name: |
|
||||
author_time = discussion_commentors.get( |
|
||||
reply.author.login, reply.createdAt |
|
||||
) |
|
||||
discussion_commentors[reply.author.login] = max( |
|
||||
author_time, reply.createdAt |
|
||||
) |
|
||||
for author_name, author_time in discussion_commentors.items(): |
|
||||
commenters[author_name] += 1 |
|
||||
if author_time > one_month_ago: |
|
||||
last_month_commenters[author_name] += 1 |
|
||||
if author_time > three_months_ago: |
|
||||
three_months_commenters[author_name] += 1 |
|
||||
if author_time > six_months_ago: |
|
||||
six_months_commenters[author_name] += 1 |
|
||||
if author_time > one_year_ago: |
|
||||
one_year_commenters[author_name] += 1 |
|
||||
discussion_experts_results = DiscussionExpertsResults( |
|
||||
authors=authors, |
|
||||
commenters=commenters, |
|
||||
last_month_commenters=last_month_commenters, |
|
||||
three_months_commenters=three_months_commenters, |
|
||||
six_months_commenters=six_months_commenters, |
|
||||
one_year_commenters=one_year_commenters, |
|
||||
) |
|
||||
return discussion_experts_results |
|
||||
|
|
||||
|
|
||||
def get_pr_nodes(settings: Settings) -> List[PullRequestNode]: |
|
||||
pr_nodes: List[PullRequestNode] = [] |
|
||||
pr_edges = get_graphql_pr_edges(settings=settings) |
|
||||
|
|
||||
while pr_edges: |
|
||||
for edge in pr_edges: |
|
||||
pr_nodes.append(edge.node) |
|
||||
last_edge = pr_edges[-1] |
|
||||
pr_edges = get_graphql_pr_edges(settings=settings, after=last_edge.cursor) |
|
||||
return pr_nodes |
|
||||
|
|
||||
|
|
||||
class ContributorsResults(BaseModel): |
|
||||
contributors: Counter |
|
||||
commenters: Counter |
|
||||
reviewers: Counter |
|
||||
translation_reviewers: Counter |
|
||||
authors: Dict[str, Author] |
|
||||
|
|
||||
|
|
||||
def get_contributors(pr_nodes: List[PullRequestNode]) -> ContributorsResults: |
|
||||
contributors = Counter() |
|
||||
commenters = Counter() |
|
||||
reviewers = Counter() |
|
||||
translation_reviewers = Counter() |
|
||||
authors: Dict[str, Author] = {} |
|
||||
|
|
||||
for pr in pr_nodes: |
|
||||
author_name = None |
|
||||
if pr.author: |
|
||||
authors[pr.author.login] = pr.author |
|
||||
author_name = pr.author.login |
|
||||
pr_commentors: Set[str] = set() |
|
||||
pr_reviewers: Set[str] = set() |
|
||||
for comment in pr.comments.nodes: |
|
||||
if comment.author: |
|
||||
authors[comment.author.login] = comment.author |
|
||||
if comment.author.login == author_name: |
|
||||
continue |
|
||||
pr_commentors.add(comment.author.login) |
|
||||
for author_name in pr_commentors: |
|
||||
commenters[author_name] += 1 |
|
||||
for review in pr.reviews.nodes: |
|
||||
if review.author: |
|
||||
authors[review.author.login] = review.author |
|
||||
pr_reviewers.add(review.author.login) |
|
||||
for label in pr.labels.nodes: |
|
||||
if label.name == "lang-all": |
|
||||
translation_reviewers[review.author.login] += 1 |
|
||||
break |
|
||||
for reviewer in pr_reviewers: |
|
||||
reviewers[reviewer] += 1 |
|
||||
if pr.state == "MERGED" and pr.author: |
|
||||
contributors[pr.author.login] += 1 |
|
||||
return ContributorsResults( |
|
||||
contributors=contributors, |
|
||||
commenters=commenters, |
|
||||
reviewers=reviewers, |
|
||||
translation_reviewers=translation_reviewers, |
|
||||
authors=authors, |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def get_individual_sponsors(settings: Settings): |
|
||||
nodes: List[SponsorshipAsMaintainerNode] = [] |
|
||||
edges = get_graphql_sponsor_edges(settings=settings) |
|
||||
|
|
||||
while edges: |
|
||||
for edge in edges: |
|
||||
nodes.append(edge.node) |
|
||||
last_edge = edges[-1] |
|
||||
edges = get_graphql_sponsor_edges(settings=settings, after=last_edge.cursor) |
|
||||
|
|
||||
tiers: DefaultDict[float, Dict[str, SponsorEntity]] = defaultdict(dict) |
|
||||
for node in nodes: |
|
||||
tiers[node.tier.monthlyPriceInDollars][node.sponsorEntity.login] = ( |
|
||||
node.sponsorEntity |
|
||||
) |
|
||||
return tiers |
|
||||
|
|
||||
|
|
||||
def get_top_users( |
|
||||
*, |
|
||||
counter: Counter, |
|
||||
authors: Dict[str, Author], |
|
||||
skip_users: Container[str], |
|
||||
min_count: int = 2, |
|
||||
): |
|
||||
users = [] |
|
||||
for commenter, count in counter.most_common(50): |
|
||||
if commenter in skip_users: |
|
||||
continue |
|
||||
if count >= min_count: |
|
||||
author = authors[commenter] |
|
||||
users.append( |
|
||||
{ |
|
||||
"login": commenter, |
|
||||
"count": count, |
|
||||
"avatarUrl": author.avatarUrl, |
|
||||
"url": author.url, |
|
||||
} |
|
||||
) |
|
||||
return users |
|
||||
|
|
||||
|
|
||||
if __name__ == "__main__": |
|
||||
logging.basicConfig(level=logging.INFO) |
|
||||
settings = Settings() |
|
||||
logging.info(f"Using config: {settings.model_dump_json()}") |
|
||||
g = Github(settings.input_token.get_secret_value()) |
|
||||
repo = g.get_repo(settings.github_repository) |
|
||||
discussion_nodes = get_discussion_nodes(settings=settings) |
|
||||
experts_results = get_discussions_experts(discussion_nodes=discussion_nodes) |
|
||||
pr_nodes = get_pr_nodes(settings=settings) |
|
||||
contributors_results = get_contributors(pr_nodes=pr_nodes) |
|
||||
authors = {**experts_results.authors, **contributors_results.authors} |
|
||||
maintainers_logins = {"tiangolo"} |
|
||||
bot_names = {"codecov", "github-actions", "pre-commit-ci", "dependabot"} |
|
||||
maintainers = [] |
|
||||
for login in maintainers_logins: |
|
||||
user = authors[login] |
|
||||
maintainers.append( |
|
||||
{ |
|
||||
"login": login, |
|
||||
"answers": experts_results.commenters[login], |
|
||||
"prs": contributors_results.contributors[login], |
|
||||
"avatarUrl": user.avatarUrl, |
|
||||
"url": user.url, |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
skip_users = maintainers_logins | bot_names |
|
||||
experts = get_top_users( |
|
||||
counter=experts_results.commenters, |
|
||||
authors=authors, |
|
||||
skip_users=skip_users, |
|
||||
) |
|
||||
last_month_experts = get_top_users( |
|
||||
counter=experts_results.last_month_commenters, |
|
||||
authors=authors, |
|
||||
skip_users=skip_users, |
|
||||
) |
|
||||
three_months_experts = get_top_users( |
|
||||
counter=experts_results.three_months_commenters, |
|
||||
authors=authors, |
|
||||
skip_users=skip_users, |
|
||||
) |
|
||||
six_months_experts = get_top_users( |
|
||||
counter=experts_results.six_months_commenters, |
|
||||
authors=authors, |
|
||||
skip_users=skip_users, |
|
||||
) |
|
||||
one_year_experts = get_top_users( |
|
||||
counter=experts_results.one_year_commenters, |
|
||||
authors=authors, |
|
||||
skip_users=skip_users, |
|
||||
) |
|
||||
top_contributors = get_top_users( |
|
||||
counter=contributors_results.contributors, |
|
||||
authors=authors, |
|
||||
skip_users=skip_users, |
|
||||
) |
|
||||
top_reviewers = get_top_users( |
|
||||
counter=contributors_results.reviewers, |
|
||||
authors=authors, |
|
||||
skip_users=skip_users, |
|
||||
) |
|
||||
top_translations_reviewers = get_top_users( |
|
||||
counter=contributors_results.translation_reviewers, |
|
||||
authors=authors, |
|
||||
skip_users=skip_users, |
|
||||
) |
|
||||
|
|
||||
tiers = get_individual_sponsors(settings=settings) |
|
||||
keys = list(tiers.keys()) |
|
||||
keys.sort(reverse=True) |
|
||||
sponsors = [] |
|
||||
for key in keys: |
|
||||
sponsor_group = [] |
|
||||
for login, sponsor in tiers[key].items(): |
|
||||
sponsor_group.append( |
|
||||
{"login": login, "avatarUrl": sponsor.avatarUrl, "url": sponsor.url} |
|
||||
) |
|
||||
sponsors.append(sponsor_group) |
|
||||
|
|
||||
people = { |
|
||||
"maintainers": maintainers, |
|
||||
"experts": experts, |
|
||||
"last_month_experts": last_month_experts, |
|
||||
"three_months_experts": three_months_experts, |
|
||||
"six_months_experts": six_months_experts, |
|
||||
"one_year_experts": one_year_experts, |
|
||||
"top_contributors": top_contributors, |
|
||||
"top_reviewers": top_reviewers, |
|
||||
"top_translations_reviewers": top_translations_reviewers, |
|
||||
} |
|
||||
github_sponsors = { |
|
||||
"sponsors": sponsors, |
|
||||
} |
|
||||
# For local development |
|
||||
# people_path = Path("../../../../docs/en/data/people.yml") |
|
||||
people_path = Path("./docs/en/data/people.yml") |
|
||||
github_sponsors_path = Path("./docs/en/data/github_sponsors.yml") |
|
||||
people_old_content = people_path.read_text(encoding="utf-8") |
|
||||
github_sponsors_old_content = github_sponsors_path.read_text(encoding="utf-8") |
|
||||
new_people_content = yaml.dump( |
|
||||
people, sort_keys=False, width=200, allow_unicode=True |
|
||||
) |
|
||||
new_github_sponsors_content = yaml.dump( |
|
||||
github_sponsors, sort_keys=False, width=200, allow_unicode=True |
|
||||
) |
|
||||
if ( |
|
||||
people_old_content == new_people_content |
|
||||
and github_sponsors_old_content == new_github_sponsors_content |
|
||||
): |
|
||||
logging.info("The FastAPI People data hasn't changed, finishing.") |
|
||||
sys.exit(0) |
|
||||
people_path.write_text(new_people_content, encoding="utf-8") |
|
||||
github_sponsors_path.write_text(new_github_sponsors_content, encoding="utf-8") |
|
||||
logging.info("Setting up GitHub Actions git user") |
|
||||
subprocess.run(["git", "config", "user.name", "github-actions"], check=True) |
|
||||
subprocess.run( |
|
||||
["git", "config", "user.email", "[email protected]"], check=True |
|
||||
) |
|
||||
branch_name = "fastapi-people" |
|
||||
logging.info(f"Creating a new branch {branch_name}") |
|
||||
subprocess.run(["git", "checkout", "-b", branch_name], check=True) |
|
||||
logging.info("Adding updated file") |
|
||||
subprocess.run( |
|
||||
["git", "add", str(people_path), str(github_sponsors_path)], check=True |
|
||||
) |
|
||||
logging.info("Committing updated file") |
|
||||
message = "👥 Update FastAPI People" |
|
||||
result = subprocess.run(["git", "commit", "-m", message], check=True) |
|
||||
logging.info("Pushing branch") |
|
||||
subprocess.run(["git", "push", "origin", branch_name], check=True) |
|
||||
logging.info("Creating PR") |
|
||||
pr = repo.create_pull(title=message, body=message, base="master", head=branch_name) |
|
||||
logging.info(f"Created PR: {pr.number}") |
|
||||
logging.info("Finished") |
|
@ -35,7 +35,7 @@ jobs: |
|||||
TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} |
TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} |
||||
run: python -m build |
run: python -m build |
||||
- name: Publish |
- name: Publish |
||||
uses: pypa/[email protected].3 |
uses: pypa/[email protected].4 |
||||
- name: Dump GitHub context |
- name: Dump GitHub context |
||||
env: |
env: |
||||
GITHUB_CONTEXT: ${{ toJson(github) }} |
GITHUB_CONTEXT: ${{ toJson(github) }} |
||||
|
@ -0,0 +1,52 @@ |
|||||
|
name: FastAPI People Sponsors |
||||
|
|
||||
|
on: |
||||
|
schedule: |
||||
|
- cron: "0 6 1 * *" |
||||
|
workflow_dispatch: |
||||
|
inputs: |
||||
|
debug_enabled: |
||||
|
description: "Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)" |
||||
|
required: false |
||||
|
default: "false" |
||||
|
|
||||
|
env: |
||||
|
UV_SYSTEM_PYTHON: 1 |
||||
|
|
||||
|
jobs: |
||||
|
job: |
||||
|
if: github.repository_owner == 'fastapi' |
||||
|
runs-on: ubuntu-latest |
||||
|
permissions: |
||||
|
contents: write |
||||
|
steps: |
||||
|
- name: Dump GitHub context |
||||
|
env: |
||||
|
GITHUB_CONTEXT: ${{ toJson(github) }} |
||||
|
run: echo "$GITHUB_CONTEXT" |
||||
|
- uses: actions/checkout@v4 |
||||
|
- name: Set up Python |
||||
|
uses: actions/setup-python@v5 |
||||
|
with: |
||||
|
python-version: "3.11" |
||||
|
- name: Setup uv |
||||
|
uses: astral-sh/setup-uv@v5 |
||||
|
with: |
||||
|
version: "0.4.15" |
||||
|
enable-cache: true |
||||
|
cache-dependency-glob: | |
||||
|
requirements**.txt |
||||
|
pyproject.toml |
||||
|
- name: Install Dependencies |
||||
|
run: uv pip install -r requirements-github-actions.txt |
||||
|
# Allow debugging with tmate |
||||
|
- name: Setup tmate session |
||||
|
uses: mxschmitt/action-tmate@v3 |
||||
|
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} |
||||
|
with: |
||||
|
limit-access-to-actor: true |
||||
|
- name: FastAPI People Sponsors |
||||
|
run: python ./scripts/sponsors.py |
||||
|
env: |
||||
|
SPONSORS_TOKEN: ${{ secrets.SPONSORS_TOKEN }} |
||||
|
PR_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} |
File diff suppressed because it is too large
After Width: | Height: | Size: 38 KiB |
@ -0,0 +1,495 @@ |
|||||
|
# FastAPI |
||||
|
|
||||
|
<style> |
||||
|
.md-content .md-typeset h1 { display: none; } |
||||
|
</style> |
||||
|
|
||||
|
<p align="center"> |
||||
|
<a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> |
||||
|
</p> |
||||
|
<p align="center"> |
||||
|
<em>FastAPI, framework performa tinggi, mudah dipelajari, cepat untuk coding, siap untuk pengembangan</em> |
||||
|
</p> |
||||
|
<p align="center"> |
||||
|
<a href="https://github.com/fastapi/fastapi/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank"> |
||||
|
<img src="https://github.com/fastapi/fastapi/workflows/Test/badge.svg?event=push&branch=master" alt="Test"> |
||||
|
</a> |
||||
|
<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/fastapi" target="_blank"> |
||||
|
<img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/fastapi.svg" alt="Coverage"> |
||||
|
</a> |
||||
|
<a href="https://pypi.org/project/fastapi" target="_blank"> |
||||
|
<img src="https://img.shields.io/pypi/v/fastapi?color=%2334D058&label=pypi%20package" alt="Package version"> |
||||
|
</a> |
||||
|
<a href="https://pypi.org/project/fastapi" target="_blank"> |
||||
|
<img src="https://img.shields.io/pypi/pyversions/fastapi.svg?color=%2334D058" alt="Supported Python versions"> |
||||
|
</a> |
||||
|
</p> |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Dokumentasi**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a> |
||||
|
|
||||
|
**Kode Sumber**: <a href="https://github.com/fastapi/fastapi" target="_blank">https://github.com/fastapi/fastapi</a> |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
FastAPI adalah *framework* *web* moderen, cepat (performa-tinggi) untuk membangun API dengan Python berdasarkan tipe petunjuk Python. |
||||
|
|
||||
|
Fitur utama FastAPI: |
||||
|
|
||||
|
* **Cepat**: Performa sangat tinggi, setara **NodeJS** dan **Go** (berkat Starlette dan Pydantic). [Salah satu *framework* Python tercepat yang ada](#performa). |
||||
|
* **Cepat untuk coding**: Meningkatkan kecepatan pengembangan fitur dari 200% sampai 300%. * |
||||
|
* **Sedikit bug**: Mengurangi hingga 40% kesalahan dari manusia (pemrogram). * |
||||
|
* **Intuitif**: Dukungan editor hebat. <abbr title="juga dikenal otomatis-lengkap, pelengkapan otomatis, kecerdasan">Penyelesaian</abbr> di mana pun. Lebih sedikit *debugging*. |
||||
|
* **Mudah**: Dibuat mudah digunakan dan dipelajari. Sedikit waktu membaca dokumentasi. |
||||
|
* **Ringkas**: Mengurasi duplikasi kode. Beragam fitur dari setiap deklarasi parameter. Lebih sedikit *bug*. |
||||
|
* **Handal**: Dapatkan kode siap-digunakan. Dengan dokumentasi otomatis interaktif. |
||||
|
* **Standar-resmi**: Berdasarkan (kompatibel dengan ) standar umum untuk API: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (sebelumnya disebut Swagger) dan <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. |
||||
|
|
||||
|
<small>* estimasi berdasarkan pengujian tim internal pengembangan applikasi siap pakai.</small> |
||||
|
|
||||
|
## Sponsor |
||||
|
|
||||
|
<!-- sponsors --> |
||||
|
|
||||
|
{% if sponsors %} |
||||
|
{% for sponsor in sponsors.gold -%} |
||||
|
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> |
||||
|
{% endfor -%} |
||||
|
{%- for sponsor in sponsors.silver -%} |
||||
|
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> |
||||
|
{% endfor %} |
||||
|
{% endif %} |
||||
|
|
||||
|
<!-- /sponsors --> |
||||
|
|
||||
|
<a href="https://fastapi.tiangolo.com/fastapi-people/#sponsors" class="external-link" target="_blank">Sponsor lainnya</a> |
||||
|
|
||||
|
## Opini |
||||
|
|
||||
|
"_[...] Saya banyak menggunakan **FastAPI** sekarang ini. [...] Saya berencana menggunakannya di semua tim servis ML Microsoft. Beberapa dari mereka sudah mengintegrasikan dengan produk inti *Windows** dan sebagian produk **Office**._" |
||||
|
|
||||
|
<div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/fastapi/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div> |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
"_Kami adopsi library **FastAPI** untuk membuat server **REST** yang melakukan kueri untuk menghasilkan **prediksi**. [untuk Ludwig]_" |
||||
|
|
||||
|
<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div> |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
"_**Netflix** dengan bangga mengumumkan rilis open-source orkestrasi framework **manajemen krisis** : **Dispatch**! [dibuat dengan **FastAPI**]_" |
||||
|
|
||||
|
<div style="text-align: right; margin-right: 10%;">Kevin Glisson, Marc Vilanova, Forest Monsen - <strong>Netflix</strong> <a href="https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072" target="_blank"><small>(ref)</small></a></div> |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
"_Saya sangat senang dengan **FastAPI**. Sangat menyenangkan!_" |
||||
|
|
||||
|
<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> podcast host</strong> <a href="https://twitter.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div> |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
"_Jujur, apa yang anda buat sangat solid dan berkualitas. Ini adalah yang saya inginkan di **Hug** - sangat menginspirasi melihat seseorang membuat ini._" |
||||
|
|
||||
|
<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://github.com/hugapi/hug" target="_blank">Hug</a> creator</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
"_Jika anda ingin mempelajari **framework moderen** untuk membangun REST API, coba **FastAPI** [...] cepat, mudah digunakan dan dipelajari [...]_" |
||||
|
|
||||
|
"_Kami sudah pindah ke **FastAPI** untuk **API** kami [...] Saya pikir kamu juga akan suka [...]_" |
||||
|
|
||||
|
<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div> |
||||
|
|
||||
|
--- |
||||
|
"_Jika anda ingin membuat API Python siap pakai, saya merekomendasikan **FastAPI**. FastAPI **didesain indah**, **mudah digunakan** dan **sangat scalable**, FastAPI adalah **komponen kunci** di strategi pengembangan API pertama kami dan mengatur banyak otomatisasi dan service seperti TAC Engineer kami._" |
||||
|
|
||||
|
<div style="text-align: right; margin-right: 10%;">Deon Pillsbury - <strong>Cisco</strong> <a href="https://www.linkedin.com/posts/deonpillsbury_cisco-cx-python-activity-6963242628536487936-trAp/" target="_blank"><small>(ref)</small></a></div> |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## **Typer**, CLI FastAPI |
||||
|
|
||||
|
<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> |
||||
|
|
||||
|
Jika anda mengembangkan app <abbr title="Command Line Interface">CLI</abbr> yang digunakan di terminal bukan sebagai API web, kunjungi <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>. |
||||
|
|
||||
|
**Typer** adalah saudara kecil FastAPI. Dan ditujukan sebagai **CLI FastAPI**. ⌨️ 🚀 |
||||
|
|
||||
|
## Prayarat |
||||
|
|
||||
|
FastAPI berdiri di pundak raksasa: |
||||
|
|
||||
|
* <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a> untuk bagian web. |
||||
|
* <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> untuk bagian data. |
||||
|
|
||||
|
## Instalasi |
||||
|
|
||||
|
Buat dan aktifkan <a href="https://fastapi.tiangolo.com/virtual-environments/" class="external-link" target="_blank">virtual environment</a> kemudian *install* FastAPI: |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
$ pip install "fastapi[standard]" |
||||
|
|
||||
|
---> 100% |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
**Catatan**: Pastikan anda menulis `"fastapi[standard]"` dengan tanda petik untuk memastikan bisa digunakan di semua *terminal*. |
||||
|
|
||||
|
## Contoh |
||||
|
|
||||
|
### Buat app |
||||
|
|
||||
|
* Buat file `main.py` dengan: |
||||
|
|
||||
|
```Python |
||||
|
from typing import Union |
||||
|
|
||||
|
from fastapi import FastAPI |
||||
|
|
||||
|
app = FastAPI() |
||||
|
|
||||
|
|
||||
|
@app.get("/") |
||||
|
def read_root(): |
||||
|
return {"Hello": "World"} |
||||
|
|
||||
|
|
||||
|
@app.get("/items/{item_id}") |
||||
|
def read_item(item_id: int, q: Union[str, None] = None): |
||||
|
return {"item_id": item_id, "q": q} |
||||
|
``` |
||||
|
|
||||
|
<details markdown="1"> |
||||
|
<summary>Atau gunakan <code>async def</code>...</summary> |
||||
|
|
||||
|
Jika kode anda menggunakan `async` / `await`, gunakan `async def`: |
||||
|
|
||||
|
```Python hl_lines="9 14" |
||||
|
from typing import Union |
||||
|
|
||||
|
from fastapi import FastAPI |
||||
|
|
||||
|
app = FastAPI() |
||||
|
|
||||
|
|
||||
|
@app.get("/") |
||||
|
async def read_root(): |
||||
|
return {"Hello": "World"} |
||||
|
|
||||
|
|
||||
|
@app.get("/items/{item_id}") |
||||
|
async def read_item(item_id: int, q: Union[str, None] = None): |
||||
|
return {"item_id": item_id, "q": q} |
||||
|
``` |
||||
|
|
||||
|
**Catatan**: |
||||
|
|
||||
|
Jika anda tidak paham, kunjungi _"Panduan cepat"_ bagian <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` dan `await` di dokumentasi</a>. |
||||
|
|
||||
|
</details> |
||||
|
|
||||
|
### Jalankan |
||||
|
|
||||
|
Jalankan *server* dengan: |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
$ fastapi dev main.py |
||||
|
|
||||
|
╭────────── FastAPI CLI - Development mode ───────────╮ |
||||
|
│ │ |
||||
|
│ Serving at: http://127.0.0.1:8000 │ |
||||
|
│ │ |
||||
|
│ API docs: http://127.0.0.1:8000/docs │ |
||||
|
│ │ |
||||
|
│ Running in development mode, for production use: │ |
||||
|
│ │ |
||||
|
│ fastapi run │ |
||||
|
│ │ |
||||
|
╰─────────────────────────────────────────────────────╯ |
||||
|
|
||||
|
INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] |
||||
|
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
||||
|
INFO: Started reloader process [2248755] using WatchFiles |
||||
|
INFO: Started server process [2248757] |
||||
|
INFO: Waiting for application startup. |
||||
|
INFO: Application startup complete. |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<details markdown="1"> |
||||
|
<summary>Mengenai perintah <code>fastapi dev main.py</code>...</summary> |
||||
|
|
||||
|
Perintah `fastapi dev` membaca file `main.py`, memeriksa app **FastAPI** di dalamnya, dan menjalan server dengan <a href="https://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a>. |
||||
|
|
||||
|
Secara otomatis, `fastapi dev` akan mengaktifkan *auto-reload* untuk pengembangan lokal. |
||||
|
|
||||
|
Informasi lebih lanjut kunjungi <a href="https://fastapi.tiangolo.com/fastapi-cli/" target="_blank">Dokumen FastAPI CLI</a>. |
||||
|
|
||||
|
</details> |
||||
|
|
||||
|
### Periksa |
||||
|
|
||||
|
Buka *browser* di <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>. |
||||
|
|
||||
|
Anda akan melihat respon JSON berikut: |
||||
|
|
||||
|
```JSON |
||||
|
{"item_id": 5, "q": "somequery"} |
||||
|
``` |
||||
|
|
||||
|
Anda telah membuat API: |
||||
|
|
||||
|
* Menerima permintaan HTTP di _path_ `/` dan `/items/{item_id}`. |
||||
|
* Kedua _paths_ menerima <em>operasi</em> `GET` (juga disebut _metode_ HTTP). |
||||
|
* _path_ `/items/{item_id}` memiliki _parameter path_ `item_id` yang harus berjenis `int`. |
||||
|
* _path_ `/items/{item_id}` memiliki _query parameter_ `q` berjenis `str`. |
||||
|
|
||||
|
### Dokumentasi API interaktif |
||||
|
|
||||
|
Sekarang kunjungi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. |
||||
|
|
||||
|
Anda akan melihat dokumentasi API interaktif otomatis (dibuat oleh <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>): |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Dokumentasi API alternatif |
||||
|
|
||||
|
Kemudian kunjungi <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. |
||||
|
|
||||
|
Anda akan melihat dokumentasi alternatif otomatis (dibuat oleh <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>): |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Contoh upgrade |
||||
|
|
||||
|
Sekarang ubah `main.py` untuk menerima struktur permintaan `PUT`. |
||||
|
|
||||
|
Deklarasikan struktur menggunakan tipe standar Python, berkat Pydantic. |
||||
|
|
||||
|
```Python hl_lines="4 9-12 25-27" |
||||
|
from typing import Union |
||||
|
|
||||
|
from fastapi import FastAPI |
||||
|
from pydantic import BaseModel |
||||
|
|
||||
|
app = FastAPI() |
||||
|
|
||||
|
|
||||
|
class Item(BaseModel): |
||||
|
name: str |
||||
|
price: float |
||||
|
is_offer: Union[bool, None] = None |
||||
|
|
||||
|
|
||||
|
@app.get("/") |
||||
|
def read_root(): |
||||
|
return {"Hello": "World"} |
||||
|
|
||||
|
|
||||
|
@app.get("/items/{item_id}") |
||||
|
def read_item(item_id: int, q: Union[str, None] = None): |
||||
|
return {"item_id": item_id, "q": q} |
||||
|
|
||||
|
|
||||
|
@app.put("/items/{item_id}") |
||||
|
def update_item(item_id: int, item: Item): |
||||
|
return {"item_name": item.name, "item_id": item_id} |
||||
|
``` |
||||
|
|
||||
|
Server `fastapi dev` akan otomatis memuat kembali. |
||||
|
|
||||
|
### Upgrade dokumentasi API interaktif |
||||
|
|
||||
|
Kunjungi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. |
||||
|
|
||||
|
* Dokumentasi API interaktif akan otomatis diperbarui, termasuk kode yang baru: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
* Klik tombol "Try it out", anda dapat mengisi parameter dan langsung berinteraksi dengan API: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
* Kemudian klik tombol "Execute", tampilan pengguna akan berkomunikasi dengan API, mengirim parameter, mendapatkan dan menampilkan hasil ke layar: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Upgrade dokumentasi API alternatif |
||||
|
|
||||
|
Kunjungi <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. |
||||
|
|
||||
|
* Dokumentasi alternatif akan menampilkan parameter *query* dan struktur *request*: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Ringkasan |
||||
|
|
||||
|
Singkatnya, anda mendeklarasikan **sekali** jenis parameter, struktur, dll. sebagai parameter fungsi. |
||||
|
|
||||
|
Anda melakukannya dengan tipe standar moderen Python. |
||||
|
|
||||
|
Anda tidak perlu belajar sintaksis, metode, *classs* baru dari *library* tertentu, dll. |
||||
|
|
||||
|
Cukup **Python** standar. |
||||
|
|
||||
|
Sebagai contoh untuk `int`: |
||||
|
|
||||
|
```Python |
||||
|
item_id: int |
||||
|
``` |
||||
|
|
||||
|
atau untuk model lebih rumit `Item`: |
||||
|
|
||||
|
```Python |
||||
|
item: Item |
||||
|
``` |
||||
|
|
||||
|
...dengan sekali deklarasi anda mendapatkan: |
||||
|
|
||||
|
* Dukungan editor, termasuk: |
||||
|
* Pelengkapan kode. |
||||
|
* Pengecekan tipe. |
||||
|
* Validasi data: |
||||
|
* Kesalahan otomatis dan jelas ketika data tidak sesuai. |
||||
|
* Validasi hingga untuk object JSON bercabang mendalam. |
||||
|
* <abbr title="juga disebut: serialization, parsing, marshalling">Konversi</abbr> input data: berasal dari jaringan ke data dan tipe Python. Membaca dari: |
||||
|
* JSON. |
||||
|
* Parameter path. |
||||
|
* Parameter query. |
||||
|
* Cookie. |
||||
|
* Header. |
||||
|
* Form. |
||||
|
* File. |
||||
|
* <abbr title="juga disebut: serialization, parsing, marshalling">Konversi</abbr> output data: konversi data Python ke tipe jaringan data (seperti JSON): |
||||
|
* Konversi tipe Python (`str`, `int`, `float`, `bool`, `list`, dll). |
||||
|
* Objek `datetime`. |
||||
|
* Objek `UUID`. |
||||
|
* Model database. |
||||
|
* ...dan banyak lagi. |
||||
|
* Dokumentasi interaktif otomatis, termasuk 2 alternatif tampilan pengguna: |
||||
|
* Swagger UI. |
||||
|
* ReDoc. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
Kembali ke kode contoh sebelumnya, **FastAPI** akan: |
||||
|
|
||||
|
* Validasi apakah terdapat `item_id` di *path* untuk permintaan `GET` dan `PUT` requests. |
||||
|
* Validasi apakah `item_id` berjenit `int` untuk permintaan `GET` dan `PUT`. |
||||
|
* Jika tidak, klien akan melihat pesan kesalahan jelas. |
||||
|
* Periksa jika ada parameter *query* opsional bernama `q` (seperti `http://127.0.0.1:8000/items/foo?q=somequery`) untuk permintaan `GET`. |
||||
|
* Karena parameter `q` dideklarasikan dengan `= None`, maka bersifat opsional. |
||||
|
* Tanpa `None` maka akan menjadi wajib ada (seperti struktur di kondisi dengan `PUT`). |
||||
|
* Untuk permintaan `PUT` `/items/{item_id}`, membaca struktur sebagai JSON: |
||||
|
* Memeriksa terdapat atribut wajib `name` harus berjenis `str`. |
||||
|
* Memeriksa terdapat atribut wajib`price` harus berjenis `float`. |
||||
|
* Memeriksa atribut opsional `is_offer`, harus berjenis `bool`, jika ada. |
||||
|
* Semua ini juga sama untuk objek json yang bersarang mendalam. |
||||
|
* Konversi dari dan ke JSON secara otomatis. |
||||
|
* Dokumentasi segalanya dengan OpenAPI, dengan menggunakan: |
||||
|
* Sistem dokumentasi interaktif. |
||||
|
* Sistem otomatis penghasil kode, untuk banyak bahasa. |
||||
|
* Menyediakan 2 tampilan dokumentasi web interaktif dengan langsung. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
Kita baru menyentuh permukaannya saja, tetapi anda sudah mulai paham gambaran besar cara kerjanya. |
||||
|
|
||||
|
Coba ubah baris: |
||||
|
|
||||
|
```Python |
||||
|
return {"item_name": item.name, "item_id": item_id} |
||||
|
``` |
||||
|
|
||||
|
...dari: |
||||
|
|
||||
|
```Python |
||||
|
... "item_name": item.name ... |
||||
|
``` |
||||
|
|
||||
|
...menjadi: |
||||
|
|
||||
|
```Python |
||||
|
... "item_price": item.price ... |
||||
|
``` |
||||
|
|
||||
|
...anda akan melihat kode editor secara otomatis melengkapi atributnya dan tahu tipe nya: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Untuk contoh lengkap termasuk fitur lainnya, kunjungi <a href="https://fastapi.tiangolo.com/tutorial/">Tutorial - Panduan Pengguna</a>. |
||||
|
|
||||
|
**Peringatan spoiler**: tutorial - panduan pengguna termasuk: |
||||
|
|
||||
|
* Deklarasi **parameter** dari tempat berbeda seperti: **header**, **cookie**, **form field** and **file**. |
||||
|
* Bagaimana mengatur **batasan validasi** seperti `maximum_length`atau `regex`. |
||||
|
* Sistem **<abbr title="also known as components, resources, providers, services, injectables">Dependency Injection</abbr>** yang hebat dan mudah digunakan. |
||||
|
* Keamanan dan autentikasi, termasuk dukungan ke **OAuth2** dengan **JWT token** dan autentikasi **HTTP Basic**. |
||||
|
* Teknik lebih aju (tetapi mudah dipakai untuk deklarasi **model JSON bersarang ke dalam** (berkat Pydantic). |
||||
|
* Integrasi **GraphQL** dengan <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a> dan library lainnya. |
||||
|
* Fitur lainnya (berkat Starlette) seperti: |
||||
|
* **WebSocket** |
||||
|
* Test yang sangat mudah berdasarkan HTTPX dan `pytest` |
||||
|
* **CORS** |
||||
|
* **Cookie Session** |
||||
|
* ...dan lainnya. |
||||
|
|
||||
|
## Performa |
||||
|
|
||||
|
Tolok ukur Independent TechEmpower mendapati aplikasi **FastAPI** berjalan menggunakan Uvicorn sebagai <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">salah satu framework Python tercepat yang ada</a>, hanya di bawah Starlette dan Uvicorn itu sendiri (digunakan di internal FastAPI). (*) |
||||
|
|
||||
|
Penjelasan lebih lanjut, lihat bagian <a href="https://fastapi.tiangolo.com/benchmarks/" class="internal-link" target="_blank">Tolok ukur</a>. |
||||
|
|
||||
|
## Dependensi |
||||
|
|
||||
|
FastAPI bergantung pada Pydantic dan Starlette. |
||||
|
|
||||
|
### Dependensi `standar` |
||||
|
|
||||
|
Ketika anda meng-*install* FastAPI dengan `pip install "fastapi[standard]"`, maka FastAPI akan menggunakan sekumpulan dependensi opsional `standar`: |
||||
|
|
||||
|
Digunakan oleh Pydantic: |
||||
|
|
||||
|
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email-validator</code></a> - untuk validasi email. |
||||
|
|
||||
|
Digunakan oleh Starlette: |
||||
|
|
||||
|
* <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - Dibutuhkan jika anda menggunakan `TestClient`. |
||||
|
* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - Dibutuhkan jika anda menggunakan konfigurasi template bawaan. |
||||
|
* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - Dibutuhkan jika anda menggunakan form dukungan <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>, dengan `request.form()`. |
||||
|
|
||||
|
Digunakan oleh FastAPI / Starlette: |
||||
|
|
||||
|
* <a href="https://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - untuk server yang memuat dan melayani aplikasi anda. Termasuk `uvicorn[standard]`, yang memasukan sejumlah dependensi (misal `uvloop`) untuk needed melayani dengan performa tinggi. |
||||
|
* `fastapi-cli` - untuk menyediakan perintah `fastapi`. |
||||
|
|
||||
|
### Tanpda dependensi `standard` |
||||
|
|
||||
|
Jika anda tidak ingin menambahkan dependensi opsional `standard`, anda dapat menggunakan `pip install fastapi` daripada `pip install "fastapi[standard]"`. |
||||
|
|
||||
|
### Dependensi Opsional Tambahan |
||||
|
|
||||
|
Ada beberapa dependensi opsional yang bisa anda install. |
||||
|
|
||||
|
Dependensi opsional tambahan Pydantic: |
||||
|
|
||||
|
* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - untuk manajemen setting. |
||||
|
* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - untuk tipe tambahan yang digunakan dengan Pydantic. |
||||
|
|
||||
|
Dependensi tambahan opsional FastAPI: |
||||
|
|
||||
|
* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - Diperlukan jika anda akan menggunakan`ORJSONResponse`. |
||||
|
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - Diperlukan jika anda akan menggunakan `UJSONResponse`. |
||||
|
|
||||
|
## Lisensi |
||||
|
|
||||
|
Project terlisensi dengan lisensi MIT. |
@ -0,0 +1,40 @@ |
|||||
|
# Berkas Statis |
||||
|
|
||||
|
Anda dapat menyajikan berkas statis secara otomatis dari sebuah direktori menggunakan `StaticFiles`. |
||||
|
|
||||
|
## Penggunaan `StaticFiles` |
||||
|
|
||||
|
* Mengimpor `StaticFiles`. |
||||
|
* "Mount" representatif `StaticFiles()` di jalur spesifik. |
||||
|
|
||||
|
{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} |
||||
|
|
||||
|
/// note | Detail Teknis |
||||
|
|
||||
|
Anda dapat pula menggunakan `from starlette.staticfiles import StaticFiles`. |
||||
|
|
||||
|
**FastAPI** menyediakan `starlette.staticfiles` sama seperti `fastapi.staticfiles` sebagai kemudahan pada Anda, yaitu para pengembang. Tetapi ini asli berasal langsung dari Starlette. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
### Apa itu "Mounting" |
||||
|
|
||||
|
"Mounting" dimaksud menambah aplikasi "independen" secara lengkap di jalur spesifik, kemudian menangani seluruh sub-jalur. |
||||
|
|
||||
|
Hal ini berbeda dari menggunakan `APIRouter` karena aplikasi yang dimount benar-benar independen. OpenAPI dan dokumentasi dari aplikasi utama Anda tak akan menyertakan apa pun dari aplikasi yang dimount, dst. |
||||
|
|
||||
|
Anda dapat mempelajari mengenai ini dalam [Panduan Pengguna Lanjutan](../advanced/index.md){.internal-link target=_blank}. |
||||
|
|
||||
|
## Detail |
||||
|
|
||||
|
Terhadap `"/static"` pertama mengacu pada sub-jalur yang akan menjadi tempat "sub-aplikasi" ini akan "dimount". Maka, jalur apa pun yang dimulai dengan `"/static"` akan ditangani oleh sub-jalur tersebut. |
||||
|
|
||||
|
Terhadap `directory="static"` mengacu pada nama direktori yang berisi berkas statis Anda. |
||||
|
|
||||
|
Terhadap `name="static"` ialah nama yang dapat digunakan secara internal oleh **FastAPI**. |
||||
|
|
||||
|
Seluruh parameter ini dapat berbeda dari sekadar "`static`", sesuaikan parameter dengan keperluan dan detail spesifik akan aplikasi Anda. |
||||
|
|
||||
|
## Info lanjutan |
||||
|
|
||||
|
Sebagai detail dan opsi tambahan lihat <a href="https://www.starlette.io/staticfiles/" class="external-link" target="_blank">dokumentasi Starlette perihal Berkas Statis</a>. |
@ -0,0 +1,301 @@ |
|||||
|
# 環境変数 |
||||
|
|
||||
|
/// tip |
||||
|
|
||||
|
もし、「環境変数」とは何か、それをどう使うかを既に知っている場合は、このセクションをスキップして構いません。 |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
環境変数(**env var**とも呼ばれる)はPythonコードの**外側**、つまり**OS**に存在する変数で、Pythonから読み取ることができます。(他のプログラムでも同様に読み取れます。) |
||||
|
|
||||
|
環境変数は、アプリケーションの**設定**の管理や、Pythonの**インストール**などに役立ちます。 |
||||
|
|
||||
|
## 環境変数の作成と使用 |
||||
|
|
||||
|
環境変数は**シェル(ターミナル)**内で**作成**して使用でき、それらにPythonは不要です。 |
||||
|
|
||||
|
//// tab | Linux, macOS, Windows Bash |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
// You could create an env var MY_NAME with |
||||
|
$ export MY_NAME="Wade Wilson" |
||||
|
|
||||
|
// Then you could use it with other programs, like |
||||
|
$ echo "Hello $MY_NAME" |
||||
|
|
||||
|
Hello Wade Wilson |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
//// |
||||
|
|
||||
|
//// tab | Windows PowerShell |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
|
||||
|
```console |
||||
|
// Create an env var MY_NAME |
||||
|
$ $Env:MY_NAME = "Wade Wilson" |
||||
|
|
||||
|
// Use it with other programs, like |
||||
|
$ echo "Hello $Env:MY_NAME" |
||||
|
|
||||
|
Hello Wade Wilson |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
//// |
||||
|
|
||||
|
## Pythonで環境変数を読み取る |
||||
|
|
||||
|
環境変数をPythonの**外側**、ターミナル(や他の方法)で作成し、**Python内で読み取る**こともできます。 |
||||
|
|
||||
|
例えば、以下のような`main.py`ファイルを用意します: |
||||
|
|
||||
|
```Python hl_lines="3" |
||||
|
import os |
||||
|
|
||||
|
name = os.getenv("MY_NAME", "World") |
||||
|
print(f"Hello {name} from Python") |
||||
|
``` |
||||
|
|
||||
|
/// tip |
||||
|
|
||||
|
<a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> の第2引数は、デフォルトで返される値を指定します。 |
||||
|
|
||||
|
この引数を省略するとデフォルト値として`None`が返されますが、ここではデフォルト値として`"World"`を指定しています。 |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
次に、このPythonプログラムを呼び出します。 |
||||
|
|
||||
|
//// tab | Linux, macOS, Windows Bash |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
// Here we don't set the env var yet |
||||
|
$ python main.py |
||||
|
|
||||
|
// As we didn't set the env var, we get the default value |
||||
|
|
||||
|
Hello World from Python |
||||
|
|
||||
|
// But if we create an environment variable first |
||||
|
$ export MY_NAME="Wade Wilson" |
||||
|
|
||||
|
// And then call the program again |
||||
|
$ python main.py |
||||
|
|
||||
|
// Now it can read the environment variable |
||||
|
|
||||
|
Hello Wade Wilson from Python |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
//// |
||||
|
|
||||
|
//// tab | Windows PowerShell |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
// Here we don't set the env var yet |
||||
|
$ python main.py |
||||
|
|
||||
|
// As we didn't set the env var, we get the default value |
||||
|
|
||||
|
Hello World from Python |
||||
|
|
||||
|
// But if we create an environment variable first |
||||
|
$ $Env:MY_NAME = "Wade Wilson" |
||||
|
|
||||
|
// And then call the program again |
||||
|
$ python main.py |
||||
|
|
||||
|
// Now it can read the environment variable |
||||
|
|
||||
|
Hello Wade Wilson from Python |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
//// |
||||
|
|
||||
|
環境変数はコードの外側で設定し、内側から読み取ることができるので、他のファイルと一緒に(`git`に)保存する必要がありません。そのため、環境変数をコンフィグレーションや**設定**に使用することが一般的です。 |
||||
|
|
||||
|
また、**特定のプログラムの呼び出し**のための環境変数を、そのプログラムのみ、その実行中に限定して利用できるよう作成できます。 |
||||
|
|
||||
|
そのためには、プログラム起動コマンドと同じコマンドライン上の、起動コマンド直前で環境変数を作成してください。 |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
// Create an env var MY_NAME in line for this program call |
||||
|
$ MY_NAME="Wade Wilson" python main.py |
||||
|
|
||||
|
// Now it can read the environment variable |
||||
|
|
||||
|
Hello Wade Wilson from Python |
||||
|
|
||||
|
// The env var no longer exists afterwards |
||||
|
$ python main.py |
||||
|
|
||||
|
Hello World from Python |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
/// tip |
||||
|
|
||||
|
詳しくは <a href="https://12factor.net/config" class="external-link" target="_blank">The Twelve-Factor App: Config</a> を参照してください。 |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## 型とバリデーション |
||||
|
|
||||
|
環境変数は**テキスト文字列**のみを扱うことができます。これは、環境変数がPython外部に存在し、他のプログラムやシステム全体(Linux、Windows、macOS間の互換性を含む)と連携する必要があるためです。 |
||||
|
|
||||
|
つまり、Pythonが環境変数から読み取る**あらゆる値**は **`str`型となり**、他の型への変換やバリデーションはコード内で行う必要があります。 |
||||
|
|
||||
|
環境変数を使用して**アプリケーション設定**を管理する方法については、[高度なユーザーガイド - Settings and Environment Variables](./advanced/settings.md){.internal-link target=_blank}で詳しく学べます。 |
||||
|
|
||||
|
## `PATH`環境変数 |
||||
|
|
||||
|
**`PATH`**という**特別な**環境変数があります。この環境変数は、OS(Linux、macOS、Windows)が実行するプログラムを発見するために使用されます。 |
||||
|
|
||||
|
`PATH`変数は、複数のディレクトリのパスから成る長い文字列です。このパスはLinuxやMacOSの場合は`:`で、Windowsの場合は`;`で区切られています。 |
||||
|
|
||||
|
例えば、`PATH`環境変数は次のような文字列かもしれません: |
||||
|
|
||||
|
//// tab | Linux, macOS |
||||
|
|
||||
|
```plaintext |
||||
|
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin |
||||
|
``` |
||||
|
|
||||
|
これは、OSはプログラムを見つけるために以下のディレクトリを探す、ということを意味します: |
||||
|
|
||||
|
* `/usr/local/bin` |
||||
|
* `/usr/bin` |
||||
|
* `/bin` |
||||
|
* `/usr/sbin` |
||||
|
* `/sbin` |
||||
|
|
||||
|
//// |
||||
|
|
||||
|
//// tab | Windows |
||||
|
|
||||
|
```plaintext |
||||
|
C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 |
||||
|
``` |
||||
|
|
||||
|
これは、OSはプログラムを見つけるために以下のディレクトリを探す、ということを意味します: |
||||
|
|
||||
|
* `C:\Program Files\Python312\Scripts` |
||||
|
* `C:\Program Files\Python312` |
||||
|
* `C:\Windows\System32` |
||||
|
|
||||
|
//// |
||||
|
|
||||
|
ターミナル上で**コマンド**を入力すると、 OSはそのプログラムを見つけるために、`PATH`環境変数のリストに記載された**それぞれのディレクトリを探し**ます。 |
||||
|
|
||||
|
例えば、ターミナル上で`python`を入力すると、OSは`python`によって呼ばれるプログラムを見つけるために、そのリストの**先頭のディレクトリ**を最初に探します。 |
||||
|
|
||||
|
OSは、もしそのプログラムをそこで発見すれば**実行し**ますが、そうでなければリストの**他のディレクトリ**を探していきます。 |
||||
|
|
||||
|
### PythonのインストールとPATH環境変数の更新 |
||||
|
|
||||
|
Pythonのインストール時に`PATH`環境変数を更新したいか聞かれるかもしれません。 |
||||
|
|
||||
|
/// tab | Linux, macOS |
||||
|
|
||||
|
Pythonをインストールして、そのプログラムが`/opt/custompython/bin`というディレクトリに配置されたとします。 |
||||
|
|
||||
|
もし、`PATH`環境変数を更新するように答えると、`PATH`環境変数に`/opt/custompython/bin`が追加されます。 |
||||
|
|
||||
|
`PATH`環境変数は以下のように更新されるでしょう: |
||||
|
|
||||
|
``` plaintext |
||||
|
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin |
||||
|
``` |
||||
|
|
||||
|
このようにして、ターミナルで`python`と入力したときに、OSは`/opt/custompython/bin`(リストの末尾のディレクトリ)にあるPythonプログラムを見つけ、使用します。 |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
/// tab | Windows |
||||
|
|
||||
|
Pythonをインストールして、そのプログラムが`C:\opt\custompython\bin`というディレクトリに配置されたとします。 |
||||
|
|
||||
|
もし、`PATH`環境変数を更新するように答えると、`PATH`環境変数に`C:\opt\custompython\bin`が追加されます。 |
||||
|
|
||||
|
`PATH`環境変数は以下のように更新されるでしょう: |
||||
|
|
||||
|
```plaintext |
||||
|
C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin |
||||
|
``` |
||||
|
|
||||
|
このようにして、ターミナルで`python`と入力したときに、OSは`C:\opt\custompython\bin\python`(リストの末尾のディレクトリ)にあるPythonプログラムを見つけ、使用します。 |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
つまり、ターミナルで以下のコマンドを入力すると: |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
``` console |
||||
|
$ python |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
/// tab | Linux, macOS |
||||
|
|
||||
|
OSは`/opt/custompython/bin`にある`python`プログラムを**見つけ**て実行します。 |
||||
|
|
||||
|
これは、次のコマンドを入力した場合とほとんど同等です: |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
$ /opt/custompython/bin/python |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
/// tab | Windows |
||||
|
|
||||
|
OSは`C:\opt\custompython\bin\python`にある`python`プログラムを**見つけ**て実行します。 |
||||
|
|
||||
|
これは、次のコマンドを入力した場合とほとんど同等です: |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
$ C:\opt\custompython\bin\python |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
この情報は、[Virtual Environments](virtual-environments.md) について学ぶ際にも役立ちます。 |
||||
|
|
||||
|
## まとめ |
||||
|
|
||||
|
これで、**環境変数**とは何か、Pythonでどのように使用するかについて、基本的な理解が得られたはずです。 |
||||
|
|
||||
|
環境変数についての詳細は、<a href="https://en.wikipedia.org/wiki/Environment_variable" class="external-link" target="_blank">Wikipedia: Environment Variable</a> を参照してください。 |
||||
|
|
||||
|
環境変数の用途や適用方法が最初は直感的ではないかもしれませんが、開発中のさまざまなシナリオで繰り返し登場します。そのため、基本を知っておくことが重要です。 |
||||
|
|
||||
|
たとえば、この情報は次のセクションで扱う[Virtual Environments](virtual-environments.md)にも関連します。 |
@ -1,34 +0,0 @@ |
|||||
# Benchmarks |
|
||||
|
|
||||
Benchmarks independentes da TechEmpower mostram que aplicações **FastAPI** rodando com o Uvicorn como <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">um dos frameworks Python mais rápidos disponíveis</a>, ficando atrás apenas do Starlette e Uvicorn (utilizado internamente pelo FastAPI). |
|
||||
|
|
||||
Porém, ao verificar benchmarks e comparações você deve prestar atenção ao seguinte: |
|
||||
|
|
||||
## Benchmarks e velocidade |
|
||||
|
|
||||
Quando você verifica os benchmarks, é comum ver diversas ferramentas de diferentes tipos comparados como se fossem equivalentes. |
|
||||
|
|
||||
Especificamente, para ver o Uvicorn, Starlette e FastAPI comparados entre si (entre diversas outras ferramentas). |
|
||||
|
|
||||
Quanto mais simples o problema resolvido pela ferramenta, melhor será a performance. E a maioria das análises não testa funcionalidades adicionais que são oferecidas pela ferramenta. |
|
||||
|
|
||||
A hierarquia é: |
|
||||
|
|
||||
* **Uvicorn**: um servidor ASGI |
|
||||
* **Starlette**: (utiliza Uvicorn) um microframework web |
|
||||
* **FastAPI**: (utiliza Starlette) um microframework para APIs com diversas funcionalidades adicionais para a construção de APIs, com validação de dados, etc. |
|
||||
|
|
||||
* **Uvicorn**: |
|
||||
* Terá a melhor performance, pois não possui muito código além do próprio servidor. |
|
||||
* Você não escreveria uma aplicação utilizando o Uvicorn diretamente. Isso significaria que o seu código teria que incluir pelo menos todo o código fornecido pelo Starlette (ou o **FastAPI**). E caso você fizesse isso, a sua aplicação final teria a mesma sobrecarga que teria se utilizasse um framework, minimizando o código e os bugs. |
|
||||
* Se você está comparando o Uvicorn, compare com os servidores de aplicação Daphne, Hypercorn, uWSGI, etc. |
|
||||
* **Starlette**: |
|
||||
* Terá o melhor desempenho, depois do Uvicorn. Na verdade, o Starlette utiliza o Uvicorn para rodar. Portanto, ele pode ficar mais "devagar" que o Uvicorn apenas por ter que executar mais código. |
|
||||
* Mas ele fornece as ferramentas para construir aplicações web simples, com roteamento baseado em caminhos, etc. |
|
||||
* Se você está comparando o Starlette, compare-o com o Sanic, Flask, Django, etc. Frameworks web (ou microframeworks). |
|
||||
* **FastAPI**: |
|
||||
* Da mesma forma que o Starlette utiliza o Uvicorn e não consegue ser mais rápido que ele, o **FastAPI** utiliza o Starlette, portanto, ele não consegue ser mais rápido que ele. |
|
||||
* O FastAPI provê mais funcionalidades em cima do Starlette. Funcionalidades que você quase sempre precisará quando estiver construindo APIs, como validação de dados e serialização. E ao utilizá-lo, você obtém documentação automática sem custo nenhum (a documentação automática sequer adiciona sobrecarga nas aplicações rodando, pois ela é gerada na inicialização). |
|
||||
* Caso você não utilize o FastAPI e faz uso do Starlette diretamente (ou outra ferramenta, como o Sanic, Flask, Responder, etc) você mesmo teria que implementar toda a validação de dados e serialização. Então, a sua aplicação final ainda teria a mesma sobrecarga caso estivesse usando o FastAPI. E em muitos casos, validação de dados e serialização é a maior parte do código escrito em aplicações. |
|
||||
* Então, ao utilizar o FastAPI, você está economizando tempo de programação, evitando bugs, linhas de código, e provavelmente terá a mesma performance (ou até melhor) do que teria caso você não o utilizasse (já que você teria que implementar tudo no seu código). |
|
||||
* Se você está comparando o FastAPI, compare-o com frameworks de aplicações web (ou conjunto de ferramentas) que oferecem validação de dados, serialização e documentação, como por exemplo o Flask-apispec, NestJS, Molten, etc. Frameworks que possuem validação integrada de dados, serialização e documentação. |
|
@ -8,7 +8,7 @@ Por isso é comum prover essas configurações como variáveis de ambiente que s |
|||||
|
|
||||
## Variáveis de Ambiente |
## Variáveis de Ambiente |
||||
|
|
||||
/// dica |
/// tip | Dica |
||||
|
|
||||
Se você já sabe o que são variáveis de ambiente e como utilizá-las, sinta-se livre para avançar para o próximo tópico. |
Se você já sabe o que são variáveis de ambiente e como utilizá-las, sinta-se livre para avançar para o próximo tópico. |
||||
|
|
||||
@ -67,7 +67,7 @@ name = os.getenv("MY_NAME", "World") |
|||||
print(f"Hello {name} from Python") |
print(f"Hello {name} from Python") |
||||
``` |
``` |
||||
|
|
||||
/// dica |
/// tip | Dica |
||||
|
|
||||
O segundo parâmetro em <a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> é o valor padrão para o retorno. |
O segundo parâmetro em <a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> é o valor padrão para o retorno. |
||||
|
|
||||
@ -124,7 +124,7 @@ Hello World from Python |
|||||
|
|
||||
</div> |
</div> |
||||
|
|
||||
/// dica |
/// tip | Dica |
||||
|
|
||||
Você pode ler mais sobre isso em: <a href="https://12factor.net/pt_br/config" class="external-link" target="_blank">The Twelve-Factor App: Configurações</a>. |
Você pode ler mais sobre isso em: <a href="https://12factor.net/pt_br/config" class="external-link" target="_blank">The Twelve-Factor App: Configurações</a>. |
||||
|
|
||||
@ -196,7 +196,7 @@ Na versão 1 do Pydantic você importaria `BaseSettings` diretamente do módulo |
|||||
|
|
||||
//// |
//// |
||||
|
|
||||
/// dica |
/// tip | Dica |
||||
|
|
||||
Se você quiser algo pronto para copiar e colar na sua aplicação, não use esse exemplo, mas sim o exemplo abaixo. |
Se você quiser algo pronto para copiar e colar na sua aplicação, não use esse exemplo, mas sim o exemplo abaixo. |
||||
|
|
||||
@ -226,7 +226,7 @@ $ ADMIN_EMAIL="[email protected]" APP_NAME="ChimichangApp" fastapi run main.p |
|||||
|
|
||||
</div> |
</div> |
||||
|
|
||||
/// dica |
/// tip | Dica |
||||
|
|
||||
Para definir múltiplas variáveis de ambiente para um único comando basta separá-las utilizando espaços, e incluir todas elas antes do comando. |
Para definir múltiplas variáveis de ambiente para um único comando basta separá-las utilizando espaços, e incluir todas elas antes do comando. |
||||
|
|
||||
@ -250,7 +250,7 @@ E utilizar essa configuração em `main.py`: |
|||||
|
|
||||
{* ../../docs_src/settings/app01/main.py hl[3,11:13] *} |
{* ../../docs_src/settings/app01/main.py hl[3,11:13] *} |
||||
|
|
||||
/// dica |
/// tip | Dica |
||||
|
|
||||
Você também precisa incluir um arquivo `__init__.py` como visto em [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=\_blank}. |
Você também precisa incluir um arquivo `__init__.py` como visto em [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=\_blank}. |
||||
|
|
||||
@ -276,7 +276,7 @@ Agora criamos a dependência que retorna um novo objeto `config.Settings()`. |
|||||
|
|
||||
{* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *} |
{* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *} |
||||
|
|
||||
/// dica |
/// tip | Dica |
||||
|
|
||||
Vamos discutir sobre `@lru_cache` logo mais. |
Vamos discutir sobre `@lru_cache` logo mais. |
||||
|
|
||||
@ -304,7 +304,7 @@ Se você tiver muitas configurações que variem bastante, talvez em ambientes d |
|||||
|
|
||||
Essa prática é tão comum que possui um nome, essas variáveis de ambiente normalmente são colocadas em um arquivo `.env`, e esse arquivo é chamado de "dotenv". |
Essa prática é tão comum que possui um nome, essas variáveis de ambiente normalmente são colocadas em um arquivo `.env`, e esse arquivo é chamado de "dotenv". |
||||
|
|
||||
/// dica |
/// tip | Dica |
||||
|
|
||||
Um arquivo iniciando com um ponto final (`.`) é um arquivo oculto em sistemas baseados em Unix, como Linux e MacOS. |
Um arquivo iniciando com um ponto final (`.`) é um arquivo oculto em sistemas baseados em Unix, como Linux e MacOS. |
||||
|
|
||||
@ -314,7 +314,7 @@ Mas um arquivo dotenv não precisa ter esse nome exato. |
|||||
|
|
||||
Pydantic suporta a leitura desses tipos de arquivos utilizando uma biblioteca externa. Você pode ler mais em <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic Settings: Dotenv (.env) support</a>. |
Pydantic suporta a leitura desses tipos de arquivos utilizando uma biblioteca externa. Você pode ler mais em <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic Settings: Dotenv (.env) support</a>. |
||||
|
|
||||
/// dica |
/// tip | Dica |
||||
|
|
||||
Para que isso funcione você precisa executar `pip install python-dotenv`. |
Para que isso funcione você precisa executar `pip install python-dotenv`. |
||||
|
|
||||
@ -337,7 +337,7 @@ E então adicionar o seguinte código em `config.py`: |
|||||
|
|
||||
{* ../../docs_src/settings/app03_an/config.py hl[9] *} |
{* ../../docs_src/settings/app03_an/config.py hl[9] *} |
||||
|
|
||||
/// dica |
/// tip | Dica |
||||
|
|
||||
O atributo `model_config` é usado apenas para configuração do Pydantic. Você pode ler mais em <a href="https://docs.pydantic.dev/latest/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>. |
O atributo `model_config` é usado apenas para configuração do Pydantic. Você pode ler mais em <a href="https://docs.pydantic.dev/latest/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>. |
||||
|
|
||||
@ -349,7 +349,7 @@ O atributo `model_config` é usado apenas para configuração do Pydantic. Você |
|||||
|
|
||||
{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *} |
{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *} |
||||
|
|
||||
/// dica |
/// tip | Dica |
||||
|
|
||||
A classe `Config` é usada apenas para configuração do Pydantic. Você pode ler mais em <a href="https://docs.pydantic.dev/1.10/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>. |
A classe `Config` é usada apenas para configuração do Pydantic. Você pode ler mais em <a href="https://docs.pydantic.dev/1.10/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>. |
||||
|
|
||||
|
@ -0,0 +1,105 @@ |
|||||
|
# Obter Usuário Atual |
||||
|
|
||||
|
No capítulo anterior, o sistema de segurança (que é baseado no sistema de injeção de dependências) estava fornecendo à *função de operação de rota* um `token` como uma `str`: |
||||
|
|
||||
|
{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} |
||||
|
|
||||
|
Mas isso ainda não é tão útil. |
||||
|
|
||||
|
Vamos fazer com que ele nos forneça o usuário atual. |
||||
|
|
||||
|
## Criar um modelo de usuário |
||||
|
|
||||
|
Primeiro, vamos criar um modelo de usuário com Pydantic. |
||||
|
|
||||
|
Da mesma forma que usamos o Pydantic para declarar corpos, podemos usá-lo em qualquer outro lugar: |
||||
|
|
||||
|
{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *} |
||||
|
|
||||
|
## Criar uma dependência `get_current_user` |
||||
|
|
||||
|
Vamos criar uma dependência chamada `get_current_user`. |
||||
|
|
||||
|
Lembra que as dependências podem ter subdependências? |
||||
|
|
||||
|
`get_current_user` terá uma dependência com o mesmo `oauth2_scheme` que criamos antes. |
||||
|
|
||||
|
Da mesma forma que estávamos fazendo antes diretamente na *operação de rota*, a nossa nova dependência `get_current_user` receberá um `token` como uma `str` da subdependência `oauth2_scheme`: |
||||
|
|
||||
|
{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *} |
||||
|
|
||||
|
## Obter o usuário |
||||
|
|
||||
|
`get_current_user` usará uma função utilitária (falsa) que criamos, que recebe um token como uma `str` e retorna nosso modelo Pydantic `User`: |
||||
|
|
||||
|
{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *} |
||||
|
|
||||
|
## Injetar o usuário atual |
||||
|
|
||||
|
Então agora nós podemos usar o mesmo `Depends` com nosso `get_current_user` na *operação de rota*: |
||||
|
|
||||
|
{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} |
||||
|
|
||||
|
Observe que nós declaramos o tipo de `current_user` como o modelo Pydantic `User`. |
||||
|
|
||||
|
Isso nos ajudará dentro da função com todo o preenchimento automático e verificações de tipo. |
||||
|
|
||||
|
/// tip | Dica |
||||
|
|
||||
|
Você pode se lembrar que corpos de requisição também são declarados com modelos Pydantic. |
||||
|
|
||||
|
Aqui, o **FastAPI** não ficará confuso porque você está usando `Depends`. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
/// check | Verifique |
||||
|
|
||||
|
A forma como esse sistema de dependências foi projetado nos permite ter diferentes dependências (diferentes "dependables") que retornam um modelo `User`. |
||||
|
|
||||
|
Não estamos restritos a ter apenas uma dependência que possa retornar esse tipo de dado. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Outros modelos |
||||
|
|
||||
|
Agora você pode obter o usuário atual diretamente nas *funções de operação de rota* e lidar com os mecanismos de segurança no nível da **Injeção de Dependências**, usando `Depends`. |
||||
|
|
||||
|
E você pode usar qualquer modelo ou dado para os requisitos de segurança (neste caso, um modelo Pydantic `User`). |
||||
|
|
||||
|
Mas você não está restrito a usar um modelo de dados, classe ou tipo específico. |
||||
|
|
||||
|
Você quer ter apenas um `id` e `email`, sem incluir nenhum `username` no modelo? Claro. Você pode usar essas mesmas ferramentas. |
||||
|
|
||||
|
Você quer ter apenas uma `str`? Ou apenas um `dict`? Ou uma instância de modelo de classe de banco de dados diretamente? Tudo funciona da mesma forma. |
||||
|
|
||||
|
Na verdade, você não tem usuários que fazem login no seu aplicativo, mas sim robôs, bots ou outros sistemas, que possuem apenas um token de acesso? Novamente, tudo funciona da mesma forma. |
||||
|
|
||||
|
Apenas use qualquer tipo de modelo, qualquer tipo de classe, qualquer tipo de banco de dados que você precise para a sua aplicação. O **FastAPI** cobre tudo com o sistema de injeção de dependências. |
||||
|
|
||||
|
## Tamanho do código |
||||
|
|
||||
|
Este exemplo pode parecer verboso. Lembre-se de que estamos misturando segurança, modelos de dados, funções utilitárias e *operações de rota* no mesmo arquivo. |
||||
|
|
||||
|
Mas aqui está o ponto principal. |
||||
|
|
||||
|
O código relacionado à segurança e à injeção de dependências é escrito apenas uma vez. |
||||
|
|
||||
|
E você pode torná-lo tão complexo quanto quiser. E ainda assim, tê-lo escrito apenas uma vez, em um único lugar. Com toda a flexibilidade. |
||||
|
|
||||
|
Mas você pode ter milhares de endpoints (*operações de rota*) usando o mesmo sistema de segurança. |
||||
|
|
||||
|
E todos eles (ou qualquer parte deles que você desejar) podem aproveitar o reuso dessas dependências ou de quaisquer outras dependências que você criar. |
||||
|
|
||||
|
E todos esses milhares de *operações de rota* podem ter apenas 3 linhas: |
||||
|
|
||||
|
{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} |
||||
|
|
||||
|
## Recapitulação |
||||
|
|
||||
|
Agora você pode obter o usuário atual diretamente na sua *função de operação de rota*. |
||||
|
|
||||
|
Já estamos na metade do caminho. |
||||
|
|
||||
|
Só precisamos adicionar uma *operação de rota* para que o usuário/cliente realmente envie o `username` e `password`. |
||||
|
|
||||
|
Isso vem a seguir. |
@ -0,0 +1,274 @@ |
|||||
|
# OAuth2 com Senha (e hashing), Bearer com tokens JWT |
||||
|
|
||||
|
Agora que temos todo o fluxo de segurança, vamos tornar a aplicação realmente segura, usando tokens <abbr title="JSON Web Tokens">JWT</abbr> e hashing de senhas seguras. |
||||
|
|
||||
|
Este código é algo que você pode realmente usar na sua aplicação, salvar os hashes das senhas no seu banco de dados, etc. |
||||
|
|
||||
|
Vamos começar de onde paramos no capítulo anterior e incrementá-lo. |
||||
|
|
||||
|
## Sobre o JWT |
||||
|
|
||||
|
JWT significa "JSON Web Tokens". |
||||
|
|
||||
|
É um padrão para codificar um objeto JSON em uma string longa e densa sem espaços. Ele se parece com isso: |
||||
|
|
||||
|
``` |
||||
|
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c |
||||
|
``` |
||||
|
|
||||
|
Ele não é criptografado, então qualquer pessoa pode recuperar as informações do seu conteúdo. |
||||
|
|
||||
|
Mas ele é assinado. Assim, quando você recebe um token que você emitiu, você pode verificar que foi realmente você quem o emitiu. |
||||
|
|
||||
|
Dessa forma, você pode criar um token com um prazo de expiração, digamos, de 1 semana. E então, quando o usuário voltar no dia seguinte com o token, você sabe que ele ainda está logado no seu sistema. |
||||
|
|
||||
|
Depois de uma semana, o token expirará e o usuário não estará autorizado, precisando fazer login novamente para obter um novo token. E se o usuário (ou uma terceira parte) tentar modificar o token para alterar a expiração, você seria capaz de descobrir isso, pois as assinaturas não iriam corresponder. |
||||
|
|
||||
|
Se você quiser brincar com tokens JWT e ver como eles funcionam, visite <a href="https://jwt.io/" class="external-link" target="_blank">https://jwt.io</a>. |
||||
|
|
||||
|
## Instalar `PyJWT` |
||||
|
|
||||
|
Nós precisamos instalar o `PyJWT` para criar e verificar os tokens JWT em Python. |
||||
|
|
||||
|
Certifique-se de criar um [ambiente virtual](../../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalar o `pyjwt`: |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
$ pip install pyjwt |
||||
|
|
||||
|
---> 100% |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
/// info | Informação |
||||
|
|
||||
|
Se você pretente utilizar algoritmos de assinatura digital como o RSA ou o ECDSA, você deve instalar a dependência da biblioteca de criptografia `pyjwt[crypto]`. |
||||
|
|
||||
|
Você pode ler mais sobre isso na <a href="https://pyjwt.readthedocs.io/en/latest/installation.html" class="external-link" target="_blank">documentação de instalação do PyJWT</a>. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Hashing de senhas |
||||
|
|
||||
|
"Hashing" significa converter algum conteúdo (uma senha neste caso) em uma sequência de bytes (apenas uma string) que parece um monte de caracteres sem sentido. |
||||
|
|
||||
|
Sempre que você passar exatamente o mesmo conteúdo (exatamente a mesma senha), você obterá exatamente o mesmo resultado. |
||||
|
|
||||
|
Mas não é possível converter os caracteres sem sentido de volta para a senha original. |
||||
|
|
||||
|
### Por que usar hashing de senhas |
||||
|
|
||||
|
Se o seu banco de dados for roubado, o invasor não terá as senhas em texto puro dos seus usuários, apenas os hashes. |
||||
|
|
||||
|
Então, o invasor não poderá tentar usar essas senhas em outro sistema (como muitos usuários utilizam a mesma senha em vários lugares, isso seria perigoso). |
||||
|
|
||||
|
## Instalar o `passlib` |
||||
|
|
||||
|
O PassLib é uma excelente biblioteca Python para lidar com hashes de senhas. |
||||
|
|
||||
|
Ele suporta muitos algoritmos de hashing seguros e utilitários para trabalhar com eles. |
||||
|
|
||||
|
O algoritmo recomendado é o "Bcrypt". |
||||
|
|
||||
|
Certifique-se de criar um [ambiente virtual](../../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalar o PassLib com Bcrypt: |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
$ pip install "passlib[bcrypt]" |
||||
|
|
||||
|
---> 100% |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
/// tip | Dica |
||||
|
|
||||
|
Com o `passlib`, você poderia até configurá-lo para ser capaz de ler senhas criadas pelo **Django**, um plug-in de segurança do **Flask** ou muitos outros. |
||||
|
|
||||
|
Assim, você poderia, por exemplo, compartilhar os mesmos dados de um aplicativo Django em um banco de dados com um aplicativo FastAPI. Ou migrar gradualmente uma aplicação Django usando o mesmo banco de dados. |
||||
|
|
||||
|
E seus usuários poderiam fazer login tanto pela sua aplicação Django quanto pela sua aplicação **FastAPI**, ao mesmo tempo. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Criar o hash e verificar as senhas |
||||
|
|
||||
|
Importe as ferramentas que nós precisamos de `passlib`. |
||||
|
|
||||
|
Crie um "contexto" do PassLib. Este será usado para criar o hash e verificar as senhas. |
||||
|
|
||||
|
/// tip | Dica |
||||
|
|
||||
|
O contexto do PassLib também possui funcionalidades para usar diferentes algoritmos de hashing, incluindo algoritmos antigos que estão obsoletos, apenas para permitir verificá-los, etc. |
||||
|
|
||||
|
Por exemplo, você poderia usá-lo para ler e verificar senhas geradas por outro sistema (como Django), mas criar o hash de novas senhas com um algoritmo diferente, como o Bcrypt. |
||||
|
|
||||
|
E ser compatível com todos eles ao mesmo tempo. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
Crie uma função utilitária para criar o hash de uma senha fornecida pelo usuário. |
||||
|
|
||||
|
E outra função utilitária para verificar se uma senha recebida corresponde ao hash armazenado. |
||||
|
|
||||
|
E outra para autenticar e retornar um usuário. |
||||
|
|
||||
|
{* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,56:57,60:61,70:76] *} |
||||
|
|
||||
|
/// note | Nota |
||||
|
|
||||
|
Se você verificar o novo banco de dados (falso) `fake_users_db`, você verá como o hash da senha se parece agora: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Manipular tokens JWT |
||||
|
|
||||
|
Importe os módulos instalados. |
||||
|
|
||||
|
Crie uma chave secreta aleatória que será usada para assinar os tokens JWT. |
||||
|
|
||||
|
Para gerar uma chave secreta aleatória e segura, use o comando: |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
$ openssl rand -hex 32 |
||||
|
|
||||
|
09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7 |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
E copie a saída para a variável `SECRET_KEY` (não use a do exemplo). |
||||
|
|
||||
|
Crie uma variável `ALGORITHM` com o algoritmo usado para assinar o token JWT e defina como `"HS256"`. |
||||
|
|
||||
|
Crie uma variável para a expiração do token. |
||||
|
|
||||
|
Defina um modelo Pydantic que será usado no endpoint de token para a resposta. |
||||
|
|
||||
|
Crie uma função utilitária para gerar um novo token de acesso. |
||||
|
|
||||
|
{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *} |
||||
|
|
||||
|
## Atualize as dependências |
||||
|
|
||||
|
Atualize `get_current_user` para receber o mesmo token de antes, mas desta vez, usando tokens JWT. |
||||
|
|
||||
|
Decodifique o token recebido, verifique-o e retorne o usuário atual. |
||||
|
|
||||
|
Se o token for inválido, retorne um erro HTTP imediatamente. |
||||
|
|
||||
|
{* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *} |
||||
|
|
||||
|
## Atualize a *operação de rota* `/token` |
||||
|
|
||||
|
Crie um `timedelta` com o tempo de expiração do token. |
||||
|
|
||||
|
Crie um token de acesso JWT real e o retorne. |
||||
|
|
||||
|
{* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *} |
||||
|
|
||||
|
### Detalhes técnicos sobre o "sujeito" `sub` do JWT |
||||
|
|
||||
|
A especificação JWT diz que existe uma chave `sub`, com o sujeito do token. |
||||
|
|
||||
|
É opcional usá-la, mas é onde você colocaria a identificação do usuário, então nós estamos usando aqui. |
||||
|
|
||||
|
O JWT pode ser usado para outras coisas além de identificar um usuário e permitir que ele execute operações diretamente na sua API. |
||||
|
|
||||
|
Por exemplo, você poderia identificar um "carro" ou uma "postagem de blog". |
||||
|
|
||||
|
Depois, você poderia adicionar permissões sobre essa entidade, como "dirigir" (para o carro) ou "editar" (para o blog). |
||||
|
|
||||
|
E então, poderia dar esse token JWT para um usuário (ou bot), e ele poderia usá-lo para realizar essas ações (dirigir o carro ou editar o blog) sem sequer precisar ter uma conta, apenas com o token JWT que sua API gerou para isso. |
||||
|
|
||||
|
Usando essas ideias, o JWT pode ser usado para cenários muito mais sofisticados. |
||||
|
|
||||
|
Nesses casos, várias dessas entidades poderiam ter o mesmo ID, digamos `foo` (um usuário `foo`, um carro `foo` e uma postagem de blog `foo`). |
||||
|
|
||||
|
Então, para evitar colisões de ID, ao criar o token JWT para o usuário, você poderia prefixar o valor da chave `sub`, por exemplo, com `username:`. Assim, neste exemplo, o valor de `sub` poderia ser: `username:johndoe`. |
||||
|
|
||||
|
O importante a se lembrar é que a chave `sub` deve ter um identificador único em toda a aplicação e deve ser uma string. |
||||
|
|
||||
|
## Testando |
||||
|
|
||||
|
Execute o servidor e vá para a documentação: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. |
||||
|
|
||||
|
Você verá a interface de usuário assim: |
||||
|
|
||||
|
<img src="/img/tutorial/security/image07.png"> |
||||
|
|
||||
|
Autorize a aplicação da mesma maneira que antes. |
||||
|
|
||||
|
Usando as credenciais: |
||||
|
|
||||
|
Username: `johndoe` |
||||
|
Password: `secret` |
||||
|
|
||||
|
/// check | Verifique |
||||
|
|
||||
|
Observe que em nenhuma parte do código está a senha em texto puro "`secret`", nós temos apenas o hash. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
<img src="/img/tutorial/security/image08.png"> |
||||
|
|
||||
|
Chame o endpoint `/users/me/`, você receberá o retorno como: |
||||
|
|
||||
|
```JSON |
||||
|
{ |
||||
|
"username": "johndoe", |
||||
|
"email": "[email protected]", |
||||
|
"full_name": "John Doe", |
||||
|
"disabled": false |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
<img src="/img/tutorial/security/image09.png"> |
||||
|
|
||||
|
Se você abrir as ferramentas de desenvolvedor, poderá ver que os dados enviados incluem apenas o token. A senha é enviada apenas na primeira requisição para autenticar o usuário e obter o token de acesso, mas não é enviada nas próximas requisições: |
||||
|
|
||||
|
<img src="/img/tutorial/security/image10.png"> |
||||
|
|
||||
|
/// note | Nota |
||||
|
|
||||
|
Perceba que o cabeçalho `Authorization`, com o valor que começa com `Bearer `. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Uso avançado com `scopes` |
||||
|
|
||||
|
O OAuth2 tem a noção de "scopes" (escopos). |
||||
|
|
||||
|
Você pode usá-los para adicionar um conjunto específico de permissões a um token JWT. |
||||
|
|
||||
|
Então, você pode dar este token diretamente a um usuário ou a uma terceira parte para interagir com sua API com um conjunto de restrições. |
||||
|
|
||||
|
Você pode aprender como usá-los e como eles são integrados ao **FastAPI** mais adiante no **Guia Avançado do Usuário**. |
||||
|
|
||||
|
|
||||
|
## Recapitulação |
||||
|
|
||||
|
Com o que você viu até agora, você pode configurar uma aplicação **FastAPI** segura usando padrões como OAuth2 e JWT. |
||||
|
|
||||
|
Em quase qualquer framework, lidar com a segurança se torna rapidamente um assunto bastante complexo. |
||||
|
|
||||
|
Muitos pacotes que simplificam bastante isso precisam fazer muitas concessões com o modelo de dados, o banco de dados e os recursos disponíveis. E alguns desses pacotes que simplificam demais na verdade têm falhas de segurança subjacentes. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
O **FastAPI** não faz nenhuma concessão com nenhum banco de dados, modelo de dados ou ferramenta. |
||||
|
|
||||
|
Ele oferece toda a flexibilidade para você escolher as opções que melhor se ajustam ao seu projeto. |
||||
|
|
||||
|
E você pode usar diretamente muitos pacotes bem mantidos e amplamente utilizados, como `passlib` e `PyJWT`, porque o **FastAPI** não exige mecanismos complexos para integrar pacotes externos. |
||||
|
|
||||
|
Mas ele fornece as ferramentas para simplificar o processo o máximo possível, sem comprometer a flexibilidade, robustez ou segurança. |
||||
|
|
||||
|
E você pode usar e implementar protocolos padrão seguros, como o OAuth2, de uma maneira relativamente simples. |
||||
|
|
||||
|
Você pode aprender mais no **Guia Avançado do Usuário** sobre como usar os "scopes" do OAuth2 para um sistema de permissões mais refinado, seguindo esses mesmos padrões. O OAuth2 com scopes é o mecanismo usado por muitos provedores grandes de autenticação, como o Facebook, Google, GitHub, Microsoft, Twitter, etc. para autorizar aplicativos de terceiros a interagir com suas APIs em nome de seus usuários. |
@ -0,0 +1,99 @@ |
|||||
|
# Асинхронное тестирование |
||||
|
|
||||
|
Вы уже видели как тестировать **FastAPI** приложение, используя имеющийся класс `TestClient`. К этому моменту вы видели только как писать тесты в синхронном стиле без использования `async` функций. |
||||
|
|
||||
|
Возможность использования асинхронных функций в ваших тестах может быть полезнa, когда, например, вы асинхронно обращаетесь к вашей базе данных. Представьте, что вы хотите отправить запросы в ваше FastAPI приложение, а затем при помощи асинхронной библиотеки для работы с базой данных удостовериться, что ваш бекэнд корректно записал данные в базу данных. |
||||
|
|
||||
|
Давайте рассмотрим, как мы можем это реализовать. |
||||
|
|
||||
|
## pytest.mark.anyio |
||||
|
|
||||
|
Если мы хотим вызывать асинхронные функции в наших тестах, то наши тестовые функции должны быть асинхронными. AnyIO предоставляет для этого отличный плагин, который позволяет нам указывать, какие тестовые функции должны вызываться асинхронно. |
||||
|
|
||||
|
## HTTPX |
||||
|
|
||||
|
Даже если **FastAPI** приложение использует обычные функции `def` вместо `async def`, это все равно `async` приложение 'под капотом'. |
||||
|
|
||||
|
Чтобы работать с асинхронным FastAPI приложением в ваших обычных тестовых функциях `def`, используя стандартный pytest, `TestClient` внутри себя делает некоторую магию. Но эта магия перестает работать, когда мы используем его внутри асинхронных функций. Запуская наши тесты асинхронно, мы больше не можем использовать `TestClient` внутри наших тестовых функций. |
||||
|
|
||||
|
`TestClient` основан на <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>, и, к счастью, мы можем использовать его (`HTTPX`) напрямую для тестирования API. |
||||
|
|
||||
|
## Пример |
||||
|
|
||||
|
В качестве простого примера, давайте рассмотрим файловую структуру, схожую с описанной в [Большие приложения](../tutorial/bigger-applications.md){.internal-link target=_blank} и [Тестирование](../tutorial/testing.md){.internal-link target=_blank}: |
||||
|
|
||||
|
``` |
||||
|
. |
||||
|
├── app |
||||
|
│ ├── __init__.py |
||||
|
│ ├── main.py |
||||
|
│ └── test_main.py |
||||
|
``` |
||||
|
|
||||
|
Файл `main.py`: |
||||
|
|
||||
|
{* ../../docs_src/async_tests/main.py *} |
||||
|
|
||||
|
Файл `test_main.py` содержит тесты для `main.py`, теперь он может выглядеть так: |
||||
|
|
||||
|
{* ../../docs_src/async_tests/test_main.py *} |
||||
|
|
||||
|
## Запуск тестов |
||||
|
|
||||
|
Вы можете запустить свои тесты как обычно: |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
$ pytest |
||||
|
|
||||
|
---> 100% |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
## Подробнее |
||||
|
|
||||
|
Маркер `@pytest.mark.anyio` говорит pytest, что тестовая функция должна быть вызвана асинхронно: |
||||
|
|
||||
|
{* ../../docs_src/async_tests/test_main.py hl[7] *} |
||||
|
|
||||
|
/// tip | Подсказка |
||||
|
|
||||
|
Обратите внимание, что тестовая функция теперь `async def` вместо простого `def`, как это было при использовании `TestClient`. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
Затем мы можем создать `AsyncClient` со ссылкой на приложение и посылать асинхронные запросы, используя `await`. |
||||
|
|
||||
|
{* ../../docs_src/async_tests/test_main.py hl[9:12] *} |
||||
|
|
||||
|
Это эквивалентно следующему: |
||||
|
|
||||
|
```Python |
||||
|
response = client.get('/') |
||||
|
``` |
||||
|
|
||||
|
...которое мы использовали для отправки наших запросов с `TestClient`. |
||||
|
|
||||
|
/// tip | Подсказка |
||||
|
|
||||
|
Обратите внимание, что мы используем async/await с `AsyncClient` - запрос асинхронный. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
/// warning | Внимание |
||||
|
|
||||
|
Если ваше приложение полагается на lifespan события, то `AsyncClient` не запустит эти события. Чтобы обеспечить их срабатывание используйте `LifespanManager` из <a href="https://github.com/florimondmanca/asgi-lifespan#usage" class="external-link" target="_blank">florimondmanca/asgi-lifespan</a>. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Вызов других асинхронных функций |
||||
|
|
||||
|
Теперь тестовая функция стала асинхронной, поэтому внутри нее вы можете вызывать также и другие `async` функции, не связанные с отправлением запросов в ваше FastAPI приложение. Как если бы вы вызывали их в любом другом месте вашего кода. |
||||
|
|
||||
|
/// tip | Подсказка |
||||
|
|
||||
|
Если вы столкнулись с `RuntimeError: Task attached to a different loop` при вызове асинхронных функций в ваших тестах (например, при использовании <a href="https://stackoverflow.com/questions/41584243/runtimeerror-task-attached-to-a-different-loop" class="external-link" target="_blank">MongoDB's MotorClient</a>), то не забывайте инициализировать объекты, которым нужен цикл событий (event loop), только внутри асинхронных функций, например, в `'@app.on_event("startup")` callback. |
||||
|
|
||||
|
/// |
@ -0,0 +1,556 @@ |
|||||
|
# Большие приложения, в которых много файлов |
||||
|
|
||||
|
При построении приложения или веб-API нам редко удается поместить всё в один файл. |
||||
|
|
||||
|
**FastAPI** предоставляет удобный инструментарий, который позволяет нам структурировать приложение, сохраняя при этом всю необходимую гибкость. |
||||
|
|
||||
|
/// info | Примечание |
||||
|
|
||||
|
Если вы раньше использовали Flask, то это аналог шаблонов Flask (Flask's Blueprints). |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Пример структуры приложения |
||||
|
|
||||
|
Давайте предположим, что наше приложение имеет следующую структуру: |
||||
|
|
||||
|
``` |
||||
|
. |
||||
|
├── app |
||||
|
│ ├── __init__.py |
||||
|
│ ├── main.py |
||||
|
│ ├── dependencies.py |
||||
|
│ └── routers |
||||
|
│ │ ├── __init__.py |
||||
|
│ │ ├── items.py |
||||
|
│ │ └── users.py |
||||
|
│ └── internal |
||||
|
│ ├── __init__.py |
||||
|
│ └── admin.py |
||||
|
``` |
||||
|
|
||||
|
/// tip | Подсказка |
||||
|
|
||||
|
Обратите внимание, что в каждом каталоге и подкаталоге имеется файл `__init__.py` |
||||
|
|
||||
|
Это как раз то, что позволяет импортировать код из одного файла в другой. |
||||
|
|
||||
|
Например, в файле `app/main.py` может быть следующая строка: |
||||
|
|
||||
|
``` |
||||
|
from app.routers import items |
||||
|
``` |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
* Всё помещается в каталоге `app`. В нём также находится пустой файл `app/__init__.py`. Таким образом, `app` является "Python-пакетом" (коллекцией модулей Python). |
||||
|
* Он содержит файл `app/main.py`. Данный файл является частью пакета (т.е. находится внутри каталога, содержащего файл `__init__.py`), и, соответственно, он является модулем пакета: `app.main`. |
||||
|
* Он также содержит файл `app/dependencies.py`, который также, как и `app/main.py`, является модулем: `app.dependencies`. |
||||
|
* Здесь также находится подкаталог `app/routers/`, содержащий `__init__.py`. Он является суб-пакетом: `app.routers`. |
||||
|
* Файл `app/routers/items.py` находится внутри пакета `app/routers/`. Таким образом, он является суб-модулем: `app.routers.items`. |
||||
|
* Точно также `app/routers/users.py` является ещё одним суб-модулем: `app.routers.users`. |
||||
|
* Подкаталог `app/internal/`, содержащий файл `__init__.py`, является ещё одним суб-пакетом: `app.internal`. |
||||
|
* А файл `app/internal/admin.py` является ещё одним суб-модулем: `app.internal.admin`. |
||||
|
|
||||
|
<img src="/img/tutorial/bigger-applications/package.svg"> |
||||
|
|
||||
|
Та же самая файловая структура приложения, но с комментариями: |
||||
|
|
||||
|
``` |
||||
|
. |
||||
|
├── app # "app" пакет |
||||
|
│ ├── __init__.py # этот файл превращает "app" в "Python-пакет" |
||||
|
│ ├── main.py # модуль "main", напр.: import app.main |
||||
|
│ ├── dependencies.py # модуль "dependencies", напр.: import app.dependencies |
||||
|
│ └── routers # суб-пакет "routers" |
||||
|
│ │ ├── __init__.py # превращает "routers" в суб-пакет |
||||
|
│ │ ├── items.py # суб-модуль "items", напр.: import app.routers.items |
||||
|
│ │ └── users.py # суб-модуль "users", напр.: import app.routers.users |
||||
|
│ └── internal # суб-пакет "internal" |
||||
|
│ ├── __init__.py # превращает "internal" в суб-пакет |
||||
|
│ └── admin.py # суб-модуль "admin", напр.: import app.internal.admin |
||||
|
``` |
||||
|
|
||||
|
## `APIRouter` |
||||
|
|
||||
|
Давайте предположим, что для работы с пользователями используется отдельный файл (суб-модуль) `/app/routers/users.py`. |
||||
|
|
||||
|
Для лучшей организации приложения, вы хотите отделить операции пути, связанные с пользователями, от остального кода. |
||||
|
|
||||
|
Но так, чтобы эти операции по-прежнему оставались частью **FastAPI** приложения/веб-API (частью одного пакета) |
||||
|
|
||||
|
С помощью `APIRouter` вы можете создать *операции пути* (*эндпоинты*) для данного модуля. |
||||
|
|
||||
|
|
||||
|
### Импорт `APIRouter` |
||||
|
|
||||
|
Точно также, как и в случае с классом `FastAPI`, вам нужно импортировать и создать объект класса `APIRouter`. |
||||
|
|
||||
|
```Python hl_lines="1 3" title="app/routers/users.py" |
||||
|
{!../../docs_src/bigger_applications/app/routers/users.py!} |
||||
|
``` |
||||
|
|
||||
|
### Создание *эндпоинтов* с помощью `APIRouter` |
||||
|
|
||||
|
В дальнейшем используйте `APIRouter` для объявления *эндпоинтов*, точно также, как вы используете класс `FastAPI`: |
||||
|
|
||||
|
```Python hl_lines="6 11 16" title="app/routers/users.py" |
||||
|
{!../../docs_src/bigger_applications/app/routers/users.py!} |
||||
|
``` |
||||
|
|
||||
|
Вы можете думать об `APIRouter` как об "уменьшенной версии" класса FastAPI`. |
||||
|
|
||||
|
`APIRouter` поддерживает все те же самые опции. |
||||
|
|
||||
|
`APIRouter` поддерживает все те же самые параметры, такие как `parameters`, `responses`, `dependencies`, `tags`, и т. д. |
||||
|
|
||||
|
/// tip | Подсказка |
||||
|
|
||||
|
В данном примере, в качестве названия переменной используется `router`, но вы можете использовать любое другое имя. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
Мы собираемся подключить данный `APIRouter` к нашему основному приложению на `FastAPI`, но сначала давайте проверим зависимости и создадим ещё один модуль с `APIRouter`. |
||||
|
|
||||
|
## Зависимости |
||||
|
|
||||
|
Нам понадобятся некоторые зависимости, которые мы будем использовать в разных местах нашего приложения. |
||||
|
|
||||
|
Мы поместим их в отдельный модуль `dependencies` (`app/dependencies.py`). |
||||
|
|
||||
|
Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомизированный `X-Token` из заголовка: |
||||
|
|
||||
|
//// tab | Python 3.9+ |
||||
|
|
||||
|
```Python hl_lines="3 6-8" title="app/dependencies.py" |
||||
|
{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!} |
||||
|
``` |
||||
|
|
||||
|
//// |
||||
|
|
||||
|
//// tab | Python 3.8+ |
||||
|
|
||||
|
```Python hl_lines="1 5-7" title="app/dependencies.py" |
||||
|
{!> ../../docs_src/bigger_applications/app_an/dependencies.py!} |
||||
|
``` |
||||
|
|
||||
|
//// |
||||
|
|
||||
|
//// tab | Python 3.8+ non-Annotated |
||||
|
|
||||
|
/// tip | Подсказка |
||||
|
|
||||
|
Мы рекомендуем использовать версию `Annotated`, когда это возможно. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
```Python hl_lines="1 4-6" title="app/dependencies.py" |
||||
|
{!> ../../docs_src/bigger_applications/app/dependencies.py!} |
||||
|
``` |
||||
|
|
||||
|
//// |
||||
|
|
||||
|
/// tip | Подсказка |
||||
|
|
||||
|
Для простоты мы воспользовались неким воображаемым заголовоком. |
||||
|
|
||||
|
В реальных случаях для получения наилучших результатов используйте интегрированные утилиты обеспечения безопасности [Security utilities](security/index.md){.internal-link target=_blank}. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Ещё один модуль с `APIRouter` |
||||
|
|
||||
|
Давайте также предположим, что у вас есть *эндпоинты*, отвечающие за обработку "items", и они находятся в модуле `app/routers/items.py`. |
||||
|
|
||||
|
У вас определены следующие *операции пути* (*эндпоинты*): |
||||
|
|
||||
|
* `/items/` |
||||
|
* `/items/{item_id}` |
||||
|
|
||||
|
Тут всё точно также, как и в ситуации с `app/routers/users.py`. |
||||
|
|
||||
|
Но теперь мы хотим поступить немного умнее и слегка упростить код. |
||||
|
|
||||
|
Мы знаем, что все *эндпоинты* данного модуля имеют некоторые общие свойства: |
||||
|
|
||||
|
* Префикс пути: `/items`. |
||||
|
* Теги: (один единственный тег: `items`). |
||||
|
* Дополнительные ответы (responses) |
||||
|
* Зависимости: использование созданной нами зависимости `X-token` |
||||
|
|
||||
|
Таким образом, вместо того чтобы добавлять все эти свойства в функцию каждого отдельного *эндпоинта*, |
||||
|
мы добавим их в `APIRouter`. |
||||
|
|
||||
|
```Python hl_lines="5-10 16 21" title="app/routers/items.py" |
||||
|
{!../../docs_src/bigger_applications/app/routers/items.py!} |
||||
|
``` |
||||
|
|
||||
|
Так как каждый *эндпоинт* начинается с символа `/`: |
||||
|
|
||||
|
```Python hl_lines="1" |
||||
|
@router.get("/{item_id}") |
||||
|
async def read_item(item_id: str): |
||||
|
... |
||||
|
``` |
||||
|
|
||||
|
...то префикс не должен заканчиваться символом `/`. |
||||
|
|
||||
|
В нашем случае префиксом является `/items`. |
||||
|
|
||||
|
Мы также можем добавить в наш маршрутизатор (router) список `тегов` (`tags`) и дополнительных `ответов` (`responses`), которые являются общими для каждого *эндпоинта*. |
||||
|
|
||||
|
И ещё мы можем добавить в наш маршрутизатор список `зависимостей`, которые должны вызываться при каждом обращении к *эндпоинтам*. |
||||
|
|
||||
|
/// tip | Подсказка |
||||
|
|
||||
|
Обратите внимание, что также, как и в случае с зависимостями в декораторах *эндпоинтов* ([dependencies in *path operation decorators*](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), никакого значения в *функцию эндпоинта* передано не будет. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
В результате мы получим следующие эндпоинты: |
||||
|
|
||||
|
* `/items/` |
||||
|
* `/items/{item_id}` |
||||
|
|
||||
|
...как мы и планировали. |
||||
|
|
||||
|
* Они будут помечены тегами из заданного списка, в нашем случае это `"items"`. |
||||
|
* Эти теги особенно полезны для системы автоматической интерактивной документации (с использованием OpenAPI). |
||||
|
* Каждый из них будет включать предопределенные ответы `responses`. |
||||
|
* Каждый *эндпоинт* будет иметь список зависимостей (`dependencies`), исполняемых перед вызовом *эндпоинта*. |
||||
|
* Если вы определили зависимости в самой операции пути, **то она также будет выполнена**. |
||||
|
* Сначала выполняются зависимости маршрутизатора, затем вызываются зависимости, определенные в декораторе *эндпоинта* ([`dependencies` in the decorator](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), и, наконец, обычные параметрические зависимости. |
||||
|
* Вы также можете добавить зависимости безопасности с областями видимости (`scopes`) [`Security` dependencies with `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}. |
||||
|
|
||||
|
/// tip | Подсказка |
||||
|
|
||||
|
Например, с помощью зависимостей в `APIRouter` мы можем потребовать аутентификации для доступа ко всей группе *эндпоинтов*, не указывая зависимости для каждой отдельной функции *эндпоинта*. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
/// check | Заметка |
||||
|
|
||||
|
Параметры `prefix`, `tags`, `responses` и `dependencies` относятся к функционалу **FastAPI**, помогающему избежать дублирования кода. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
### Импорт зависимостей |
||||
|
|
||||
|
Наш код находится в модуле `app.routers.items` (файл `app/routers/items.py`). |
||||
|
|
||||
|
И нам нужно вызвать функцию зависимости из модуля `app.dependencies` (файл `app/dependencies.py`). |
||||
|
|
||||
|
Мы используем операцию относительного импорта `..` для импорта зависимости: |
||||
|
|
||||
|
```Python hl_lines="3" title="app/routers/items.py" |
||||
|
{!../../docs_src/bigger_applications/app/routers/items.py!} |
||||
|
``` |
||||
|
|
||||
|
#### Как работает относительный импорт? |
||||
|
|
||||
|
/// tip | Подсказка |
||||
|
|
||||
|
Если вы прекрасно знаете, как работает импорт в Python, то переходите к следующему разделу. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
Одна точка `.`, как в данном примере: |
||||
|
|
||||
|
```Python |
||||
|
from .dependencies import get_token_header |
||||
|
``` |
||||
|
означает: |
||||
|
|
||||
|
* Начните с пакета, в котором находится данный модуль (файл `app/routers/items.py` расположен в каталоге `app/routers/`)... |
||||
|
* ... найдите модуль `dependencies` (файл `app/routers/dependencies.py`)... |
||||
|
* ... и импортируйте из него функцию `get_token_header`. |
||||
|
|
||||
|
К сожалению, такого файла не существует, и наши зависимости находятся в файле `app/dependencies.py`. |
||||
|
|
||||
|
Вспомните, как выглядит файловая структура нашего приложения: |
||||
|
|
||||
|
<img src="/img/tutorial/bigger-applications/package.svg"> |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
Две точки `..`, как в данном примере: |
||||
|
|
||||
|
```Python |
||||
|
from ..dependencies import get_token_header |
||||
|
``` |
||||
|
|
||||
|
означают: |
||||
|
|
||||
|
* Начните с пакета, в котором находится данный модуль (файл `app/routers/items.py` находится в каталоге `app/routers/`)... |
||||
|
* ... перейдите в родительский пакет (каталог `app/`)... |
||||
|
* ... найдите в нём модуль `dependencies` (файл `app/dependencies.py`)... |
||||
|
* ... и импортируйте из него функцию `get_token_header`. |
||||
|
|
||||
|
Это работает верно! 🎉 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
Аналогично, если бы мы использовали три точки `...`, как здесь: |
||||
|
|
||||
|
```Python |
||||
|
from ...dependencies import get_token_header |
||||
|
``` |
||||
|
|
||||
|
то это бы означало: |
||||
|
|
||||
|
* Начните с пакета, в котором находится данный модуль (файл `app/routers/items.py` находится в каталоге `app/routers/`)... |
||||
|
* ... перейдите в родительский пакет (каталог `app/`)... |
||||
|
* ... затем перейдите в родительский пакет текущего пакета (такого пакета не существует, `app` находится на самом верхнем уровне 😱)... |
||||
|
* ... найдите в нём модуль `dependencies` (файл `app/dependencies.py`)... |
||||
|
* ... и импортируйте из него функцию `get_token_header`. |
||||
|
|
||||
|
Это будет относиться к некоторому пакету, находящемуся на один уровень выше чем `app/` и содержащему свой собственный файл `__init__.py`. Но ничего такого у нас нет. Поэтому это приведет к ошибке в нашем примере. 🚨 |
||||
|
|
||||
|
Теперь вы знаете, как работает импорт в Python, и сможете использовать относительное импортирование в своих собственных приложениях любого уровня сложности. 🤓 |
||||
|
|
||||
|
### Добавление пользовательских тегов (`tags`), ответов (`responses`) и зависимостей (`dependencies`) |
||||
|
|
||||
|
Мы не будем добавлять префикс `/items` и список тегов `tags=["items"]` для каждого *эндпоинта*, т.к. мы уже их добавили с помощью `APIRouter`. |
||||
|
|
||||
|
Но помимо этого мы можем добавить новые теги для каждого отдельного *эндпоинта*, а также некоторые дополнительные ответы (`responses`), характерные для данного *эндпоинта*: |
||||
|
|
||||
|
```Python hl_lines="30-31" title="app/routers/items.py" |
||||
|
{!../../docs_src/bigger_applications/app/routers/items.py!} |
||||
|
``` |
||||
|
|
||||
|
/// tip | Подсказка |
||||
|
|
||||
|
Последний *эндпоинт* будет иметь следующую комбинацию тегов: `["items", "custom"]`. |
||||
|
|
||||
|
А также в его документации будут содержаться оба ответа: один для `404` и другой для `403`. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Модуль main в `FastAPI` |
||||
|
|
||||
|
Теперь давайте посмотрим на модуль `app/main.py`. |
||||
|
|
||||
|
Именно сюда вы импортируете и именно здесь вы используете класс `FastAPI`. |
||||
|
|
||||
|
Это основной файл вашего приложения, который объединяет всё в одно целое. |
||||
|
|
||||
|
И теперь, когда большая часть логики приложения разделена на отдельные модули, основной файл `app/main.py` будет достаточно простым. |
||||
|
|
||||
|
### Импорт `FastAPI` |
||||
|
|
||||
|
Вы импортируете и создаете класс `FastAPI` как обычно. |
||||
|
|
||||
|
Мы даже можем объявить глобальные зависимости [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank}, которые будут объединены с зависимостями для каждого отдельного маршрутизатора: |
||||
|
|
||||
|
```Python hl_lines="1 3 7" title="app/main.py" |
||||
|
{!../../docs_src/bigger_applications/app/main.py!} |
||||
|
``` |
||||
|
|
||||
|
### Импорт `APIRouter` |
||||
|
|
||||
|
Теперь мы импортируем другие суб-модули, содержащие `APIRouter`: |
||||
|
|
||||
|
```Python hl_lines="4-5" title="app/main.py" |
||||
|
{!../../docs_src/bigger_applications/app/main.py!} |
||||
|
``` |
||||
|
|
||||
|
Так как файлы `app/routers/users.py` и `app/routers/items.py` являются суб-модулями одного и того же Python-пакета `app`, то мы сможем их импортировать, воспользовавшись операцией относительного импорта `.`. |
||||
|
|
||||
|
### Как работает импорт? |
||||
|
|
||||
|
Данная строка кода: |
||||
|
|
||||
|
```Python |
||||
|
from .routers import items, users |
||||
|
``` |
||||
|
|
||||
|
означает: |
||||
|
|
||||
|
* Начните с пакета, в котором содержится данный модуль (файл `app/main.py` содержится в каталоге `app/`)... |
||||
|
* ... найдите суб-пакет `routers` (каталог `app/routers/`)... |
||||
|
* ... и из него импортируйте суб-модули `items` (файл `app/routers/items.py`) и `users` (файл `app/routers/users.py`)... |
||||
|
|
||||
|
В модуле `items` содержится переменная `router` (`items.router`), та самая, которую мы создали в файле `app/routers/items.py`, она является объектом класса `APIRouter`. |
||||
|
|
||||
|
И затем мы сделаем то же самое для модуля `users`. |
||||
|
|
||||
|
Мы также могли бы импортировать и другим методом: |
||||
|
|
||||
|
```Python |
||||
|
from app.routers import items, users |
||||
|
``` |
||||
|
|
||||
|
/// info | Примечание |
||||
|
|
||||
|
Первая версия является примером относительного импорта: |
||||
|
|
||||
|
```Python |
||||
|
from .routers import items, users |
||||
|
``` |
||||
|
|
||||
|
Вторая версия является примером абсолютного импорта: |
||||
|
|
||||
|
```Python |
||||
|
from app.routers import items, users |
||||
|
``` |
||||
|
|
||||
|
Узнать больше о пакетах и модулях в Python вы можете из <a href="https://docs.python.org/3/tutorial/modules.html" class="external-link" target="_blank">официальной документации Python о модулях</a> |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
### Избегайте конфликтов имен |
||||
|
|
||||
|
Вместо того чтобы импортировать только переменную `router`, мы импортируем непосредственно суб-модуль `items`. |
||||
|
|
||||
|
Мы делаем это потому, что у нас есть ещё одна переменная `router` в суб-модуле `users`. |
||||
|
|
||||
|
Если бы мы импортировали их одну за другой, как показано в примере: |
||||
|
|
||||
|
```Python |
||||
|
from .routers.items import router |
||||
|
from .routers.users import router |
||||
|
``` |
||||
|
|
||||
|
то переменная `router` из `users` переписал бы переменную `router` из `items`, и у нас не было бы возможности использовать их одновременно. |
||||
|
|
||||
|
Поэтому, для того чтобы использовать обе эти переменные в одном файле, мы импортировали соответствующие суб-модули: |
||||
|
|
||||
|
```Python hl_lines="5" title="app/main.py" |
||||
|
{!../../docs_src/bigger_applications/app/main.py!} |
||||
|
``` |
||||
|
|
||||
|
### Подключение маршрутизаторов (`APIRouter`) для `users` и для `items` |
||||
|
|
||||
|
Давайте подключим маршрутизаторы (`router`) из суб-модулей `users` и `items`: |
||||
|
|
||||
|
```Python hl_lines="10-11" title="app/main.py" |
||||
|
{!../../docs_src/bigger_applications/app/main.py!} |
||||
|
``` |
||||
|
|
||||
|
/// info | Примечание |
||||
|
|
||||
|
`users.router` содержит `APIRouter` из файла `app/routers/users.py`. |
||||
|
|
||||
|
А `items.router` содержит `APIRouter` из файла `app/routers/items.py`. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
С помощью `app.include_router()` мы можем добавить каждый из маршрутизаторов (`APIRouter`) в основное приложение `FastAPI`. |
||||
|
|
||||
|
Он подключит все маршруты заданного маршрутизатора к нашему приложению. |
||||
|
|
||||
|
/// note | Технические детали |
||||
|
|
||||
|
Фактически, внутри он создаст все *операции пути* для каждой операции пути объявленной в `APIRouter`. |
||||
|
|
||||
|
И под капотом всё будет работать так, как будто бы мы имеем дело с одним файлом приложения. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
/// check | Заметка |
||||
|
|
||||
|
При подключении маршрутизаторов не стоит беспокоиться о производительности. |
||||
|
|
||||
|
Операция подключения займёт микросекунды и понадобится только при запуске приложения. |
||||
|
|
||||
|
Таким образом, это не повлияет на производительность. ⚡ |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
### Подключение `APIRouter` с пользовательскими префиксом (`prefix`), тегами (`tags`), ответами (`responses`), и зависимостями (`dependencies`) |
||||
|
|
||||
|
Теперь давайте представим, что ваша организация передала вам файл `app/internal/admin.py`. |
||||
|
|
||||
|
Он содержит `APIRouter` с некоторыми *эндпоитами* администрирования, которые ваша организация использует для нескольких проектов. |
||||
|
|
||||
|
В данном примере это сделать очень просто. Но давайте предположим, что поскольку файл используется для нескольких проектов, |
||||
|
то мы не можем модифицировать его, добавляя префиксы (`prefix`), зависимости (`dependencies`), теги (`tags`), и т.д. непосредственно в `APIRouter`: |
||||
|
|
||||
|
```Python hl_lines="3" title="app/internal/admin.py" |
||||
|
{!../../docs_src/bigger_applications/app/internal/admin.py!} |
||||
|
``` |
||||
|
|
||||
|
Но, несмотря на это, мы хотим использовать кастомный префикс (`prefix`) для подключенного маршрутизатора (`APIRouter`), в результате чего, каждая *операция пути* будет начинаться с `/admin`. Также мы хотим защитить наш маршрутизатор с помощью зависимостей, созданных для нашего проекта. И ещё мы хотим включить теги (`tags`) и ответы (`responses`). |
||||
|
|
||||
|
Мы можем применить все вышеперечисленные настройки, не изменяя начальный `APIRouter`. Нам всего лишь нужно передать нужные параметры в `app.include_router()`. |
||||
|
|
||||
|
```Python hl_lines="14-17" title="app/main.py" |
||||
|
{!../../docs_src/bigger_applications/app/main.py!} |
||||
|
``` |
||||
|
|
||||
|
Таким образом, оригинальный `APIRouter` не будет модифицирован, и мы сможем использовать файл `app/internal/admin.py` сразу в нескольких проектах организации. |
||||
|
|
||||
|
В результате, в нашем приложении каждый *эндпоинт* модуля `admin` будет иметь: |
||||
|
|
||||
|
* Префикс `/admin`. |
||||
|
* Тег `admin`. |
||||
|
* Зависимость `get_token_header`. |
||||
|
* Ответ `418`. 🍵 |
||||
|
|
||||
|
Это будет иметь место исключительно для `APIRouter` в нашем приложении, и не затронет любой другой код, использующий его. |
||||
|
|
||||
|
Например, другие проекты, могут использовать тот же самый `APIRouter` с другими методами аутентификации. |
||||
|
|
||||
|
### Подключение отдельного *эндпоинта* |
||||
|
|
||||
|
Мы также можем добавить *эндпоинт* непосредственно в основное приложение `FastAPI`. |
||||
|
|
||||
|
Здесь мы это делаем ... просто, чтобы показать, что это возможно 🤷: |
||||
|
|
||||
|
```Python hl_lines="21-23" title="app/main.py" |
||||
|
{!../../docs_src/bigger_applications/app/main.py!} |
||||
|
``` |
||||
|
|
||||
|
и это будет работать корректно вместе с другими *эндпоинтами*, добавленными с помощью `app.include_router()`. |
||||
|
|
||||
|
/// info | Сложные технические детали |
||||
|
|
||||
|
**Примечание**: это сложная техническая деталь, которую, скорее всего, **вы можете пропустить**. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
Маршрутизаторы (`APIRouter`) не "монтируются" по-отдельности и не изолируются от остального приложения. |
||||
|
|
||||
|
Это происходит потому, что нужно включить их *эндпоинты* в OpenAPI схему и в интерфейс пользователя. |
||||
|
|
||||
|
В силу того, что мы не можем их изолировать и "примонтировать" независимо от остальных, *эндпоинты* клонируются (пересоздаются) и не подключаются напрямую. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Проверка автоматической документации API |
||||
|
|
||||
|
Теперь запустите приложение: |
||||
|
|
||||
|
<div class="termy"> |
||||
|
|
||||
|
```console |
||||
|
$ fastapi dev app/main.py |
||||
|
|
||||
|
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
||||
|
``` |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
Откройте документацию по адресу <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. |
||||
|
|
||||
|
Вы увидите автоматическую API документацию. Она включает в себя маршруты из суб-модулей, используя верные маршруты, префиксы и теги: |
||||
|
|
||||
|
<img src="/img/tutorial/bigger-applications/image01.png"> |
||||
|
|
||||
|
## Подключение существующего маршрута через новый префикс (`prefix`) |
||||
|
|
||||
|
Вы можете использовать `.include_router()` несколько раз с одним и тем же маршрутом, применив различные префиксы. |
||||
|
|
||||
|
Это может быть полезным, если нужно предоставить доступ к одному и тому же API через различные префиксы, например, `/api/v1` и `/api/latest`. |
||||
|
|
||||
|
Это продвинутый способ, который вам может и не пригодится. Мы приводим его на случай, если вдруг вам это понадобится. |
||||
|
|
||||
|
## Включение одного маршрутизатора (`APIRouter`) в другой |
||||
|
|
||||
|
Точно так же, как вы включаете `APIRouter` в приложение `FastAPI`, вы можете включить `APIRouter` в другой `APIRouter`: |
||||
|
|
||||
|
```Python |
||||
|
router.include_router(other_router) |
||||
|
``` |
||||
|
|
||||
|
Удостоверьтесь, что вы сделали это до того, как подключить маршрутизатор (`router`) к вашему `FastAPI` приложению, и *эндпоинты* маршрутизатора `other_router` были также подключены. |
@ -1,30 +0,0 @@ |
|||||
from . import models, schemas |
|
||||
|
|
||||
|
|
||||
def get_user(user_id: int): |
|
||||
return models.User.filter(models.User.id == user_id).first() |
|
||||
|
|
||||
|
|
||||
def get_user_by_email(email: str): |
|
||||
return models.User.filter(models.User.email == email).first() |
|
||||
|
|
||||
|
|
||||
def get_users(skip: int = 0, limit: int = 100): |
|
||||
return list(models.User.select().offset(skip).limit(limit)) |
|
||||
|
|
||||
|
|
||||
def create_user(user: schemas.UserCreate): |
|
||||
fake_hashed_password = user.password + "notreallyhashed" |
|
||||
db_user = models.User(email=user.email, hashed_password=fake_hashed_password) |
|
||||
db_user.save() |
|
||||
return db_user |
|
||||
|
|
||||
|
|
||||
def get_items(skip: int = 0, limit: int = 100): |
|
||||
return list(models.Item.select().offset(skip).limit(limit)) |
|
||||
|
|
||||
|
|
||||
def create_user_item(item: schemas.ItemCreate, user_id: int): |
|
||||
db_item = models.Item(**item.dict(), owner_id=user_id) |
|
||||
db_item.save() |
|
||||
return db_item |
|
@ -1,24 +0,0 @@ |
|||||
from contextvars import ContextVar |
|
||||
|
|
||||
import peewee |
|
||||
|
|
||||
DATABASE_NAME = "test.db" |
|
||||
db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None} |
|
||||
db_state = ContextVar("db_state", default=db_state_default.copy()) |
|
||||
|
|
||||
|
|
||||
class PeeweeConnectionState(peewee._ConnectionState): |
|
||||
def __init__(self, **kwargs): |
|
||||
super().__setattr__("_state", db_state) |
|
||||
super().__init__(**kwargs) |
|
||||
|
|
||||
def __setattr__(self, name, value): |
|
||||
self._state.get()[name] = value |
|
||||
|
|
||||
def __getattr__(self, name): |
|
||||
return self._state.get()[name] |
|
||||
|
|
||||
|
|
||||
db = peewee.SqliteDatabase(DATABASE_NAME, check_same_thread=False) |
|
||||
|
|
||||
db._state = PeeweeConnectionState() |
|
@ -1,79 +0,0 @@ |
|||||
import time |
|
||||
from typing import List |
|
||||
|
|
||||
from fastapi import Depends, FastAPI, HTTPException |
|
||||
|
|
||||
from . import crud, database, models, schemas |
|
||||
from .database import db_state_default |
|
||||
|
|
||||
database.db.connect() |
|
||||
database.db.create_tables([models.User, models.Item]) |
|
||||
database.db.close() |
|
||||
|
|
||||
app = FastAPI() |
|
||||
|
|
||||
sleep_time = 10 |
|
||||
|
|
||||
|
|
||||
async def reset_db_state(): |
|
||||
database.db._state._state.set(db_state_default.copy()) |
|
||||
database.db._state.reset() |
|
||||
|
|
||||
|
|
||||
def get_db(db_state=Depends(reset_db_state)): |
|
||||
try: |
|
||||
database.db.connect() |
|
||||
yield |
|
||||
finally: |
|
||||
if not database.db.is_closed(): |
|
||||
database.db.close() |
|
||||
|
|
||||
|
|
||||
@app.post("/users/", response_model=schemas.User, dependencies=[Depends(get_db)]) |
|
||||
def create_user(user: schemas.UserCreate): |
|
||||
db_user = crud.get_user_by_email(email=user.email) |
|
||||
if db_user: |
|
||||
raise HTTPException(status_code=400, detail="Email already registered") |
|
||||
return crud.create_user(user=user) |
|
||||
|
|
||||
|
|
||||
@app.get("/users/", response_model=List[schemas.User], dependencies=[Depends(get_db)]) |
|
||||
def read_users(skip: int = 0, limit: int = 100): |
|
||||
users = crud.get_users(skip=skip, limit=limit) |
|
||||
return users |
|
||||
|
|
||||
|
|
||||
@app.get( |
|
||||
"/users/{user_id}", response_model=schemas.User, dependencies=[Depends(get_db)] |
|
||||
) |
|
||||
def read_user(user_id: int): |
|
||||
db_user = crud.get_user(user_id=user_id) |
|
||||
if db_user is None: |
|
||||
raise HTTPException(status_code=404, detail="User not found") |
|
||||
return db_user |
|
||||
|
|
||||
|
|
||||
@app.post( |
|
||||
"/users/{user_id}/items/", |
|
||||
response_model=schemas.Item, |
|
||||
dependencies=[Depends(get_db)], |
|
||||
) |
|
||||
def create_item_for_user(user_id: int, item: schemas.ItemCreate): |
|
||||
return crud.create_user_item(item=item, user_id=user_id) |
|
||||
|
|
||||
|
|
||||
@app.get("/items/", response_model=List[schemas.Item], dependencies=[Depends(get_db)]) |
|
||||
def read_items(skip: int = 0, limit: int = 100): |
|
||||
items = crud.get_items(skip=skip, limit=limit) |
|
||||
return items |
|
||||
|
|
||||
|
|
||||
@app.get( |
|
||||
"/slowusers/", response_model=List[schemas.User], dependencies=[Depends(get_db)] |
|
||||
) |
|
||||
def read_slow_users(skip: int = 0, limit: int = 100): |
|
||||
global sleep_time |
|
||||
sleep_time = max(0, sleep_time - 1) |
|
||||
time.sleep(sleep_time) # Fake long processing request |
|
||||
users = crud.get_users(skip=skip, limit=limit) |
|
||||
return users |
|
@ -1,21 +0,0 @@ |
|||||
import peewee |
|
||||
|
|
||||
from .database import db |
|
||||
|
|
||||
|
|
||||
class User(peewee.Model): |
|
||||
email = peewee.CharField(unique=True, index=True) |
|
||||
hashed_password = peewee.CharField() |
|
||||
is_active = peewee.BooleanField(default=True) |
|
||||
|
|
||||
class Meta: |
|
||||
database = db |
|
||||
|
|
||||
|
|
||||
class Item(peewee.Model): |
|
||||
title = peewee.CharField(index=True) |
|
||||
description = peewee.CharField(index=True) |
|
||||
owner = peewee.ForeignKeyField(User, backref="items") |
|
||||
|
|
||||
class Meta: |
|
||||
database = db |
|
@ -1,49 +0,0 @@ |
|||||
from typing import Any, List, Union |
|
||||
|
|
||||
import peewee |
|
||||
from pydantic import BaseModel |
|
||||
from pydantic.utils import GetterDict |
|
||||
|
|
||||
|
|
||||
class PeeweeGetterDict(GetterDict): |
|
||||
def get(self, key: Any, default: Any = None): |
|
||||
res = getattr(self._obj, key, default) |
|
||||
if isinstance(res, peewee.ModelSelect): |
|
||||
return list(res) |
|
||||
return res |
|
||||
|
|
||||
|
|
||||
class ItemBase(BaseModel): |
|
||||
title: str |
|
||||
description: Union[str, None] = None |
|
||||
|
|
||||
|
|
||||
class ItemCreate(ItemBase): |
|
||||
pass |
|
||||
|
|
||||
|
|
||||
class Item(ItemBase): |
|
||||
id: int |
|
||||
owner_id: int |
|
||||
|
|
||||
class Config: |
|
||||
orm_mode = True |
|
||||
getter_dict = PeeweeGetterDict |
|
||||
|
|
||||
|
|
||||
class UserBase(BaseModel): |
|
||||
email: str |
|
||||
|
|
||||
|
|
||||
class UserCreate(UserBase): |
|
||||
password: str |
|
||||
|
|
||||
|
|
||||
class User(UserBase): |
|
||||
id: int |
|
||||
is_active: bool |
|
||||
items: List[Item] = [] |
|
||||
|
|
||||
class Config: |
|
||||
orm_mode = True |
|
||||
getter_dict = PeeweeGetterDict |
|
@ -0,0 +1,401 @@ |
|||||
|
import logging |
||||
|
import secrets |
||||
|
import subprocess |
||||
|
import time |
||||
|
from collections import Counter |
||||
|
from datetime import datetime, timedelta, timezone |
||||
|
from pathlib import Path |
||||
|
from typing import Any, Container, Union |
||||
|
|
||||
|
import httpx |
||||
|
import yaml |
||||
|
from github import Github |
||||
|
from pydantic import BaseModel, SecretStr |
||||
|
from pydantic_settings import BaseSettings |
||||
|
|
||||
|
github_graphql_url = "https://api.github.com/graphql" |
||||
|
questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0" |
||||
|
|
||||
|
discussions_query = """ |
||||
|
query Q($after: String, $category_id: ID) { |
||||
|
repository(name: "fastapi", owner: "fastapi") { |
||||
|
discussions(first: 100, after: $after, categoryId: $category_id) { |
||||
|
edges { |
||||
|
cursor |
||||
|
node { |
||||
|
number |
||||
|
author { |
||||
|
login |
||||
|
avatarUrl |
||||
|
url |
||||
|
} |
||||
|
createdAt |
||||
|
comments(first: 50) { |
||||
|
totalCount |
||||
|
nodes { |
||||
|
createdAt |
||||
|
author { |
||||
|
login |
||||
|
avatarUrl |
||||
|
url |
||||
|
} |
||||
|
isAnswer |
||||
|
replies(first: 10) { |
||||
|
totalCount |
||||
|
nodes { |
||||
|
createdAt |
||||
|
author { |
||||
|
login |
||||
|
avatarUrl |
||||
|
url |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
class Author(BaseModel): |
||||
|
login: str |
||||
|
avatarUrl: str | None = None |
||||
|
url: str | None = None |
||||
|
|
||||
|
|
||||
|
class CommentsNode(BaseModel): |
||||
|
createdAt: datetime |
||||
|
author: Union[Author, None] = None |
||||
|
|
||||
|
|
||||
|
class Replies(BaseModel): |
||||
|
totalCount: int |
||||
|
nodes: list[CommentsNode] |
||||
|
|
||||
|
|
||||
|
class DiscussionsCommentsNode(CommentsNode): |
||||
|
replies: Replies |
||||
|
|
||||
|
|
||||
|
class DiscussionsComments(BaseModel): |
||||
|
totalCount: int |
||||
|
nodes: list[DiscussionsCommentsNode] |
||||
|
|
||||
|
|
||||
|
class DiscussionsNode(BaseModel): |
||||
|
number: int |
||||
|
author: Union[Author, None] = None |
||||
|
title: str | None = None |
||||
|
createdAt: datetime |
||||
|
comments: DiscussionsComments |
||||
|
|
||||
|
|
||||
|
class DiscussionsEdge(BaseModel): |
||||
|
cursor: str |
||||
|
node: DiscussionsNode |
||||
|
|
||||
|
|
||||
|
class Discussions(BaseModel): |
||||
|
edges: list[DiscussionsEdge] |
||||
|
|
||||
|
|
||||
|
class DiscussionsRepository(BaseModel): |
||||
|
discussions: Discussions |
||||
|
|
||||
|
|
||||
|
class DiscussionsResponseData(BaseModel): |
||||
|
repository: DiscussionsRepository |
||||
|
|
||||
|
|
||||
|
class DiscussionsResponse(BaseModel): |
||||
|
data: DiscussionsResponseData |
||||
|
|
||||
|
|
||||
|
class Settings(BaseSettings): |
||||
|
github_token: SecretStr |
||||
|
github_repository: str |
||||
|
httpx_timeout: int = 30 |
||||
|
|
||||
|
|
||||
|
def get_graphql_response( |
||||
|
*, |
||||
|
settings: Settings, |
||||
|
query: str, |
||||
|
after: Union[str, None] = None, |
||||
|
category_id: Union[str, None] = None, |
||||
|
) -> dict[str, Any]: |
||||
|
headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"} |
||||
|
variables = {"after": after, "category_id": category_id} |
||||
|
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(data["errors"]) |
||||
|
logging.error(response.text) |
||||
|
raise RuntimeError(response.text) |
||||
|
return data |
||||
|
|
||||
|
|
||||
|
def get_graphql_question_discussion_edges( |
||||
|
*, |
||||
|
settings: Settings, |
||||
|
after: Union[str, None] = None, |
||||
|
) -> list[DiscussionsEdge]: |
||||
|
data = get_graphql_response( |
||||
|
settings=settings, |
||||
|
query=discussions_query, |
||||
|
after=after, |
||||
|
category_id=questions_category_id, |
||||
|
) |
||||
|
graphql_response = DiscussionsResponse.model_validate(data) |
||||
|
return graphql_response.data.repository.discussions.edges |
||||
|
|
||||
|
|
||||
|
class DiscussionExpertsResults(BaseModel): |
||||
|
commenters: Counter[str] |
||||
|
last_month_commenters: Counter[str] |
||||
|
three_months_commenters: Counter[str] |
||||
|
six_months_commenters: Counter[str] |
||||
|
one_year_commenters: Counter[str] |
||||
|
authors: dict[str, Author] |
||||
|
|
||||
|
|
||||
|
def get_discussion_nodes(settings: Settings) -> list[DiscussionsNode]: |
||||
|
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] |
||||
|
# Handle GitHub secondary rate limits, requests per minute |
||||
|
time.sleep(5) |
||||
|
discussion_edges = get_graphql_question_discussion_edges( |
||||
|
settings=settings, after=last_edge.cursor |
||||
|
) |
||||
|
return discussion_nodes |
||||
|
|
||||
|
|
||||
|
def get_discussions_experts( |
||||
|
discussion_nodes: list[DiscussionsNode], |
||||
|
) -> DiscussionExpertsResults: |
||||
|
commenters = Counter[str]() |
||||
|
last_month_commenters = Counter[str]() |
||||
|
three_months_commenters = Counter[str]() |
||||
|
six_months_commenters = Counter[str]() |
||||
|
one_year_commenters = Counter[str]() |
||||
|
authors: dict[str, Author] = {} |
||||
|
|
||||
|
now = datetime.now(tz=timezone.utc) |
||||
|
one_month_ago = now - timedelta(days=30) |
||||
|
three_months_ago = now - timedelta(days=90) |
||||
|
six_months_ago = now - timedelta(days=180) |
||||
|
one_year_ago = now - timedelta(days=365) |
||||
|
|
||||
|
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: dict[str, datetime] = {} |
||||
|
for comment in discussion.comments.nodes: |
||||
|
if comment.author: |
||||
|
authors[comment.author.login] = comment.author |
||||
|
if comment.author.login != discussion_author_name: |
||||
|
author_time = discussion_commentors.get( |
||||
|
comment.author.login, comment.createdAt |
||||
|
) |
||||
|
discussion_commentors[comment.author.login] = max( |
||||
|
author_time, comment.createdAt |
||||
|
) |
||||
|
for reply in comment.replies.nodes: |
||||
|
if reply.author: |
||||
|
authors[reply.author.login] = reply.author |
||||
|
if reply.author.login != discussion_author_name: |
||||
|
author_time = discussion_commentors.get( |
||||
|
reply.author.login, reply.createdAt |
||||
|
) |
||||
|
discussion_commentors[reply.author.login] = max( |
||||
|
author_time, reply.createdAt |
||||
|
) |
||||
|
for author_name, author_time in discussion_commentors.items(): |
||||
|
commenters[author_name] += 1 |
||||
|
if author_time > one_month_ago: |
||||
|
last_month_commenters[author_name] += 1 |
||||
|
if author_time > three_months_ago: |
||||
|
three_months_commenters[author_name] += 1 |
||||
|
if author_time > six_months_ago: |
||||
|
six_months_commenters[author_name] += 1 |
||||
|
if author_time > one_year_ago: |
||||
|
one_year_commenters[author_name] += 1 |
||||
|
discussion_experts_results = DiscussionExpertsResults( |
||||
|
authors=authors, |
||||
|
commenters=commenters, |
||||
|
last_month_commenters=last_month_commenters, |
||||
|
three_months_commenters=three_months_commenters, |
||||
|
six_months_commenters=six_months_commenters, |
||||
|
one_year_commenters=one_year_commenters, |
||||
|
) |
||||
|
return discussion_experts_results |
||||
|
|
||||
|
|
||||
|
def get_top_users( |
||||
|
*, |
||||
|
counter: Counter[str], |
||||
|
authors: dict[str, Author], |
||||
|
skip_users: Container[str], |
||||
|
min_count: int = 2, |
||||
|
) -> list[dict[str, Any]]: |
||||
|
users: list[dict[str, Any]] = [] |
||||
|
for commenter, count in counter.most_common(50): |
||||
|
if commenter in skip_users: |
||||
|
continue |
||||
|
if count >= min_count: |
||||
|
author = authors[commenter] |
||||
|
users.append( |
||||
|
{ |
||||
|
"login": commenter, |
||||
|
"count": count, |
||||
|
"avatarUrl": author.avatarUrl, |
||||
|
"url": author.url, |
||||
|
} |
||||
|
) |
||||
|
return users |
||||
|
|
||||
|
|
||||
|
def get_users_to_write( |
||||
|
*, |
||||
|
counter: Counter[str], |
||||
|
authors: dict[str, Author], |
||||
|
min_count: int = 2, |
||||
|
) -> list[dict[str, Any]]: |
||||
|
users: dict[str, Any] = {} |
||||
|
users_list: list[dict[str, Any]] = [] |
||||
|
for user, count in counter.most_common(60): |
||||
|
if count >= min_count: |
||||
|
author = authors[user] |
||||
|
user_data = { |
||||
|
"login": user, |
||||
|
"count": count, |
||||
|
"avatarUrl": author.avatarUrl, |
||||
|
"url": author.url, |
||||
|
} |
||||
|
users[user] = user_data |
||||
|
users_list.append(user_data) |
||||
|
return users_list |
||||
|
|
||||
|
|
||||
|
def update_content(*, content_path: Path, new_content: Any) -> bool: |
||||
|
old_content = content_path.read_text(encoding="utf-8") |
||||
|
|
||||
|
new_content = yaml.dump(new_content, sort_keys=False, width=200, allow_unicode=True) |
||||
|
if old_content == new_content: |
||||
|
logging.info(f"The content hasn't changed for {content_path}") |
||||
|
return False |
||||
|
content_path.write_text(new_content, encoding="utf-8") |
||||
|
logging.info(f"Updated {content_path}") |
||||
|
return True |
||||
|
|
||||
|
|
||||
|
def main() -> None: |
||||
|
logging.basicConfig(level=logging.INFO) |
||||
|
settings = Settings() |
||||
|
logging.info(f"Using config: {settings.model_dump_json()}") |
||||
|
g = Github(settings.github_token.get_secret_value()) |
||||
|
repo = g.get_repo(settings.github_repository) |
||||
|
|
||||
|
discussion_nodes = get_discussion_nodes(settings=settings) |
||||
|
experts_results = get_discussions_experts(discussion_nodes=discussion_nodes) |
||||
|
|
||||
|
authors = experts_results.authors |
||||
|
maintainers_logins = {"tiangolo"} |
||||
|
maintainers = [] |
||||
|
for login in maintainers_logins: |
||||
|
user = authors[login] |
||||
|
maintainers.append( |
||||
|
{ |
||||
|
"login": login, |
||||
|
"answers": experts_results.commenters[login], |
||||
|
"avatarUrl": user.avatarUrl, |
||||
|
"url": user.url, |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
experts = get_users_to_write( |
||||
|
counter=experts_results.commenters, |
||||
|
authors=authors, |
||||
|
) |
||||
|
last_month_experts = get_users_to_write( |
||||
|
counter=experts_results.last_month_commenters, |
||||
|
authors=authors, |
||||
|
) |
||||
|
three_months_experts = get_users_to_write( |
||||
|
counter=experts_results.three_months_commenters, |
||||
|
authors=authors, |
||||
|
) |
||||
|
six_months_experts = get_users_to_write( |
||||
|
counter=experts_results.six_months_commenters, |
||||
|
authors=authors, |
||||
|
) |
||||
|
one_year_experts = get_users_to_write( |
||||
|
counter=experts_results.one_year_commenters, |
||||
|
authors=authors, |
||||
|
) |
||||
|
|
||||
|
people = { |
||||
|
"maintainers": maintainers, |
||||
|
"experts": experts, |
||||
|
"last_month_experts": last_month_experts, |
||||
|
"three_months_experts": three_months_experts, |
||||
|
"six_months_experts": six_months_experts, |
||||
|
"one_year_experts": one_year_experts, |
||||
|
} |
||||
|
|
||||
|
# For local development |
||||
|
# people_path = Path("../docs/en/data/people.yml") |
||||
|
people_path = Path("./docs/en/data/people.yml") |
||||
|
|
||||
|
updated = update_content(content_path=people_path, new_content=people) |
||||
|
|
||||
|
if not updated: |
||||
|
logging.info("The data hasn't changed, finishing.") |
||||
|
return |
||||
|
|
||||
|
logging.info("Setting up GitHub Actions git user") |
||||
|
subprocess.run(["git", "config", "user.name", "github-actions"], check=True) |
||||
|
subprocess.run( |
||||
|
["git", "config", "user.email", "[email protected]"], check=True |
||||
|
) |
||||
|
branch_name = f"fastapi-people-experts-{secrets.token_hex(4)}" |
||||
|
logging.info(f"Creating a new branch {branch_name}") |
||||
|
subprocess.run(["git", "checkout", "-b", branch_name], check=True) |
||||
|
logging.info("Adding updated file") |
||||
|
subprocess.run(["git", "add", str(people_path)], check=True) |
||||
|
logging.info("Committing updated file") |
||||
|
message = "👥 Update FastAPI People - Experts" |
||||
|
subprocess.run(["git", "commit", "-m", message], check=True) |
||||
|
logging.info("Pushing branch") |
||||
|
subprocess.run(["git", "push", "origin", branch_name], check=True) |
||||
|
logging.info("Creating PR") |
||||
|
pr = repo.create_pull(title=message, body=message, base="master", head=branch_name) |
||||
|
logging.info(f"Created PR: {pr.number}") |
||||
|
logging.info("Finished") |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
main() |
@ -0,0 +1,221 @@ |
|||||
|
import logging |
||||
|
import secrets |
||||
|
import subprocess |
||||
|
from collections import defaultdict |
||||
|
from pathlib import Path |
||||
|
from typing import Any |
||||
|
|
||||
|
import httpx |
||||
|
import yaml |
||||
|
from github import Github |
||||
|
from pydantic import BaseModel, SecretStr |
||||
|
from pydantic_settings import BaseSettings |
||||
|
|
||||
|
github_graphql_url = "https://api.github.com/graphql" |
||||
|
|
||||
|
|
||||
|
sponsors_query = """ |
||||
|
query Q($after: String) { |
||||
|
user(login: "tiangolo") { |
||||
|
sponsorshipsAsMaintainer(first: 100, after: $after) { |
||||
|
edges { |
||||
|
cursor |
||||
|
node { |
||||
|
sponsorEntity { |
||||
|
... on Organization { |
||||
|
login |
||||
|
avatarUrl |
||||
|
url |
||||
|
} |
||||
|
... on User { |
||||
|
login |
||||
|
avatarUrl |
||||
|
url |
||||
|
} |
||||
|
} |
||||
|
tier { |
||||
|
name |
||||
|
monthlyPriceInDollars |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
class SponsorEntity(BaseModel): |
||||
|
login: str |
||||
|
avatarUrl: str |
||||
|
url: str |
||||
|
|
||||
|
|
||||
|
class Tier(BaseModel): |
||||
|
name: str |
||||
|
monthlyPriceInDollars: float |
||||
|
|
||||
|
|
||||
|
class SponsorshipAsMaintainerNode(BaseModel): |
||||
|
sponsorEntity: SponsorEntity |
||||
|
tier: Tier |
||||
|
|
||||
|
|
||||
|
class SponsorshipAsMaintainerEdge(BaseModel): |
||||
|
cursor: str |
||||
|
node: SponsorshipAsMaintainerNode |
||||
|
|
||||
|
|
||||
|
class SponsorshipAsMaintainer(BaseModel): |
||||
|
edges: list[SponsorshipAsMaintainerEdge] |
||||
|
|
||||
|
|
||||
|
class SponsorsUser(BaseModel): |
||||
|
sponsorshipsAsMaintainer: SponsorshipAsMaintainer |
||||
|
|
||||
|
|
||||
|
class SponsorsResponseData(BaseModel): |
||||
|
user: SponsorsUser |
||||
|
|
||||
|
|
||||
|
class SponsorsResponse(BaseModel): |
||||
|
data: SponsorsResponseData |
||||
|
|
||||
|
|
||||
|
class Settings(BaseSettings): |
||||
|
sponsors_token: SecretStr |
||||
|
pr_token: SecretStr |
||||
|
github_repository: str |
||||
|
httpx_timeout: int = 30 |
||||
|
|
||||
|
|
||||
|
def get_graphql_response( |
||||
|
*, |
||||
|
settings: Settings, |
||||
|
query: str, |
||||
|
after: str | None = None, |
||||
|
) -> dict[str, Any]: |
||||
|
headers = {"Authorization": f"token {settings.sponsors_token.get_secret_value()}"} |
||||
|
variables = {"after": after} |
||||
|
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}") |
||||
|
logging.error(response.text) |
||||
|
raise RuntimeError(response.text) |
||||
|
data = response.json() |
||||
|
if "errors" in data: |
||||
|
logging.error(f"Errors in response, after: {after}") |
||||
|
logging.error(data["errors"]) |
||||
|
logging.error(response.text) |
||||
|
raise RuntimeError(response.text) |
||||
|
return data |
||||
|
|
||||
|
|
||||
|
def get_graphql_sponsor_edges( |
||||
|
*, settings: Settings, after: str | None = None |
||||
|
) -> list[SponsorshipAsMaintainerEdge]: |
||||
|
data = get_graphql_response(settings=settings, query=sponsors_query, after=after) |
||||
|
graphql_response = SponsorsResponse.model_validate(data) |
||||
|
return graphql_response.data.user.sponsorshipsAsMaintainer.edges |
||||
|
|
||||
|
|
||||
|
def get_individual_sponsors( |
||||
|
settings: Settings, |
||||
|
) -> defaultdict[float, dict[str, SponsorEntity]]: |
||||
|
nodes: list[SponsorshipAsMaintainerNode] = [] |
||||
|
edges = get_graphql_sponsor_edges(settings=settings) |
||||
|
|
||||
|
while edges: |
||||
|
for edge in edges: |
||||
|
nodes.append(edge.node) |
||||
|
last_edge = edges[-1] |
||||
|
edges = get_graphql_sponsor_edges(settings=settings, after=last_edge.cursor) |
||||
|
|
||||
|
tiers: defaultdict[float, dict[str, SponsorEntity]] = defaultdict(dict) |
||||
|
for node in nodes: |
||||
|
tiers[node.tier.monthlyPriceInDollars][node.sponsorEntity.login] = ( |
||||
|
node.sponsorEntity |
||||
|
) |
||||
|
return tiers |
||||
|
|
||||
|
|
||||
|
def update_content(*, content_path: Path, new_content: Any) -> bool: |
||||
|
old_content = content_path.read_text(encoding="utf-8") |
||||
|
|
||||
|
new_content = yaml.dump(new_content, sort_keys=False, width=200, allow_unicode=True) |
||||
|
if old_content == new_content: |
||||
|
logging.info(f"The content hasn't changed for {content_path}") |
||||
|
return False |
||||
|
content_path.write_text(new_content, encoding="utf-8") |
||||
|
logging.info(f"Updated {content_path}") |
||||
|
return True |
||||
|
|
||||
|
|
||||
|
def main() -> None: |
||||
|
logging.basicConfig(level=logging.INFO) |
||||
|
settings = Settings() |
||||
|
logging.info(f"Using config: {settings.model_dump_json()}") |
||||
|
g = Github(settings.pr_token.get_secret_value()) |
||||
|
repo = g.get_repo(settings.github_repository) |
||||
|
|
||||
|
tiers = get_individual_sponsors(settings=settings) |
||||
|
keys = list(tiers.keys()) |
||||
|
keys.sort(reverse=True) |
||||
|
sponsors = [] |
||||
|
for key in keys: |
||||
|
sponsor_group = [] |
||||
|
for login, sponsor in tiers[key].items(): |
||||
|
sponsor_group.append( |
||||
|
{"login": login, "avatarUrl": sponsor.avatarUrl, "url": sponsor.url} |
||||
|
) |
||||
|
sponsors.append(sponsor_group) |
||||
|
github_sponsors = { |
||||
|
"sponsors": sponsors, |
||||
|
} |
||||
|
|
||||
|
# For local development |
||||
|
# github_sponsors_path = Path("../docs/en/data/github_sponsors.yml") |
||||
|
github_sponsors_path = Path("./docs/en/data/github_sponsors.yml") |
||||
|
updated = update_content( |
||||
|
content_path=github_sponsors_path, new_content=github_sponsors |
||||
|
) |
||||
|
|
||||
|
if not updated: |
||||
|
logging.info("The data hasn't changed, finishing.") |
||||
|
return |
||||
|
|
||||
|
logging.info("Setting up GitHub Actions git user") |
||||
|
subprocess.run(["git", "config", "user.name", "github-actions"], check=True) |
||||
|
subprocess.run( |
||||
|
["git", "config", "user.email", "[email protected]"], check=True |
||||
|
) |
||||
|
branch_name = f"fastapi-people-sponsors-{secrets.token_hex(4)}" |
||||
|
logging.info(f"Creating a new branch {branch_name}") |
||||
|
subprocess.run(["git", "checkout", "-b", branch_name], check=True) |
||||
|
logging.info("Adding updated file") |
||||
|
subprocess.run( |
||||
|
[ |
||||
|
"git", |
||||
|
"add", |
||||
|
str(github_sponsors_path), |
||||
|
], |
||||
|
check=True, |
||||
|
) |
||||
|
logging.info("Committing updated file") |
||||
|
message = "👥 Update FastAPI People - Sponsors" |
||||
|
subprocess.run(["git", "commit", "-m", message], check=True) |
||||
|
logging.info("Pushing branch") |
||||
|
subprocess.run(["git", "push", "origin", branch_name], check=True) |
||||
|
logging.info("Creating PR") |
||||
|
pr = repo.create_pull(title=message, body=message, base="master", head=branch_name) |
||||
|
logging.info(f"Created PR: {pr.number}") |
||||
|
logging.info("Finished") |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
main() |
@ -1,19 +0,0 @@ |
|||||
import os |
|
||||
from pathlib import Path |
|
||||
|
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from docs_src.background_tasks.tutorial002_an import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
|
|
||||
|
|
||||
def test(): |
|
||||
log = Path("log.txt") |
|
||||
if log.is_file(): |
|
||||
os.remove(log) # pragma: no cover |
|
||||
response = client.post("/send-notification/[email protected]?q=some-query") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == {"message": "Message sent"} |
|
||||
with open("./log.txt") as f: |
|
||||
assert "found query: some-query\nmessage to [email protected]" in f.read() |
|
@ -1,21 +0,0 @@ |
|||||
import os |
|
||||
from pathlib import Path |
|
||||
|
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test(): |
|
||||
from docs_src.background_tasks.tutorial002_an_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
log = Path("log.txt") |
|
||||
if log.is_file(): |
|
||||
os.remove(log) # pragma: no cover |
|
||||
response = client.post("/send-notification/[email protected]?q=some-query") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == {"message": "Message sent"} |
|
||||
with open("./log.txt") as f: |
|
||||
assert "found query: some-query\nmessage to [email protected]" in f.read() |
|
@ -1,21 +0,0 @@ |
|||||
import os |
|
||||
from pathlib import Path |
|
||||
|
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py39 |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test(): |
|
||||
from docs_src.background_tasks.tutorial002_an_py39 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
log = Path("log.txt") |
|
||||
if log.is_file(): |
|
||||
os.remove(log) # pragma: no cover |
|
||||
response = client.post("/send-notification/[email protected]?q=some-query") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == {"message": "Message sent"} |
|
||||
with open("./log.txt") as f: |
|
||||
assert "found query: some-query\nmessage to [email protected]" in f.read() |
|
@ -1,21 +0,0 @@ |
|||||
import os |
|
||||
from pathlib import Path |
|
||||
|
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test(): |
|
||||
from docs_src.background_tasks.tutorial002_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
log = Path("log.txt") |
|
||||
if log.is_file(): |
|
||||
os.remove(log) # pragma: no cover |
|
||||
response = client.post("/send-notification/[email protected]?q=some-query") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == {"message": "Message sent"} |
|
||||
with open("./log.txt") as f: |
|
||||
assert "found query: some-query\nmessage to [email protected]" in f.read() |
|
@ -1,715 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.bigger_applications.app_an.main import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
def test_users_token_jessica(client: TestClient): |
|
||||
response = client.get("/users?token=jessica") |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] |
|
||||
|
|
||||
|
|
||||
def test_users_with_no_token(client: TestClient): |
|
||||
response = client.get("/users") |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_users_foo_token_jessica(client: TestClient): |
|
||||
response = client.get("/users/foo?token=jessica") |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"username": "foo"} |
|
||||
|
|
||||
|
|
||||
def test_users_foo_with_no_token(client: TestClient): |
|
||||
response = client.get("/users/foo") |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_users_me_token_jessica(client: TestClient): |
|
||||
response = client.get("/users/me?token=jessica") |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"username": "fakecurrentuser"} |
|
||||
|
|
||||
|
|
||||
def test_users_me_with_no_token(client: TestClient): |
|
||||
response = client.get("/users/me") |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_users_token_monica_with_no_jessica(client: TestClient): |
|
||||
response = client.get("/users?token=monica") |
|
||||
assert response.status_code == 400 |
|
||||
assert response.json() == {"detail": "No Jessica token provided"} |
|
||||
|
|
||||
|
|
||||
def test_items_token_jessica(client: TestClient): |
|
||||
response = client.get( |
|
||||
"/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"plumbus": {"name": "Plumbus"}, |
|
||||
"gun": {"name": "Portal Gun"}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
def test_items_with_no_token_jessica(client: TestClient): |
|
||||
response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_items_plumbus_token_jessica(client: TestClient): |
|
||||
response = client.get( |
|
||||
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} |
|
||||
|
|
||||
|
|
||||
def test_items_bar_token_jessica(client: TestClient): |
|
||||
response = client.get( |
|
||||
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 404 |
|
||||
assert response.json() == {"detail": "Item not found"} |
|
||||
|
|
||||
|
|
||||
def test_items_plumbus_with_no_token(client: TestClient): |
|
||||
response = client.get( |
|
||||
"/items/plumbus", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_items_with_invalid_token(client: TestClient): |
|
||||
response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) |
|
||||
assert response.status_code == 400 |
|
||||
assert response.json() == {"detail": "X-Token header invalid"} |
|
||||
|
|
||||
|
|
||||
def test_items_bar_with_invalid_token(client: TestClient): |
|
||||
response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) |
|
||||
assert response.status_code == 400 |
|
||||
assert response.json() == {"detail": "X-Token header invalid"} |
|
||||
|
|
||||
|
|
||||
def test_items_with_missing_x_token_header(client: TestClient): |
|
||||
response = client.get("/items?token=jessica") |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["header", "x-token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["header", "x-token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_items_plumbus_with_missing_x_token_header(client: TestClient): |
|
||||
response = client.get("/items/plumbus?token=jessica") |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["header", "x-token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["header", "x-token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_root_token_jessica(client: TestClient): |
|
||||
response = client.get("/?token=jessica") |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"message": "Hello Bigger Applications!"} |
|
||||
|
|
||||
|
|
||||
def test_root_with_no_token(client: TestClient): |
|
||||
response = client.get("/") |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_put_no_header(client: TestClient): |
|
||||
response = client.put("/items/foo") |
|
||||
assert response.status_code == 422, response.text |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["header", "x-token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["header", "x-token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_put_invalid_header(client: TestClient): |
|
||||
response = client.put("/items/foo", headers={"X-Token": "invalid"}) |
|
||||
assert response.status_code == 400, response.text |
|
||||
assert response.json() == {"detail": "X-Token header invalid"} |
|
||||
|
|
||||
|
|
||||
def test_put(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} |
|
||||
|
|
||||
|
|
||||
def test_put_forbidden(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 403, response.text |
|
||||
assert response.json() == {"detail": "You can only update the item: plumbus"} |
|
||||
|
|
||||
|
|
||||
def test_admin(client: TestClient): |
|
||||
response = client.post( |
|
||||
"/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == {"message": "Admin getting schwifty"} |
|
||||
|
|
||||
|
|
||||
def test_admin_invalid_header(client: TestClient): |
|
||||
response = client.post("/admin/", headers={"X-Token": "invalid"}) |
|
||||
assert response.status_code == 400, response.text |
|
||||
assert response.json() == {"detail": "X-Token header invalid"} |
|
||||
|
|
||||
|
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/users/": { |
|
||||
"get": { |
|
||||
"tags": ["users"], |
|
||||
"summary": "Read Users", |
|
||||
"operationId": "read_users_users__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
} |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"/users/me": { |
|
||||
"get": { |
|
||||
"tags": ["users"], |
|
||||
"summary": "Read User Me", |
|
||||
"operationId": "read_user_me_users_me_get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
} |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"/users/{username}": { |
|
||||
"get": { |
|
||||
"tags": ["users"], |
|
||||
"summary": "Read User", |
|
||||
"operationId": "read_user_users__username__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Username", "type": "string"}, |
|
||||
"name": "username", |
|
||||
"in": "path", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"tags": ["items"], |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "X-Token", "type": "string"}, |
|
||||
"name": "x-token", |
|
||||
"in": "header", |
|
||||
}, |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"404": {"description": "Not found"}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"/items/{item_id}": { |
|
||||
"get": { |
|
||||
"tags": ["items"], |
|
||||
"summary": "Read Item", |
|
||||
"operationId": "read_item_items__item_id__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "string"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "X-Token", "type": "string"}, |
|
||||
"name": "x-token", |
|
||||
"in": "header", |
|
||||
}, |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"404": {"description": "Not found"}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"put": { |
|
||||
"tags": ["items", "custom"], |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "string"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "X-Token", "type": "string"}, |
|
||||
"name": "x-token", |
|
||||
"in": "header", |
|
||||
}, |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"404": {"description": "Not found"}, |
|
||||
"403": {"description": "Operation forbidden"}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"/admin/": { |
|
||||
"post": { |
|
||||
"tags": ["admin"], |
|
||||
"summary": "Update Admin", |
|
||||
"operationId": "update_admin_admin__post", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "X-Token", "type": "string"}, |
|
||||
"name": "x-token", |
|
||||
"in": "header", |
|
||||
}, |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"418": {"description": "I'm a teapot"}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"/": { |
|
||||
"get": { |
|
||||
"summary": "Root", |
|
||||
"operationId": "root__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
} |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,742 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py39 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.bigger_applications.app_an_py39.main import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_users_token_jessica(client: TestClient): |
|
||||
response = client.get("/users?token=jessica") |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_users_with_no_token(client: TestClient): |
|
||||
response = client.get("/users") |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_users_foo_token_jessica(client: TestClient): |
|
||||
response = client.get("/users/foo?token=jessica") |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"username": "foo"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_users_foo_with_no_token(client: TestClient): |
|
||||
response = client.get("/users/foo") |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_users_me_token_jessica(client: TestClient): |
|
||||
response = client.get("/users/me?token=jessica") |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"username": "fakecurrentuser"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_users_me_with_no_token(client: TestClient): |
|
||||
response = client.get("/users/me") |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_users_token_monica_with_no_jessica(client: TestClient): |
|
||||
response = client.get("/users?token=monica") |
|
||||
assert response.status_code == 400 |
|
||||
assert response.json() == {"detail": "No Jessica token provided"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_items_token_jessica(client: TestClient): |
|
||||
response = client.get( |
|
||||
"/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"plumbus": {"name": "Plumbus"}, |
|
||||
"gun": {"name": "Portal Gun"}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_items_with_no_token_jessica(client: TestClient): |
|
||||
response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_items_plumbus_token_jessica(client: TestClient): |
|
||||
response = client.get( |
|
||||
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_items_bar_token_jessica(client: TestClient): |
|
||||
response = client.get( |
|
||||
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 404 |
|
||||
assert response.json() == {"detail": "Item not found"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_items_plumbus_with_no_token(client: TestClient): |
|
||||
response = client.get( |
|
||||
"/items/plumbus", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_items_with_invalid_token(client: TestClient): |
|
||||
response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) |
|
||||
assert response.status_code == 400 |
|
||||
assert response.json() == {"detail": "X-Token header invalid"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_items_bar_with_invalid_token(client: TestClient): |
|
||||
response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) |
|
||||
assert response.status_code == 400 |
|
||||
assert response.json() == {"detail": "X-Token header invalid"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_items_with_missing_x_token_header(client: TestClient): |
|
||||
response = client.get("/items?token=jessica") |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["header", "x-token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["header", "x-token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_items_plumbus_with_missing_x_token_header(client: TestClient): |
|
||||
response = client.get("/items/plumbus?token=jessica") |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["header", "x-token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["header", "x-token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_root_token_jessica(client: TestClient): |
|
||||
response = client.get("/?token=jessica") |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"message": "Hello Bigger Applications!"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_root_with_no_token(client: TestClient): |
|
||||
response = client.get("/") |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_put_no_header(client: TestClient): |
|
||||
response = client.put("/items/foo") |
|
||||
assert response.status_code == 422, response.text |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["header", "x-token"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["query", "token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["header", "x-token"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_put_invalid_header(client: TestClient): |
|
||||
response = client.put("/items/foo", headers={"X-Token": "invalid"}) |
|
||||
assert response.status_code == 400, response.text |
|
||||
assert response.json() == {"detail": "X-Token header invalid"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_put(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_put_forbidden(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 403, response.text |
|
||||
assert response.json() == {"detail": "You can only update the item: plumbus"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_admin(client: TestClient): |
|
||||
response = client.post( |
|
||||
"/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} |
|
||||
) |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == {"message": "Admin getting schwifty"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_admin_invalid_header(client: TestClient): |
|
||||
response = client.post("/admin/", headers={"X-Token": "invalid"}) |
|
||||
assert response.status_code == 400, response.text |
|
||||
assert response.json() == {"detail": "X-Token header invalid"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/users/": { |
|
||||
"get": { |
|
||||
"tags": ["users"], |
|
||||
"summary": "Read Users", |
|
||||
"operationId": "read_users_users__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
} |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"/users/me": { |
|
||||
"get": { |
|
||||
"tags": ["users"], |
|
||||
"summary": "Read User Me", |
|
||||
"operationId": "read_user_me_users_me_get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
} |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"/users/{username}": { |
|
||||
"get": { |
|
||||
"tags": ["users"], |
|
||||
"summary": "Read User", |
|
||||
"operationId": "read_user_users__username__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Username", "type": "string"}, |
|
||||
"name": "username", |
|
||||
"in": "path", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"tags": ["items"], |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "X-Token", "type": "string"}, |
|
||||
"name": "x-token", |
|
||||
"in": "header", |
|
||||
}, |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"404": {"description": "Not found"}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"/items/{item_id}": { |
|
||||
"get": { |
|
||||
"tags": ["items"], |
|
||||
"summary": "Read Item", |
|
||||
"operationId": "read_item_items__item_id__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "string"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "X-Token", "type": "string"}, |
|
||||
"name": "x-token", |
|
||||
"in": "header", |
|
||||
}, |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"404": {"description": "Not found"}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"put": { |
|
||||
"tags": ["items", "custom"], |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "string"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "X-Token", "type": "string"}, |
|
||||
"name": "x-token", |
|
||||
"in": "header", |
|
||||
}, |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"404": {"description": "Not found"}, |
|
||||
"403": {"description": "Operation forbidden"}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"/admin/": { |
|
||||
"post": { |
|
||||
"tags": ["admin"], |
|
||||
"summary": "Update Admin", |
|
||||
"operationId": "update_admin_admin__post", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "X-Token", "type": "string"}, |
|
||||
"name": "x-token", |
|
||||
"in": "header", |
|
||||
}, |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"418": {"description": "I'm a teapot"}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"/": { |
|
||||
"get": { |
|
||||
"summary": "Root", |
|
||||
"operationId": "root__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Token", "type": "string"}, |
|
||||
"name": "token", |
|
||||
"in": "query", |
|
||||
} |
|
||||
], |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,498 +0,0 @@ |
|||||
from unittest.mock import patch |
|
||||
|
|
||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture |
|
||||
def client(): |
|
||||
from docs_src.body.tutorial001_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_body_float(client: TestClient): |
|
||||
response = client.post("/items/", json={"name": "Foo", "price": 50.5}) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"name": "Foo", |
|
||||
"price": 50.5, |
|
||||
"description": None, |
|
||||
"tax": None, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_with_str_float(client: TestClient): |
|
||||
response = client.post("/items/", json={"name": "Foo", "price": "50.5"}) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"name": "Foo", |
|
||||
"price": 50.5, |
|
||||
"description": None, |
|
||||
"tax": None, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_with_str_float_description(client: TestClient): |
|
||||
response = client.post( |
|
||||
"/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"} |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"name": "Foo", |
|
||||
"price": 50.5, |
|
||||
"description": "Some Foo", |
|
||||
"tax": None, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_with_str_float_description_tax(client: TestClient): |
|
||||
response = client.post( |
|
||||
"/items/", |
|
||||
json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"name": "Foo", |
|
||||
"price": 50.5, |
|
||||
"description": "Some Foo", |
|
||||
"tax": 0.3, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_with_only_name(client: TestClient): |
|
||||
response = client.post("/items/", json={"name": "Foo"}) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "price"], |
|
||||
"msg": "Field required", |
|
||||
"input": {"name": "Foo"}, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", "price"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_with_only_name_price(client: TestClient): |
|
||||
response = client.post("/items/", json={"name": "Foo", "price": "twenty"}) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "float_parsing", |
|
||||
"loc": ["body", "price"], |
|
||||
"msg": "Input should be a valid number, unable to parse string as a number", |
|
||||
"input": "twenty", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", "price"], |
|
||||
"msg": "value is not a valid float", |
|
||||
"type": "type_error.float", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_with_no_data(client: TestClient): |
|
||||
response = client.post("/items/", json={}) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "name"], |
|
||||
"msg": "Field required", |
|
||||
"input": {}, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "price"], |
|
||||
"msg": "Field required", |
|
||||
"input": {}, |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", "name"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "price"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_with_none(client: TestClient): |
|
||||
response = client.post("/items/", json=None) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_broken_body(client: TestClient): |
|
||||
response = client.post( |
|
||||
"/items/", |
|
||||
headers={"content-type": "application/json"}, |
|
||||
content="{some broken json}", |
|
||||
) |
|
||||
assert response.status_code == 422, response.text |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "json_invalid", |
|
||||
"loc": ["body", 1], |
|
||||
"msg": "JSON decode error", |
|
||||
"input": {}, |
|
||||
"ctx": { |
|
||||
"error": "Expecting property name enclosed in double quotes" |
|
||||
}, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", 1], |
|
||||
"msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", |
|
||||
"type": "value_error.jsondecode", |
|
||||
"ctx": { |
|
||||
"msg": "Expecting property name enclosed in double quotes", |
|
||||
"doc": "{some broken json}", |
|
||||
"pos": 1, |
|
||||
"lineno": 1, |
|
||||
"colno": 2, |
|
||||
}, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_form_for_json(client: TestClient): |
|
||||
response = client.post("/items/", data={"name": "Foo", "price": 50.5}) |
|
||||
assert response.status_code == 422, response.text |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "model_attributes_type", |
|
||||
"loc": ["body"], |
|
||||
"msg": "Input should be a valid dictionary or object to extract fields from", |
|
||||
"input": "name=Foo&price=50.5", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body"], |
|
||||
"msg": "value is not a valid dict", |
|
||||
"type": "type_error.dict", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_explicit_content_type(client: TestClient): |
|
||||
response = client.post( |
|
||||
"/items/", |
|
||||
content='{"name": "Foo", "price": 50.5}', |
|
||||
headers={"Content-Type": "application/json"}, |
|
||||
) |
|
||||
assert response.status_code == 200, response.text |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_geo_json(client: TestClient): |
|
||||
response = client.post( |
|
||||
"/items/", |
|
||||
content='{"name": "Foo", "price": 50.5}', |
|
||||
headers={"Content-Type": "application/geo+json"}, |
|
||||
) |
|
||||
assert response.status_code == 200, response.text |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_no_content_type_is_json(client: TestClient): |
|
||||
response = client.post( |
|
||||
"/items/", |
|
||||
content='{"name": "Foo", "price": 50.5}', |
|
||||
) |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"name": "Foo", |
|
||||
"description": None, |
|
||||
"price": 50.5, |
|
||||
"tax": None, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_wrong_headers(client: TestClient): |
|
||||
data = '{"name": "Foo", "price": 50.5}' |
|
||||
response = client.post( |
|
||||
"/items/", content=data, headers={"Content-Type": "text/plain"} |
|
||||
) |
|
||||
assert response.status_code == 422, response.text |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "model_attributes_type", |
|
||||
"loc": ["body"], |
|
||||
"msg": "Input should be a valid dictionary or object to extract fields from", |
|
||||
"input": '{"name": "Foo", "price": 50.5}', |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body"], |
|
||||
"msg": "value is not a valid dict", |
|
||||
"type": "type_error.dict", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
response = client.post( |
|
||||
"/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} |
|
||||
) |
|
||||
assert response.status_code == 422, response.text |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "model_attributes_type", |
|
||||
"loc": ["body"], |
|
||||
"msg": "Input should be a valid dictionary or object to extract fields from", |
|
||||
"input": '{"name": "Foo", "price": 50.5}', |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body"], |
|
||||
"msg": "value is not a valid dict", |
|
||||
"type": "type_error.dict", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
response = client.post( |
|
||||
"/items/", content=data, headers={"Content-Type": "application/not-really-json"} |
|
||||
) |
|
||||
assert response.status_code == 422, response.text |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "model_attributes_type", |
|
||||
"loc": ["body"], |
|
||||
"msg": "Input should be a valid dictionary or object to extract fields from", |
|
||||
"input": '{"name": "Foo", "price": 50.5}', |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body"], |
|
||||
"msg": "value is not a valid dict", |
|
||||
"type": "type_error.dict", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_other_exceptions(client: TestClient): |
|
||||
with patch("json.loads", side_effect=Exception): |
|
||||
response = client.post("/items/", json={"test": "test2"}) |
|
||||
assert response.status_code == 400, response.text |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/": { |
|
||||
"post": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Create Item", |
|
||||
"operationId": "create_item_items__post", |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"price": {"title": "Price", "type": "number"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "Description", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Description", "type": "string"} |
|
||||
), |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,203 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_fields.tutorial001_an import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
def test_items_5(client: TestClient): |
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 5, |
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
def test_items_6(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/6", |
|
||||
json={ |
|
||||
"item": { |
|
||||
"name": "Bar", |
|
||||
"price": 0.2, |
|
||||
"description": "Some bar", |
|
||||
"tax": "5.4", |
|
||||
} |
|
||||
}, |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 6, |
|
||||
"item": { |
|
||||
"name": "Bar", |
|
||||
"price": 0.2, |
|
||||
"description": "Some bar", |
|
||||
"tax": 5.4, |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
def test_invalid_price(client: TestClient): |
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "greater_than", |
|
||||
"loc": ["body", "item", "price"], |
|
||||
"msg": "Input should be greater than 0", |
|
||||
"input": -3.0, |
|
||||
"ctx": {"gt": 0.0}, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"ctx": {"limit_value": 0}, |
|
||||
"loc": ["body", "item", "price"], |
|
||||
"msg": "ensure this value is greater than 0", |
|
||||
"type": "value_error.number.not_gt", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "integer"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/Body_update_item_items__item_id__put" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "The description of the item", |
|
||||
"anyOf": [ |
|
||||
{"maxLength": 300, "type": "string"}, |
|
||||
{"type": "null"}, |
|
||||
], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"title": "The description of the item", |
|
||||
"maxLength": 300, |
|
||||
"type": "string", |
|
||||
} |
|
||||
), |
|
||||
"price": { |
|
||||
"title": "Price", |
|
||||
"exclusiveMinimum": 0.0, |
|
||||
"type": "number", |
|
||||
"description": "The price must be greater than zero", |
|
||||
}, |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"Body_update_item_items__item_id__put": { |
|
||||
"title": "Body_update_item_items__item_id__put", |
|
||||
"required": ["item"], |
|
||||
"type": "object", |
|
||||
"properties": {"item": {"$ref": "#/components/schemas/Item"}}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,209 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_fields.tutorial001_an_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_items_5(client: TestClient): |
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 5, |
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_items_6(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/6", |
|
||||
json={ |
|
||||
"item": { |
|
||||
"name": "Bar", |
|
||||
"price": 0.2, |
|
||||
"description": "Some bar", |
|
||||
"tax": "5.4", |
|
||||
} |
|
||||
}, |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 6, |
|
||||
"item": { |
|
||||
"name": "Bar", |
|
||||
"price": 0.2, |
|
||||
"description": "Some bar", |
|
||||
"tax": 5.4, |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_invalid_price(client: TestClient): |
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "greater_than", |
|
||||
"loc": ["body", "item", "price"], |
|
||||
"msg": "Input should be greater than 0", |
|
||||
"input": -3.0, |
|
||||
"ctx": {"gt": 0.0}, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"ctx": {"limit_value": 0}, |
|
||||
"loc": ["body", "item", "price"], |
|
||||
"msg": "ensure this value is greater than 0", |
|
||||
"type": "value_error.number.not_gt", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "integer"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/Body_update_item_items__item_id__put" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "The description of the item", |
|
||||
"anyOf": [ |
|
||||
{"maxLength": 300, "type": "string"}, |
|
||||
{"type": "null"}, |
|
||||
], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"title": "The description of the item", |
|
||||
"maxLength": 300, |
|
||||
"type": "string", |
|
||||
} |
|
||||
), |
|
||||
"price": { |
|
||||
"title": "Price", |
|
||||
"exclusiveMinimum": 0.0, |
|
||||
"type": "number", |
|
||||
"description": "The price must be greater than zero", |
|
||||
}, |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"Body_update_item_items__item_id__put": { |
|
||||
"title": "Body_update_item_items__item_id__put", |
|
||||
"required": ["item"], |
|
||||
"type": "object", |
|
||||
"properties": {"item": {"$ref": "#/components/schemas/Item"}}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,209 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py39 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_fields.tutorial001_an_py39 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_items_5(client: TestClient): |
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 5, |
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_items_6(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/6", |
|
||||
json={ |
|
||||
"item": { |
|
||||
"name": "Bar", |
|
||||
"price": 0.2, |
|
||||
"description": "Some bar", |
|
||||
"tax": "5.4", |
|
||||
} |
|
||||
}, |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 6, |
|
||||
"item": { |
|
||||
"name": "Bar", |
|
||||
"price": 0.2, |
|
||||
"description": "Some bar", |
|
||||
"tax": 5.4, |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_invalid_price(client: TestClient): |
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "greater_than", |
|
||||
"loc": ["body", "item", "price"], |
|
||||
"msg": "Input should be greater than 0", |
|
||||
"input": -3.0, |
|
||||
"ctx": {"gt": 0.0}, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"ctx": {"limit_value": 0}, |
|
||||
"loc": ["body", "item", "price"], |
|
||||
"msg": "ensure this value is greater than 0", |
|
||||
"type": "value_error.number.not_gt", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "integer"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/Body_update_item_items__item_id__put" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "The description of the item", |
|
||||
"anyOf": [ |
|
||||
{"maxLength": 300, "type": "string"}, |
|
||||
{"type": "null"}, |
|
||||
], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"title": "The description of the item", |
|
||||
"maxLength": 300, |
|
||||
"type": "string", |
|
||||
} |
|
||||
), |
|
||||
"price": { |
|
||||
"title": "Price", |
|
||||
"exclusiveMinimum": 0.0, |
|
||||
"type": "number", |
|
||||
"description": "The price must be greater than zero", |
|
||||
}, |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"Body_update_item_items__item_id__put": { |
|
||||
"title": "Body_update_item_items__item_id__put", |
|
||||
"required": ["item"], |
|
||||
"type": "object", |
|
||||
"properties": {"item": {"$ref": "#/components/schemas/Item"}}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,209 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_fields.tutorial001_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_items_5(client: TestClient): |
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 5, |
|
||||
"item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_items_6(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/6", |
|
||||
json={ |
|
||||
"item": { |
|
||||
"name": "Bar", |
|
||||
"price": 0.2, |
|
||||
"description": "Some bar", |
|
||||
"tax": "5.4", |
|
||||
} |
|
||||
}, |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 6, |
|
||||
"item": { |
|
||||
"name": "Bar", |
|
||||
"price": 0.2, |
|
||||
"description": "Some bar", |
|
||||
"tax": 5.4, |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_invalid_price(client: TestClient): |
|
||||
response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "greater_than", |
|
||||
"loc": ["body", "item", "price"], |
|
||||
"msg": "Input should be greater than 0", |
|
||||
"input": -3.0, |
|
||||
"ctx": {"gt": 0.0}, |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"ctx": {"limit_value": 0}, |
|
||||
"loc": ["body", "item", "price"], |
|
||||
"msg": "ensure this value is greater than 0", |
|
||||
"type": "value_error.number.not_gt", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "integer"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/Body_update_item_items__item_id__put" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "The description of the item", |
|
||||
"anyOf": [ |
|
||||
{"maxLength": 300, "type": "string"}, |
|
||||
{"type": "null"}, |
|
||||
], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"title": "The description of the item", |
|
||||
"maxLength": 300, |
|
||||
"type": "string", |
|
||||
} |
|
||||
), |
|
||||
"price": { |
|
||||
"title": "Price", |
|
||||
"exclusiveMinimum": 0.0, |
|
||||
"type": "number", |
|
||||
"description": "The price must be greater than zero", |
|
||||
}, |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"Body_update_item_items__item_id__put": { |
|
||||
"title": "Body_update_item_items__item_id__put", |
|
||||
"required": ["item"], |
|
||||
"type": "object", |
|
||||
"properties": {"item": {"$ref": "#/components/schemas/Item"}}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,206 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_multiple_params.tutorial001_an import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
def test_post_body_q_bar_content(client: TestClient): |
|
||||
response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 5, |
|
||||
"item": { |
|
||||
"name": "Foo", |
|
||||
"price": 50.5, |
|
||||
"description": None, |
|
||||
"tax": None, |
|
||||
}, |
|
||||
"q": "bar", |
|
||||
} |
|
||||
|
|
||||
|
|
||||
def test_post_no_body_q_bar(client: TestClient): |
|
||||
response = client.put("/items/5?q=bar", json=None) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"item_id": 5, "q": "bar"} |
|
||||
|
|
||||
|
|
||||
def test_post_no_body(client: TestClient): |
|
||||
response = client.put("/items/5", json=None) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"item_id": 5} |
|
||||
|
|
||||
|
|
||||
def test_post_id_foo(client: TestClient): |
|
||||
response = client.put("/items/foo", json=None) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "int_parsing", |
|
||||
"loc": ["path", "item_id"], |
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer", |
|
||||
"input": "foo", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["path", "item_id"], |
|
||||
"msg": "value is not a valid integer", |
|
||||
"type": "type_error.integer", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": { |
|
||||
"title": "The ID of the item to get", |
|
||||
"maximum": 1000.0, |
|
||||
"minimum": 0.0, |
|
||||
"type": "integer", |
|
||||
}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [ |
|
||||
{"$ref": "#/components/schemas/Item"}, |
|
||||
{"type": "null"}, |
|
||||
], |
|
||||
"title": "Item", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"$ref": "#/components/schemas/Item"} |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "Description", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Description", "type": "string"} |
|
||||
), |
|
||||
"price": {"title": "Price", "type": "number"}, |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,213 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_multiple_params.tutorial001_an_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_body_q_bar_content(client: TestClient): |
|
||||
response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 5, |
|
||||
"item": { |
|
||||
"name": "Foo", |
|
||||
"price": 50.5, |
|
||||
"description": None, |
|
||||
"tax": None, |
|
||||
}, |
|
||||
"q": "bar", |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_no_body_q_bar(client: TestClient): |
|
||||
response = client.put("/items/5?q=bar", json=None) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"item_id": 5, "q": "bar"} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_no_body(client: TestClient): |
|
||||
response = client.put("/items/5", json=None) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"item_id": 5} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_id_foo(client: TestClient): |
|
||||
response = client.put("/items/foo", json=None) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "int_parsing", |
|
||||
"loc": ["path", "item_id"], |
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer", |
|
||||
"input": "foo", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["path", "item_id"], |
|
||||
"msg": "value is not a valid integer", |
|
||||
"type": "type_error.integer", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": { |
|
||||
"title": "The ID of the item to get", |
|
||||
"maximum": 1000.0, |
|
||||
"minimum": 0.0, |
|
||||
"type": "integer", |
|
||||
}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [ |
|
||||
{"$ref": "#/components/schemas/Item"}, |
|
||||
{"type": "null"}, |
|
||||
], |
|
||||
"title": "Item", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"$ref": "#/components/schemas/Item"} |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "Description", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Description", "type": "string"} |
|
||||
), |
|
||||
"price": {"title": "Price", "type": "number"}, |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,213 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py39 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_multiple_params.tutorial001_an_py39 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_post_body_q_bar_content(client: TestClient): |
|
||||
response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 5, |
|
||||
"item": { |
|
||||
"name": "Foo", |
|
||||
"price": 50.5, |
|
||||
"description": None, |
|
||||
"tax": None, |
|
||||
}, |
|
||||
"q": "bar", |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_post_no_body_q_bar(client: TestClient): |
|
||||
response = client.put("/items/5?q=bar", json=None) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"item_id": 5, "q": "bar"} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_post_no_body(client: TestClient): |
|
||||
response = client.put("/items/5", json=None) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"item_id": 5} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_post_id_foo(client: TestClient): |
|
||||
response = client.put("/items/foo", json=None) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "int_parsing", |
|
||||
"loc": ["path", "item_id"], |
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer", |
|
||||
"input": "foo", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["path", "item_id"], |
|
||||
"msg": "value is not a valid integer", |
|
||||
"type": "type_error.integer", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": { |
|
||||
"title": "The ID of the item to get", |
|
||||
"maximum": 1000.0, |
|
||||
"minimum": 0.0, |
|
||||
"type": "integer", |
|
||||
}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [ |
|
||||
{"$ref": "#/components/schemas/Item"}, |
|
||||
{"type": "null"}, |
|
||||
], |
|
||||
"title": "Item", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"$ref": "#/components/schemas/Item"} |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "Description", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Description", "type": "string"} |
|
||||
), |
|
||||
"price": {"title": "Price", "type": "number"}, |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,213 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_multiple_params.tutorial001_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_body_q_bar_content(client: TestClient): |
|
||||
response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 5, |
|
||||
"item": { |
|
||||
"name": "Foo", |
|
||||
"price": 50.5, |
|
||||
"description": None, |
|
||||
"tax": None, |
|
||||
}, |
|
||||
"q": "bar", |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_no_body_q_bar(client: TestClient): |
|
||||
response = client.put("/items/5?q=bar", json=None) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"item_id": 5, "q": "bar"} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_no_body(client: TestClient): |
|
||||
response = client.put("/items/5", json=None) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == {"item_id": 5} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_id_foo(client: TestClient): |
|
||||
response = client.put("/items/foo", json=None) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "int_parsing", |
|
||||
"loc": ["path", "item_id"], |
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer", |
|
||||
"input": "foo", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["path", "item_id"], |
|
||||
"msg": "value is not a valid integer", |
|
||||
"type": "type_error.integer", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": { |
|
||||
"title": "The ID of the item to get", |
|
||||
"maximum": 1000.0, |
|
||||
"minimum": 0.0, |
|
||||
"type": "integer", |
|
||||
}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [ |
|
||||
{"$ref": "#/components/schemas/Item"}, |
|
||||
{"type": "null"}, |
|
||||
], |
|
||||
"title": "Item", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"$ref": "#/components/schemas/Item"} |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "Description", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Description", "type": "string"} |
|
||||
), |
|
||||
"price": {"title": "Price", "type": "number"}, |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,273 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_multiple_params.tutorial003_an import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
def test_post_body_valid(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/5", |
|
||||
json={ |
|
||||
"importance": 2, |
|
||||
"item": {"name": "Foo", "price": 50.5}, |
|
||||
"user": {"username": "Dave"}, |
|
||||
}, |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 5, |
|
||||
"importance": 2, |
|
||||
"item": { |
|
||||
"name": "Foo", |
|
||||
"price": 50.5, |
|
||||
"description": None, |
|
||||
"tax": None, |
|
||||
}, |
|
||||
"user": {"username": "Dave", "full_name": None}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
def test_post_body_no_data(client: TestClient): |
|
||||
response = client.put("/items/5", json=None) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_post_body_empty_list(client: TestClient): |
|
||||
response = client.put("/items/5", json=[]) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "integer"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/Body_update_item_items__item_id__put" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "Description", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Description", "type": "string"} |
|
||||
), |
|
||||
"price": {"title": "Price", "type": "number"}, |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"User": { |
|
||||
"title": "User", |
|
||||
"required": ["username"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"username": {"title": "Username", "type": "string"}, |
|
||||
"full_name": IsDict( |
|
||||
{ |
|
||||
"title": "Full Name", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Full Name", "type": "string"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"Body_update_item_items__item_id__put": { |
|
||||
"title": "Body_update_item_items__item_id__put", |
|
||||
"required": ["item", "user", "importance"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"item": {"$ref": "#/components/schemas/Item"}, |
|
||||
"user": {"$ref": "#/components/schemas/User"}, |
|
||||
"importance": {"title": "Importance", "type": "integer"}, |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,279 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_multiple_params.tutorial003_an_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_body_valid(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/5", |
|
||||
json={ |
|
||||
"importance": 2, |
|
||||
"item": {"name": "Foo", "price": 50.5}, |
|
||||
"user": {"username": "Dave"}, |
|
||||
}, |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 5, |
|
||||
"importance": 2, |
|
||||
"item": { |
|
||||
"name": "Foo", |
|
||||
"price": 50.5, |
|
||||
"description": None, |
|
||||
"tax": None, |
|
||||
}, |
|
||||
"user": {"username": "Dave", "full_name": None}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_body_no_data(client: TestClient): |
|
||||
response = client.put("/items/5", json=None) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_body_empty_list(client: TestClient): |
|
||||
response = client.put("/items/5", json=[]) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "integer"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/Body_update_item_items__item_id__put" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "Description", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Description", "type": "string"} |
|
||||
), |
|
||||
"price": {"title": "Price", "type": "number"}, |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"User": { |
|
||||
"title": "User", |
|
||||
"required": ["username"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"username": {"title": "Username", "type": "string"}, |
|
||||
"full_name": IsDict( |
|
||||
{ |
|
||||
"title": "Full Name", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Full Name", "type": "string"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"Body_update_item_items__item_id__put": { |
|
||||
"title": "Body_update_item_items__item_id__put", |
|
||||
"required": ["item", "user", "importance"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"item": {"$ref": "#/components/schemas/Item"}, |
|
||||
"user": {"$ref": "#/components/schemas/User"}, |
|
||||
"importance": {"title": "Importance", "type": "integer"}, |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,279 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py39 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_multiple_params.tutorial003_an_py39 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_post_body_valid(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/5", |
|
||||
json={ |
|
||||
"importance": 2, |
|
||||
"item": {"name": "Foo", "price": 50.5}, |
|
||||
"user": {"username": "Dave"}, |
|
||||
}, |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 5, |
|
||||
"importance": 2, |
|
||||
"item": { |
|
||||
"name": "Foo", |
|
||||
"price": 50.5, |
|
||||
"description": None, |
|
||||
"tax": None, |
|
||||
}, |
|
||||
"user": {"username": "Dave", "full_name": None}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_post_body_no_data(client: TestClient): |
|
||||
response = client.put("/items/5", json=None) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_post_body_empty_list(client: TestClient): |
|
||||
response = client.put("/items/5", json=[]) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "integer"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/Body_update_item_items__item_id__put" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "Description", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Description", "type": "string"} |
|
||||
), |
|
||||
"price": {"title": "Price", "type": "number"}, |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"User": { |
|
||||
"title": "User", |
|
||||
"required": ["username"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"username": {"title": "Username", "type": "string"}, |
|
||||
"full_name": IsDict( |
|
||||
{ |
|
||||
"title": "Full Name", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Full Name", "type": "string"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"Body_update_item_items__item_id__put": { |
|
||||
"title": "Body_update_item_items__item_id__put", |
|
||||
"required": ["item", "user", "importance"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"item": {"$ref": "#/components/schemas/Item"}, |
|
||||
"user": {"$ref": "#/components/schemas/User"}, |
|
||||
"importance": {"title": "Importance", "type": "integer"}, |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,279 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_multiple_params.tutorial003_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_body_valid(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/5", |
|
||||
json={ |
|
||||
"importance": 2, |
|
||||
"item": {"name": "Foo", "price": 50.5}, |
|
||||
"user": {"username": "Dave"}, |
|
||||
}, |
|
||||
) |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"item_id": 5, |
|
||||
"importance": 2, |
|
||||
"item": { |
|
||||
"name": "Foo", |
|
||||
"price": 50.5, |
|
||||
"description": None, |
|
||||
"tax": None, |
|
||||
}, |
|
||||
"user": {"username": "Dave", "full_name": None}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_body_no_data(client: TestClient): |
|
||||
response = client.put("/items/5", json=None) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_post_body_empty_list(client: TestClient): |
|
||||
response = client.put("/items/5", json=[]) |
|
||||
assert response.status_code == 422 |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
{ |
|
||||
"type": "missing", |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "Field required", |
|
||||
"input": None, |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", "item"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "user"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
{ |
|
||||
"loc": ["body", "importance"], |
|
||||
"msg": "field required", |
|
||||
"type": "value_error.missing", |
|
||||
}, |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "integer"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/Body_update_item_items__item_id__put" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"required": ["name", "price"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": IsDict( |
|
||||
{ |
|
||||
"title": "Description", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Description", "type": "string"} |
|
||||
), |
|
||||
"price": {"title": "Price", "type": "number"}, |
|
||||
"tax": IsDict( |
|
||||
{ |
|
||||
"title": "Tax", |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Tax", "type": "number"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"User": { |
|
||||
"title": "User", |
|
||||
"required": ["username"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"username": {"title": "Username", "type": "string"}, |
|
||||
"full_name": IsDict( |
|
||||
{ |
|
||||
"title": "Full Name", |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Full Name", "type": "string"} |
|
||||
), |
|
||||
}, |
|
||||
}, |
|
||||
"Body_update_item_items__item_id__put": { |
|
||||
"title": "Body_update_item_items__item_id__put", |
|
||||
"required": ["item", "user", "importance"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"item": {"$ref": "#/components/schemas/Item"}, |
|
||||
"user": {"$ref": "#/components/schemas/User"}, |
|
||||
"importance": {"title": "Importance", "type": "integer"}, |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,128 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py39 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_nested_models.tutorial009_py39 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_post_body(client: TestClient): |
|
||||
data = {"2": 2.2, "3": 3.3} |
|
||||
response = client.post("/index-weights/", json=data) |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == data |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_post_invalid_body(client: TestClient): |
|
||||
data = {"foo": 2.2, "3": 3.3} |
|
||||
response = client.post("/index-weights/", json=data) |
|
||||
assert response.status_code == 422, response.text |
|
||||
assert response.json() == IsDict( |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"type": "int_parsing", |
|
||||
"loc": ["body", "foo", "[key]"], |
|
||||
"msg": "Input should be a valid integer, unable to parse string as an integer", |
|
||||
"input": "foo", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) | IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{ |
|
||||
"detail": [ |
|
||||
{ |
|
||||
"loc": ["body", "__key__"], |
|
||||
"msg": "value is not a valid integer", |
|
||||
"type": "type_error.integer", |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/index-weights/": { |
|
||||
"post": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Create Index Weights", |
|
||||
"operationId": "create_index_weights_index_weights__post", |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"title": "Weights", |
|
||||
"type": "object", |
|
||||
"additionalProperties": {"type": "number"}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,317 +0,0 @@ |
|||||
import pytest |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_updates.tutorial001_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_get(client: TestClient): |
|
||||
response = client.get("/items/baz") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"name": "Baz", |
|
||||
"description": None, |
|
||||
"price": 50.2, |
|
||||
"tax": 10.5, |
|
||||
"tags": [], |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_put(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/bar", json={"name": "Barz", "price": 3, "description": None} |
|
||||
) |
|
||||
assert response.json() == { |
|
||||
"name": "Barz", |
|
||||
"description": None, |
|
||||
"price": 3, |
|
||||
"tax": 10.5, |
|
||||
"tags": [], |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
@needs_pydanticv2 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Item", |
|
||||
"operationId": "read_item_items__item_id__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "string"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
}, |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "string"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"type": "object", |
|
||||
"title": "Item", |
|
||||
"properties": { |
|
||||
"name": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Name", |
|
||||
}, |
|
||||
"description": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Description", |
|
||||
}, |
|
||||
"price": { |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
"title": "Price", |
|
||||
}, |
|
||||
"tax": {"title": "Tax", "type": "number", "default": 10.5}, |
|
||||
"tags": { |
|
||||
"title": "Tags", |
|
||||
"type": "array", |
|
||||
"items": {"type": "string"}, |
|
||||
"default": [], |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
@needs_py310 |
|
||||
@needs_pydanticv1 |
|
||||
def test_openapi_schema_pv1(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Item", |
|
||||
"operationId": "read_item_items__item_id__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "string"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
}, |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "string"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": {"title": "Description", "type": "string"}, |
|
||||
"price": {"title": "Price", "type": "number"}, |
|
||||
"tax": {"title": "Tax", "type": "number", "default": 10.5}, |
|
||||
"tags": { |
|
||||
"title": "Tags", |
|
||||
"type": "array", |
|
||||
"items": {"type": "string"}, |
|
||||
"default": [], |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,317 +0,0 @@ |
|||||
import pytest |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py39, needs_pydanticv1, needs_pydanticv2 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.body_updates.tutorial001_py39 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_get(client: TestClient): |
|
||||
response = client.get("/items/baz") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"name": "Baz", |
|
||||
"description": None, |
|
||||
"price": 50.2, |
|
||||
"tax": 10.5, |
|
||||
"tags": [], |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_put(client: TestClient): |
|
||||
response = client.put( |
|
||||
"/items/bar", json={"name": "Barz", "price": 3, "description": None} |
|
||||
) |
|
||||
assert response.json() == { |
|
||||
"name": "Barz", |
|
||||
"description": None, |
|
||||
"price": 3, |
|
||||
"tax": 10.5, |
|
||||
"tags": [], |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
@needs_pydanticv2 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Item", |
|
||||
"operationId": "read_item_items__item_id__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "string"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
}, |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "string"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"type": "object", |
|
||||
"title": "Item", |
|
||||
"properties": { |
|
||||
"name": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Name", |
|
||||
}, |
|
||||
"description": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Description", |
|
||||
}, |
|
||||
"price": { |
|
||||
"anyOf": [{"type": "number"}, {"type": "null"}], |
|
||||
"title": "Price", |
|
||||
}, |
|
||||
"tax": {"title": "Tax", "type": "number", "default": 10.5}, |
|
||||
"tags": { |
|
||||
"title": "Tags", |
|
||||
"type": "array", |
|
||||
"items": {"type": "string"}, |
|
||||
"default": [], |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
|
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
@needs_py39 |
|
||||
@needs_pydanticv1 |
|
||||
def test_openapi_schema_pv1(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/{item_id}": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Item", |
|
||||
"operationId": "read_item_items__item_id__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "string"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
}, |
|
||||
"put": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Update Item", |
|
||||
"operationId": "update_item_items__item_id__put", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": True, |
|
||||
"schema": {"title": "Item Id", "type": "string"}, |
|
||||
"name": "item_id", |
|
||||
"in": "path", |
|
||||
} |
|
||||
], |
|
||||
"requestBody": { |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": {"$ref": "#/components/schemas/Item"} |
|
||||
} |
|
||||
}, |
|
||||
"required": True, |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"Item": { |
|
||||
"title": "Item", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"name": {"title": "Name", "type": "string"}, |
|
||||
"description": {"title": "Description", "type": "string"}, |
|
||||
"price": {"title": "Price", "type": "number"}, |
|
||||
"tax": {"title": "Tax", "type": "number", "default": 10.5}, |
|
||||
"tags": { |
|
||||
"title": "Tags", |
|
||||
"type": "array", |
|
||||
"items": {"type": "string"}, |
|
||||
"default": [], |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,108 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from docs_src.cookie_params.tutorial001_an import app |
|
||||
|
|
||||
|
|
||||
@pytest.mark.parametrize( |
|
||||
"path,cookies,expected_status,expected_response", |
|
||||
[ |
|
||||
("/items", None, 200, {"ads_id": None}), |
|
||||
("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), |
|
||||
( |
|
||||
"/items", |
|
||||
{"ads_id": "ads_track", "session": "cookiesession"}, |
|
||||
200, |
|
||||
{"ads_id": "ads_track"}, |
|
||||
), |
|
||||
("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), |
|
||||
], |
|
||||
) |
|
||||
def test(path, cookies, expected_status, expected_response): |
|
||||
client = TestClient(app, cookies=cookies) |
|
||||
response = client.get(path) |
|
||||
assert response.status_code == expected_status |
|
||||
assert response.json() == expected_response |
|
||||
|
|
||||
|
|
||||
def test_openapi_schema(): |
|
||||
client = TestClient(app) |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Ads Id", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Ads Id", "type": "string"} |
|
||||
), |
|
||||
"name": "ads_id", |
|
||||
"in": "cookie", |
|
||||
} |
|
||||
], |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,114 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
@pytest.mark.parametrize( |
|
||||
"path,cookies,expected_status,expected_response", |
|
||||
[ |
|
||||
("/items", None, 200, {"ads_id": None}), |
|
||||
("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), |
|
||||
( |
|
||||
"/items", |
|
||||
{"ads_id": "ads_track", "session": "cookiesession"}, |
|
||||
200, |
|
||||
{"ads_id": "ads_track"}, |
|
||||
), |
|
||||
("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), |
|
||||
], |
|
||||
) |
|
||||
def test(path, cookies, expected_status, expected_response): |
|
||||
from docs_src.cookie_params.tutorial001_an_py310 import app |
|
||||
|
|
||||
client = TestClient(app, cookies=cookies) |
|
||||
response = client.get(path) |
|
||||
assert response.status_code == expected_status |
|
||||
assert response.json() == expected_response |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_openapi_schema(): |
|
||||
from docs_src.cookie_params.tutorial001_an_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Ads Id", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Ads Id", "type": "string"} |
|
||||
), |
|
||||
"name": "ads_id", |
|
||||
"in": "cookie", |
|
||||
} |
|
||||
], |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,114 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py39 |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
@pytest.mark.parametrize( |
|
||||
"path,cookies,expected_status,expected_response", |
|
||||
[ |
|
||||
("/items", None, 200, {"ads_id": None}), |
|
||||
("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), |
|
||||
( |
|
||||
"/items", |
|
||||
{"ads_id": "ads_track", "session": "cookiesession"}, |
|
||||
200, |
|
||||
{"ads_id": "ads_track"}, |
|
||||
), |
|
||||
("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), |
|
||||
], |
|
||||
) |
|
||||
def test(path, cookies, expected_status, expected_response): |
|
||||
from docs_src.cookie_params.tutorial001_an_py39 import app |
|
||||
|
|
||||
client = TestClient(app, cookies=cookies) |
|
||||
response = client.get(path) |
|
||||
assert response.status_code == expected_status |
|
||||
assert response.json() == expected_response |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_openapi_schema(): |
|
||||
from docs_src.cookie_params.tutorial001_an_py39 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Ads Id", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Ads Id", "type": "string"} |
|
||||
), |
|
||||
"name": "ads_id", |
|
||||
"in": "cookie", |
|
||||
} |
|
||||
], |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,114 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
@pytest.mark.parametrize( |
|
||||
"path,cookies,expected_status,expected_response", |
|
||||
[ |
|
||||
("/items", None, 200, {"ads_id": None}), |
|
||||
("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), |
|
||||
( |
|
||||
"/items", |
|
||||
{"ads_id": "ads_track", "session": "cookiesession"}, |
|
||||
200, |
|
||||
{"ads_id": "ads_track"}, |
|
||||
), |
|
||||
("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), |
|
||||
], |
|
||||
) |
|
||||
def test(path, cookies, expected_status, expected_response): |
|
||||
from docs_src.cookie_params.tutorial001_py310 import app |
|
||||
|
|
||||
client = TestClient(app, cookies=cookies) |
|
||||
response = client.get(path) |
|
||||
assert response.status_code == expected_status |
|
||||
assert response.json() == expected_response |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_openapi_schema(): |
|
||||
from docs_src.cookie_params.tutorial001_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200 |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Ads Id", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Ads Id", "type": "string"} |
|
||||
), |
|
||||
"name": "ads_id", |
|
||||
"in": "cookie", |
|
||||
} |
|
||||
], |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,183 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from docs_src.dependencies.tutorial001_an import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
|
|
||||
|
|
||||
@pytest.mark.parametrize( |
|
||||
"path,expected_status,expected_response", |
|
||||
[ |
|
||||
("/items", 200, {"q": None, "skip": 0, "limit": 100}), |
|
||||
("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), |
|
||||
("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), |
|
||||
("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), |
|
||||
("/users", 200, {"q": None, "skip": 0, "limit": 100}), |
|
||||
], |
|
||||
) |
|
||||
def test_get(path, expected_status, expected_response): |
|
||||
response = client.get(path) |
|
||||
assert response.status_code == expected_status |
|
||||
assert response.json() == expected_response |
|
||||
|
|
||||
|
|
||||
def test_openapi_schema(): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Skip", |
|
||||
"type": "integer", |
|
||||
"default": 0, |
|
||||
}, |
|
||||
"name": "skip", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Limit", |
|
||||
"type": "integer", |
|
||||
"default": 100, |
|
||||
}, |
|
||||
"name": "limit", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
} |
|
||||
}, |
|
||||
"/users/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Users", |
|
||||
"operationId": "read_users_users__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Skip", |
|
||||
"type": "integer", |
|
||||
"default": 0, |
|
||||
}, |
|
||||
"name": "skip", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Limit", |
|
||||
"type": "integer", |
|
||||
"default": 100, |
|
||||
}, |
|
||||
"name": "limit", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,191 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.dependencies.tutorial001_an_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
@pytest.mark.parametrize( |
|
||||
"path,expected_status,expected_response", |
|
||||
[ |
|
||||
("/items", 200, {"q": None, "skip": 0, "limit": 100}), |
|
||||
("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), |
|
||||
("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), |
|
||||
("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), |
|
||||
("/users", 200, {"q": None, "skip": 0, "limit": 100}), |
|
||||
], |
|
||||
) |
|
||||
def test_get(path, expected_status, expected_response, client: TestClient): |
|
||||
response = client.get(path) |
|
||||
assert response.status_code == expected_status |
|
||||
assert response.json() == expected_response |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Skip", |
|
||||
"type": "integer", |
|
||||
"default": 0, |
|
||||
}, |
|
||||
"name": "skip", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Limit", |
|
||||
"type": "integer", |
|
||||
"default": 100, |
|
||||
}, |
|
||||
"name": "limit", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
} |
|
||||
}, |
|
||||
"/users/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Users", |
|
||||
"operationId": "read_users_users__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Skip", |
|
||||
"type": "integer", |
|
||||
"default": 0, |
|
||||
}, |
|
||||
"name": "skip", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Limit", |
|
||||
"type": "integer", |
|
||||
"default": 100, |
|
||||
}, |
|
||||
"name": "limit", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,191 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py39 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.dependencies.tutorial001_an_py39 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
@pytest.mark.parametrize( |
|
||||
"path,expected_status,expected_response", |
|
||||
[ |
|
||||
("/items", 200, {"q": None, "skip": 0, "limit": 100}), |
|
||||
("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), |
|
||||
("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), |
|
||||
("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), |
|
||||
("/users", 200, {"q": None, "skip": 0, "limit": 100}), |
|
||||
], |
|
||||
) |
|
||||
def test_get(path, expected_status, expected_response, client: TestClient): |
|
||||
response = client.get(path) |
|
||||
assert response.status_code == expected_status |
|
||||
assert response.json() == expected_response |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Skip", |
|
||||
"type": "integer", |
|
||||
"default": 0, |
|
||||
}, |
|
||||
"name": "skip", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Limit", |
|
||||
"type": "integer", |
|
||||
"default": 100, |
|
||||
}, |
|
||||
"name": "limit", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
} |
|
||||
}, |
|
||||
"/users/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Users", |
|
||||
"operationId": "read_users_users__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Skip", |
|
||||
"type": "integer", |
|
||||
"default": 0, |
|
||||
}, |
|
||||
"name": "skip", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Limit", |
|
||||
"type": "integer", |
|
||||
"default": 100, |
|
||||
}, |
|
||||
"name": "limit", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,191 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.dependencies.tutorial001_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
@pytest.mark.parametrize( |
|
||||
"path,expected_status,expected_response", |
|
||||
[ |
|
||||
("/items", 200, {"q": None, "skip": 0, "limit": 100}), |
|
||||
("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), |
|
||||
("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), |
|
||||
("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), |
|
||||
("/users", 200, {"q": None, "skip": 0, "limit": 100}), |
|
||||
], |
|
||||
) |
|
||||
def test_get(path, expected_status, expected_response, client: TestClient): |
|
||||
response = client.get(path) |
|
||||
assert response.status_code == expected_status |
|
||||
assert response.json() == expected_response |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Skip", |
|
||||
"type": "integer", |
|
||||
"default": 0, |
|
||||
}, |
|
||||
"name": "skip", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Limit", |
|
||||
"type": "integer", |
|
||||
"default": 100, |
|
||||
}, |
|
||||
"name": "limit", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
} |
|
||||
}, |
|
||||
"/users/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Users", |
|
||||
"operationId": "read_users_users__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Skip", |
|
||||
"type": "integer", |
|
||||
"default": 0, |
|
||||
}, |
|
||||
"name": "skip", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Limit", |
|
||||
"type": "integer", |
|
||||
"default": 100, |
|
||||
}, |
|
||||
"name": "limit", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,162 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from docs_src.dependencies.tutorial004_an import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
|
|
||||
|
|
||||
@pytest.mark.parametrize( |
|
||||
"path,expected_status,expected_response", |
|
||||
[ |
|
||||
( |
|
||||
"/items", |
|
||||
200, |
|
||||
{ |
|
||||
"items": [ |
|
||||
{"item_name": "Foo"}, |
|
||||
{"item_name": "Bar"}, |
|
||||
{"item_name": "Baz"}, |
|
||||
] |
|
||||
}, |
|
||||
), |
|
||||
( |
|
||||
"/items?q=foo", |
|
||||
200, |
|
||||
{ |
|
||||
"items": [ |
|
||||
{"item_name": "Foo"}, |
|
||||
{"item_name": "Bar"}, |
|
||||
{"item_name": "Baz"}, |
|
||||
], |
|
||||
"q": "foo", |
|
||||
}, |
|
||||
), |
|
||||
( |
|
||||
"/items?q=foo&skip=1", |
|
||||
200, |
|
||||
{"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, |
|
||||
), |
|
||||
( |
|
||||
"/items?q=bar&limit=2", |
|
||||
200, |
|
||||
{"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, |
|
||||
), |
|
||||
( |
|
||||
"/items?q=bar&skip=1&limit=1", |
|
||||
200, |
|
||||
{"items": [{"item_name": "Bar"}], "q": "bar"}, |
|
||||
), |
|
||||
( |
|
||||
"/items?limit=1&q=bar&skip=1", |
|
||||
200, |
|
||||
{"items": [{"item_name": "Bar"}], "q": "bar"}, |
|
||||
), |
|
||||
], |
|
||||
) |
|
||||
def test_get(path, expected_status, expected_response): |
|
||||
response = client.get(path) |
|
||||
assert response.status_code == expected_status |
|
||||
assert response.json() == expected_response |
|
||||
|
|
||||
|
|
||||
def test_openapi_schema(): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Skip", |
|
||||
"type": "integer", |
|
||||
"default": 0, |
|
||||
}, |
|
||||
"name": "skip", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Limit", |
|
||||
"type": "integer", |
|
||||
"default": 100, |
|
||||
}, |
|
||||
"name": "limit", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,170 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py310 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.dependencies.tutorial004_an_py310 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
@pytest.mark.parametrize( |
|
||||
"path,expected_status,expected_response", |
|
||||
[ |
|
||||
( |
|
||||
"/items", |
|
||||
200, |
|
||||
{ |
|
||||
"items": [ |
|
||||
{"item_name": "Foo"}, |
|
||||
{"item_name": "Bar"}, |
|
||||
{"item_name": "Baz"}, |
|
||||
] |
|
||||
}, |
|
||||
), |
|
||||
( |
|
||||
"/items?q=foo", |
|
||||
200, |
|
||||
{ |
|
||||
"items": [ |
|
||||
{"item_name": "Foo"}, |
|
||||
{"item_name": "Bar"}, |
|
||||
{"item_name": "Baz"}, |
|
||||
], |
|
||||
"q": "foo", |
|
||||
}, |
|
||||
), |
|
||||
( |
|
||||
"/items?q=foo&skip=1", |
|
||||
200, |
|
||||
{"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, |
|
||||
), |
|
||||
( |
|
||||
"/items?q=bar&limit=2", |
|
||||
200, |
|
||||
{"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, |
|
||||
), |
|
||||
( |
|
||||
"/items?q=bar&skip=1&limit=1", |
|
||||
200, |
|
||||
{"items": [{"item_name": "Bar"}], "q": "bar"}, |
|
||||
), |
|
||||
( |
|
||||
"/items?limit=1&q=bar&skip=1", |
|
||||
200, |
|
||||
{"items": [{"item_name": "Bar"}], "q": "bar"}, |
|
||||
), |
|
||||
], |
|
||||
) |
|
||||
def test_get(path, expected_status, expected_response, client: TestClient): |
|
||||
response = client.get(path) |
|
||||
assert response.status_code == expected_status |
|
||||
assert response.json() == expected_response |
|
||||
|
|
||||
|
|
||||
@needs_py310 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Skip", |
|
||||
"type": "integer", |
|
||||
"default": 0, |
|
||||
}, |
|
||||
"name": "skip", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Limit", |
|
||||
"type": "integer", |
|
||||
"default": 100, |
|
||||
}, |
|
||||
"name": "limit", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
@ -1,170 +0,0 @@ |
|||||
import pytest |
|
||||
from dirty_equals import IsDict |
|
||||
from fastapi.testclient import TestClient |
|
||||
|
|
||||
from ...utils import needs_py39 |
|
||||
|
|
||||
|
|
||||
@pytest.fixture(name="client") |
|
||||
def get_client(): |
|
||||
from docs_src.dependencies.tutorial004_an_py39 import app |
|
||||
|
|
||||
client = TestClient(app) |
|
||||
return client |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
@pytest.mark.parametrize( |
|
||||
"path,expected_status,expected_response", |
|
||||
[ |
|
||||
( |
|
||||
"/items", |
|
||||
200, |
|
||||
{ |
|
||||
"items": [ |
|
||||
{"item_name": "Foo"}, |
|
||||
{"item_name": "Bar"}, |
|
||||
{"item_name": "Baz"}, |
|
||||
] |
|
||||
}, |
|
||||
), |
|
||||
( |
|
||||
"/items?q=foo", |
|
||||
200, |
|
||||
{ |
|
||||
"items": [ |
|
||||
{"item_name": "Foo"}, |
|
||||
{"item_name": "Bar"}, |
|
||||
{"item_name": "Baz"}, |
|
||||
], |
|
||||
"q": "foo", |
|
||||
}, |
|
||||
), |
|
||||
( |
|
||||
"/items?q=foo&skip=1", |
|
||||
200, |
|
||||
{"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, |
|
||||
), |
|
||||
( |
|
||||
"/items?q=bar&limit=2", |
|
||||
200, |
|
||||
{"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, |
|
||||
), |
|
||||
( |
|
||||
"/items?q=bar&skip=1&limit=1", |
|
||||
200, |
|
||||
{"items": [{"item_name": "Bar"}], "q": "bar"}, |
|
||||
), |
|
||||
( |
|
||||
"/items?limit=1&q=bar&skip=1", |
|
||||
200, |
|
||||
{"items": [{"item_name": "Bar"}], "q": "bar"}, |
|
||||
), |
|
||||
], |
|
||||
) |
|
||||
def test_get(path, expected_status, expected_response, client: TestClient): |
|
||||
response = client.get(path) |
|
||||
assert response.status_code == expected_status |
|
||||
assert response.json() == expected_response |
|
||||
|
|
||||
|
|
||||
@needs_py39 |
|
||||
def test_openapi_schema(client: TestClient): |
|
||||
response = client.get("/openapi.json") |
|
||||
assert response.status_code == 200, response.text |
|
||||
assert response.json() == { |
|
||||
"openapi": "3.1.0", |
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
||||
"paths": { |
|
||||
"/items/": { |
|
||||
"get": { |
|
||||
"responses": { |
|
||||
"200": { |
|
||||
"description": "Successful Response", |
|
||||
"content": {"application/json": {"schema": {}}}, |
|
||||
}, |
|
||||
"422": { |
|
||||
"description": "Validation Error", |
|
||||
"content": { |
|
||||
"application/json": { |
|
||||
"schema": { |
|
||||
"$ref": "#/components/schemas/HTTPValidationError" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
"summary": "Read Items", |
|
||||
"operationId": "read_items_items__get", |
|
||||
"parameters": [ |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": IsDict( |
|
||||
{ |
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|
||||
"title": "Q", |
|
||||
} |
|
||||
) |
|
||||
| IsDict( |
|
||||
# TODO: remove when deprecating Pydantic v1 |
|
||||
{"title": "Q", "type": "string"} |
|
||||
), |
|
||||
"name": "q", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Skip", |
|
||||
"type": "integer", |
|
||||
"default": 0, |
|
||||
}, |
|
||||
"name": "skip", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
{ |
|
||||
"required": False, |
|
||||
"schema": { |
|
||||
"title": "Limit", |
|
||||
"type": "integer", |
|
||||
"default": 100, |
|
||||
}, |
|
||||
"name": "limit", |
|
||||
"in": "query", |
|
||||
}, |
|
||||
], |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"components": { |
|
||||
"schemas": { |
|
||||
"ValidationError": { |
|
||||
"title": "ValidationError", |
|
||||
"required": ["loc", "msg", "type"], |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"loc": { |
|
||||
"title": "Location", |
|
||||
"type": "array", |
|
||||
"items": { |
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|
||||
}, |
|
||||
}, |
|
||||
"msg": {"title": "Message", "type": "string"}, |
|
||||
"type": {"title": "Error Type", "type": "string"}, |
|
||||
}, |
|
||||
}, |
|
||||
"HTTPValidationError": { |
|
||||
"title": "HTTPValidationError", |
|
||||
"type": "object", |
|
||||
"properties": { |
|
||||
"detail": { |
|
||||
"title": "Detail", |
|
||||
"type": "array", |
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|
||||
} |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
}, |
|
||||
} |
|
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue