committed by
GitHub
146 changed files with 5743 additions and 9414 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") |
@ -64,7 +64,7 @@ jobs: |
|||
BRANCH: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'master' && 'main' ) || ( github.event.workflow_run.head_sha ) }} |
|||
# TODO: Use v3 when it's fixed, probably in v3.11 |
|||
# https://github.com/cloudflare/wrangler-action/issues/307 |
|||
uses: cloudflare/[email protected]3 |
|||
uses: cloudflare/[email protected]4 |
|||
# uses: cloudflare/wrangler-action@v3 |
|||
with: |
|||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} |
|||
|
@ -35,7 +35,7 @@ jobs: |
|||
TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} |
|||
run: python -m build |
|||
- name: Publish |
|||
uses: pypa/[email protected].3 |
|||
uses: pypa/[email protected].4 |
|||
- name: Dump GitHub context |
|||
env: |
|||
GITHUB_CONTEXT: ${{ toJson(github) }} |
|||
|
File diff suppressed because it is too large
@ -1,495 +1,495 @@ |
|||
- name: full-stack-fastapi-template |
|||
html_url: https://github.com/fastapi/full-stack-fastapi-template |
|||
stars: 28796 |
|||
stars: 29409 |
|||
owner_login: fastapi |
|||
owner_html_url: https://github.com/fastapi |
|||
- name: Hello-Python |
|||
html_url: https://github.com/mouredev/Hello-Python |
|||
stars: 27554 |
|||
stars: 28113 |
|||
owner_login: mouredev |
|||
owner_html_url: https://github.com/mouredev |
|||
- name: serve |
|||
html_url: https://github.com/jina-ai/serve |
|||
stars: 21225 |
|||
stars: 21264 |
|||
owner_login: jina-ai |
|||
owner_html_url: https://github.com/jina-ai |
|||
- name: sqlmodel |
|||
html_url: https://github.com/fastapi/sqlmodel |
|||
stars: 14921 |
|||
stars: 15109 |
|||
owner_login: fastapi |
|||
owner_html_url: https://github.com/fastapi |
|||
- name: HivisionIDPhotos |
|||
html_url: https://github.com/Zeyi-Lin/HivisionIDPhotos |
|||
stars: 14025 |
|||
stars: 14564 |
|||
owner_login: Zeyi-Lin |
|||
owner_html_url: https://github.com/Zeyi-Lin |
|||
- name: Douyin_TikTok_Download_API |
|||
html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API |
|||
stars: 10001 |
|||
stars: 10701 |
|||
owner_login: Evil0ctal |
|||
owner_html_url: https://github.com/Evil0ctal |
|||
- name: fastapi-best-practices |
|||
html_url: https://github.com/zhanymkanov/fastapi-best-practices |
|||
stars: 9820 |
|||
stars: 10180 |
|||
owner_login: zhanymkanov |
|||
owner_html_url: https://github.com/zhanymkanov |
|||
- name: awesome-fastapi |
|||
html_url: https://github.com/mjhea0/awesome-fastapi |
|||
stars: 8899 |
|||
stars: 9061 |
|||
owner_login: mjhea0 |
|||
owner_html_url: https://github.com/mjhea0 |
|||
- name: FastUI |
|||
html_url: https://github.com/pydantic/FastUI |
|||
stars: 8400 |
|||
stars: 8644 |
|||
owner_login: pydantic |
|||
owner_html_url: https://github.com/pydantic |
|||
- name: nonebot2 |
|||
html_url: https://github.com/nonebot/nonebot2 |
|||
stars: 6235 |
|||
stars: 6312 |
|||
owner_login: nonebot |
|||
owner_html_url: https://github.com/nonebot |
|||
- name: serge |
|||
html_url: https://github.com/serge-chat/serge |
|||
stars: 5685 |
|||
stars: 5686 |
|||
owner_login: serge-chat |
|||
owner_html_url: https://github.com/serge-chat |
|||
- name: fastapi-users |
|||
html_url: https://github.com/fastapi-users/fastapi-users |
|||
stars: 4787 |
|||
owner_login: fastapi-users |
|||
owner_html_url: https://github.com/fastapi-users |
|||
- name: FileCodeBox |
|||
html_url: https://github.com/vastsa/FileCodeBox |
|||
stars: 4479 |
|||
stars: 4933 |
|||
owner_login: vastsa |
|||
owner_html_url: https://github.com/vastsa |
|||
- name: fastapi-users |
|||
html_url: https://github.com/fastapi-users/fastapi-users |
|||
stars: 4849 |
|||
owner_login: fastapi-users |
|||
owner_html_url: https://github.com/fastapi-users |
|||
- name: hatchet |
|||
html_url: https://github.com/hatchet-dev/hatchet |
|||
stars: 4413 |
|||
stars: 4514 |
|||
owner_login: hatchet-dev |
|||
owner_html_url: https://github.com/hatchet-dev |
|||
- name: chatgpt-web-share |
|||
html_url: https://github.com/chatpire/chatgpt-web-share |
|||
stars: 4322 |
|||
stars: 4319 |
|||
owner_login: chatpire |
|||
owner_html_url: https://github.com/chatpire |
|||
- name: atrilabs-engine |
|||
html_url: https://github.com/Atri-Labs/atrilabs-engine |
|||
stars: 4115 |
|||
owner_login: Atri-Labs |
|||
owner_html_url: https://github.com/Atri-Labs |
|||
- name: polar |
|||
html_url: https://github.com/polarsource/polar |
|||
stars: 4216 |
|||
owner_login: polarsource |
|||
owner_html_url: https://github.com/polarsource |
|||
- name: strawberry |
|||
html_url: https://github.com/strawberry-graphql/strawberry |
|||
stars: 4084 |
|||
stars: 4126 |
|||
owner_login: strawberry-graphql |
|||
owner_html_url: https://github.com/strawberry-graphql |
|||
- name: atrilabs-engine |
|||
html_url: https://github.com/Atri-Labs/atrilabs-engine |
|||
stars: 4114 |
|||
owner_login: Atri-Labs |
|||
owner_html_url: https://github.com/Atri-Labs |
|||
- name: dynaconf |
|||
html_url: https://github.com/dynaconf/dynaconf |
|||
stars: 3844 |
|||
stars: 3874 |
|||
owner_login: dynaconf |
|||
owner_html_url: https://github.com/dynaconf |
|||
- name: poem |
|||
html_url: https://github.com/poem-web/poem |
|||
stars: 3698 |
|||
stars: 3746 |
|||
owner_login: poem-web |
|||
owner_html_url: https://github.com/poem-web |
|||
- name: polar |
|||
html_url: https://github.com/polarsource/polar |
|||
stars: 3355 |
|||
owner_login: polarsource |
|||
owner_html_url: https://github.com/polarsource |
|||
- name: opyrator |
|||
html_url: https://github.com/ml-tooling/opyrator |
|||
stars: 3114 |
|||
stars: 3117 |
|||
owner_login: ml-tooling |
|||
owner_html_url: https://github.com/ml-tooling |
|||
- name: farfalle |
|||
html_url: https://github.com/rashadphz/farfalle |
|||
stars: 3022 |
|||
stars: 3094 |
|||
owner_login: rashadphz |
|||
owner_html_url: https://github.com/rashadphz |
|||
- name: fastapi-admin |
|||
html_url: https://github.com/fastapi-admin/fastapi-admin |
|||
stars: 3002 |
|||
stars: 3040 |
|||
owner_login: fastapi-admin |
|||
owner_html_url: https://github.com/fastapi-admin |
|||
- name: docarray |
|||
html_url: https://github.com/docarray/docarray |
|||
stars: 2998 |
|||
stars: 3007 |
|||
owner_login: docarray |
|||
owner_html_url: https://github.com/docarray |
|||
- name: datamodel-code-generator |
|||
html_url: https://github.com/koxudaxi/datamodel-code-generator |
|||
stars: 2845 |
|||
stars: 2914 |
|||
owner_login: koxudaxi |
|||
owner_html_url: https://github.com/koxudaxi |
|||
- name: fastapi-realworld-example-app |
|||
html_url: https://github.com/nsidnev/fastapi-realworld-example-app |
|||
stars: 2832 |
|||
stars: 2840 |
|||
owner_login: nsidnev |
|||
owner_html_url: https://github.com/nsidnev |
|||
- name: uvicorn-gunicorn-fastapi-docker |
|||
html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker |
|||
stars: 2727 |
|||
owner_login: tiangolo |
|||
owner_html_url: https://github.com/tiangolo |
|||
- name: WrenAI |
|||
html_url: https://github.com/Canner/WrenAI |
|||
stars: 2699 |
|||
owner_login: Canner |
|||
owner_html_url: https://github.com/Canner |
|||
- name: LitServe |
|||
html_url: https://github.com/Lightning-AI/LitServe |
|||
stars: 2664 |
|||
stars: 2804 |
|||
owner_login: Lightning-AI |
|||
owner_html_url: https://github.com/Lightning-AI |
|||
- name: uvicorn-gunicorn-fastapi-docker |
|||
html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker |
|||
stars: 2730 |
|||
owner_login: tiangolo |
|||
owner_html_url: https://github.com/tiangolo |
|||
- name: logfire |
|||
html_url: https://github.com/pydantic/logfire |
|||
stars: 2495 |
|||
stars: 2620 |
|||
owner_login: pydantic |
|||
owner_html_url: https://github.com/pydantic |
|||
- name: huma |
|||
html_url: https://github.com/danielgtaylor/huma |
|||
stars: 2479 |
|||
stars: 2567 |
|||
owner_login: danielgtaylor |
|||
owner_html_url: https://github.com/danielgtaylor |
|||
- name: tracecat |
|||
html_url: https://github.com/TracecatHQ/tracecat |
|||
stars: 2446 |
|||
stars: 2494 |
|||
owner_login: TracecatHQ |
|||
owner_html_url: https://github.com/TracecatHQ |
|||
- name: RasaGPT |
|||
html_url: https://github.com/paulpierre/RasaGPT |
|||
stars: 2378 |
|||
owner_login: paulpierre |
|||
owner_html_url: https://github.com/paulpierre |
|||
- name: best-of-web-python |
|||
html_url: https://github.com/ml-tooling/best-of-web-python |
|||
stars: 2374 |
|||
stars: 2433 |
|||
owner_login: ml-tooling |
|||
owner_html_url: https://github.com/ml-tooling |
|||
- name: RasaGPT |
|||
html_url: https://github.com/paulpierre/RasaGPT |
|||
stars: 2386 |
|||
owner_login: paulpierre |
|||
owner_html_url: https://github.com/paulpierre |
|||
- name: fastapi-react |
|||
html_url: https://github.com/Buuntu/fastapi-react |
|||
stars: 2274 |
|||
stars: 2293 |
|||
owner_login: Buuntu |
|||
owner_html_url: https://github.com/Buuntu |
|||
- name: nextpy |
|||
html_url: https://github.com/dot-agent/nextpy |
|||
stars: 2244 |
|||
stars: 2256 |
|||
owner_login: dot-agent |
|||
owner_html_url: https://github.com/dot-agent |
|||
- name: 30-Days-of-Python |
|||
html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python |
|||
stars: 2154 |
|||
stars: 2155 |
|||
owner_login: codingforentrepreneurs |
|||
owner_html_url: https://github.com/codingforentrepreneurs |
|||
- name: FastAPI-template |
|||
html_url: https://github.com/s3rius/FastAPI-template |
|||
stars: 2067 |
|||
stars: 2121 |
|||
owner_login: s3rius |
|||
owner_html_url: https://github.com/s3rius |
|||
- name: langserve |
|||
html_url: https://github.com/langchain-ai/langserve |
|||
stars: 1980 |
|||
owner_login: langchain-ai |
|||
owner_html_url: https://github.com/langchain-ai |
|||
- name: sqladmin |
|||
html_url: https://github.com/aminalaee/sqladmin |
|||
stars: 1980 |
|||
stars: 2021 |
|||
owner_login: aminalaee |
|||
owner_html_url: https://github.com/aminalaee |
|||
- name: langserve |
|||
html_url: https://github.com/langchain-ai/langserve |
|||
stars: 2006 |
|||
owner_login: langchain-ai |
|||
owner_html_url: https://github.com/langchain-ai |
|||
- name: fastapi-utils |
|||
html_url: https://github.com/fastapiutils/fastapi-utils |
|||
stars: 1970 |
|||
stars: 2002 |
|||
owner_login: fastapiutils |
|||
owner_html_url: https://github.com/fastapiutils |
|||
- name: solara |
|||
html_url: https://github.com/widgetti/solara |
|||
stars: 1950 |
|||
stars: 1967 |
|||
owner_login: widgetti |
|||
owner_html_url: https://github.com/widgetti |
|||
- name: python-week-2022 |
|||
html_url: https://github.com/rochacbruno/python-week-2022 |
|||
stars: 1836 |
|||
owner_login: rochacbruno |
|||
owner_html_url: https://github.com/rochacbruno |
|||
- name: supabase-py |
|||
html_url: https://github.com/supabase/supabase-py |
|||
stars: 1803 |
|||
stars: 1848 |
|||
owner_login: supabase |
|||
owner_html_url: https://github.com/supabase |
|||
- name: python-week-2022 |
|||
html_url: https://github.com/rochacbruno/python-week-2022 |
|||
stars: 1832 |
|||
owner_login: rochacbruno |
|||
owner_html_url: https://github.com/rochacbruno |
|||
- name: mangum |
|||
html_url: https://github.com/Kludex/mangum |
|||
stars: 1760 |
|||
stars: 1789 |
|||
owner_login: Kludex |
|||
owner_html_url: https://github.com/Kludex |
|||
- name: manage-fastapi |
|||
html_url: https://github.com/ycd/manage-fastapi |
|||
stars: 1704 |
|||
stars: 1711 |
|||
owner_login: ycd |
|||
owner_html_url: https://github.com/ycd |
|||
- name: ormar |
|||
html_url: https://github.com/collerek/ormar |
|||
stars: 1688 |
|||
stars: 1701 |
|||
owner_login: collerek |
|||
owner_html_url: https://github.com/collerek |
|||
- name: agentkit |
|||
html_url: https://github.com/BCG-X-Official/agentkit |
|||
stars: 1615 |
|||
stars: 1630 |
|||
owner_login: BCG-X-Official |
|||
owner_html_url: https://github.com/BCG-X-Official |
|||
- name: langchain-serve |
|||
html_url: https://github.com/jina-ai/langchain-serve |
|||
stars: 1615 |
|||
stars: 1617 |
|||
owner_login: jina-ai |
|||
owner_html_url: https://github.com/jina-ai |
|||
- name: termpair |
|||
html_url: https://github.com/cs01/termpair |
|||
stars: 1613 |
|||
stars: 1612 |
|||
owner_login: cs01 |
|||
owner_html_url: https://github.com/cs01 |
|||
- name: coronavirus-tracker-api |
|||
html_url: https://github.com/ExpDev07/coronavirus-tracker-api |
|||
stars: 1591 |
|||
stars: 1590 |
|||
owner_login: ExpDev07 |
|||
owner_html_url: https://github.com/ExpDev07 |
|||
- name: piccolo |
|||
html_url: https://github.com/piccolo-orm/piccolo |
|||
stars: 1477 |
|||
stars: 1519 |
|||
owner_login: piccolo-orm |
|||
owner_html_url: https://github.com/piccolo-orm |
|||
- name: fastapi-crudrouter |
|||
html_url: https://github.com/awtkns/fastapi-crudrouter |
|||
stars: 1435 |
|||
stars: 1449 |
|||
owner_login: awtkns |
|||
owner_html_url: https://github.com/awtkns |
|||
- name: fastapi-cache |
|||
html_url: https://github.com/long2ice/fastapi-cache |
|||
stars: 1412 |
|||
stars: 1447 |
|||
owner_login: long2ice |
|||
owner_html_url: https://github.com/long2ice |
|||
- name: openapi-python-client |
|||
html_url: https://github.com/openapi-generators/openapi-python-client |
|||
stars: 1398 |
|||
stars: 1434 |
|||
owner_login: openapi-generators |
|||
owner_html_url: https://github.com/openapi-generators |
|||
- name: awesome-fastapi-projects |
|||
html_url: https://github.com/Kludex/awesome-fastapi-projects |
|||
stars: 1386 |
|||
stars: 1398 |
|||
owner_login: Kludex |
|||
owner_html_url: https://github.com/Kludex |
|||
- name: awesome-python-resources |
|||
html_url: https://github.com/DjangoEx/awesome-python-resources |
|||
stars: 1371 |
|||
stars: 1380 |
|||
owner_login: DjangoEx |
|||
owner_html_url: https://github.com/DjangoEx |
|||
- name: budgetml |
|||
html_url: https://github.com/ebhy/budgetml |
|||
stars: 1342 |
|||
stars: 1344 |
|||
owner_login: ebhy |
|||
owner_html_url: https://github.com/ebhy |
|||
- name: slowapi |
|||
html_url: https://github.com/laurentS/slowapi |
|||
stars: 1289 |
|||
stars: 1339 |
|||
owner_login: laurentS |
|||
owner_html_url: https://github.com/laurentS |
|||
- name: fastapi-pagination |
|||
html_url: https://github.com/uriyyo/fastapi-pagination |
|||
stars: 1240 |
|||
stars: 1263 |
|||
owner_login: uriyyo |
|||
owner_html_url: https://github.com/uriyyo |
|||
- name: fastapi-boilerplate |
|||
html_url: https://github.com/teamhide/fastapi-boilerplate |
|||
stars: 1173 |
|||
stars: 1206 |
|||
owner_login: teamhide |
|||
owner_html_url: https://github.com/teamhide |
|||
- name: fastapi-tutorial |
|||
html_url: https://github.com/liaogx/fastapi-tutorial |
|||
stars: 1162 |
|||
stars: 1178 |
|||
owner_login: liaogx |
|||
owner_html_url: https://github.com/liaogx |
|||
- name: fastapi-amis-admin |
|||
html_url: https://github.com/amisadmin/fastapi-amis-admin |
|||
stars: 1118 |
|||
stars: 1142 |
|||
owner_login: amisadmin |
|||
owner_html_url: https://github.com/amisadmin |
|||
- name: fastapi-code-generator |
|||
html_url: https://github.com/koxudaxi/fastapi-code-generator |
|||
stars: 1095 |
|||
stars: 1119 |
|||
owner_login: koxudaxi |
|||
owner_html_url: https://github.com/koxudaxi |
|||
- name: bolt-python |
|||
html_url: https://github.com/slackapi/bolt-python |
|||
stars: 1086 |
|||
stars: 1116 |
|||
owner_login: slackapi |
|||
owner_html_url: https://github.com/slackapi |
|||
- name: odmantic |
|||
html_url: https://github.com/art049/odmantic |
|||
stars: 1085 |
|||
stars: 1096 |
|||
owner_login: art049 |
|||
owner_html_url: https://github.com/art049 |
|||
- name: langchain-extract |
|||
html_url: https://github.com/langchain-ai/langchain-extract |
|||
stars: 1068 |
|||
stars: 1093 |
|||
owner_login: langchain-ai |
|||
owner_html_url: https://github.com/langchain-ai |
|||
- name: fastapi_production_template |
|||
html_url: https://github.com/zhanymkanov/fastapi_production_template |
|||
stars: 1059 |
|||
stars: 1078 |
|||
owner_login: zhanymkanov |
|||
owner_html_url: https://github.com/zhanymkanov |
|||
- name: fastapi-alembic-sqlmodel-async |
|||
html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async |
|||
stars: 1031 |
|||
stars: 1055 |
|||
owner_login: jonra1993 |
|||
owner_html_url: https://github.com/jonra1993 |
|||
- name: Kokoro-FastAPI |
|||
html_url: https://github.com/remsky/Kokoro-FastAPI |
|||
stars: 1047 |
|||
owner_login: remsky |
|||
owner_html_url: https://github.com/remsky |
|||
- name: prometheus-fastapi-instrumentator |
|||
html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator |
|||
stars: 1013 |
|||
stars: 1036 |
|||
owner_login: trallnag |
|||
owner_html_url: https://github.com/trallnag |
|||
- name: SurfSense |
|||
html_url: https://github.com/MODSetter/SurfSense |
|||
stars: 1018 |
|||
owner_login: MODSetter |
|||
owner_html_url: https://github.com/MODSetter |
|||
- name: bedrock-claude-chat |
|||
html_url: https://github.com/aws-samples/bedrock-claude-chat |
|||
stars: 1010 |
|||
owner_login: aws-samples |
|||
owner_html_url: https://github.com/aws-samples |
|||
- name: runhouse |
|||
html_url: https://github.com/run-house/runhouse |
|||
stars: 988 |
|||
stars: 1000 |
|||
owner_login: run-house |
|||
owner_html_url: https://github.com/run-house |
|||
- name: lanarky |
|||
html_url: https://github.com/ajndkr/lanarky |
|||
stars: 982 |
|||
stars: 986 |
|||
owner_login: ajndkr |
|||
owner_html_url: https://github.com/ajndkr |
|||
- name: autollm |
|||
html_url: https://github.com/viddexa/autollm |
|||
stars: 981 |
|||
stars: 982 |
|||
owner_login: viddexa |
|||
owner_html_url: https://github.com/viddexa |
|||
- name: bedrock-claude-chat |
|||
html_url: https://github.com/aws-samples/bedrock-claude-chat |
|||
stars: 977 |
|||
owner_login: aws-samples |
|||
owner_html_url: https://github.com/aws-samples |
|||
- name: SurfSense |
|||
html_url: https://github.com/MODSetter/SurfSense |
|||
stars: 971 |
|||
owner_login: MODSetter |
|||
owner_html_url: https://github.com/MODSetter |
|||
- name: restish |
|||
html_url: https://github.com/danielgtaylor/restish |
|||
stars: 954 |
|||
stars: 970 |
|||
owner_login: danielgtaylor |
|||
owner_html_url: https://github.com/danielgtaylor |
|||
- name: fastcrud |
|||
html_url: https://github.com/igorbenav/fastcrud |
|||
stars: 929 |
|||
owner_login: igorbenav |
|||
owner_html_url: https://github.com/igorbenav |
|||
- name: secure |
|||
html_url: https://github.com/TypeError/secure |
|||
stars: 911 |
|||
stars: 921 |
|||
owner_login: TypeError |
|||
owner_html_url: https://github.com/TypeError |
|||
- name: langcorn |
|||
html_url: https://github.com/msoedov/langcorn |
|||
stars: 909 |
|||
stars: 915 |
|||
owner_login: msoedov |
|||
owner_html_url: https://github.com/msoedov |
|||
- name: energy-forecasting |
|||
html_url: https://github.com/iusztinpaul/energy-forecasting |
|||
stars: 884 |
|||
owner_login: iusztinpaul |
|||
owner_html_url: https://github.com/iusztinpaul |
|||
- name: vue-fastapi-admin |
|||
html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin |
|||
stars: 863 |
|||
stars: 915 |
|||
owner_login: mizhexiaoxiao |
|||
owner_html_url: https://github.com/mizhexiaoxiao |
|||
- name: energy-forecasting |
|||
html_url: https://github.com/iusztinpaul/energy-forecasting |
|||
stars: 891 |
|||
owner_login: iusztinpaul |
|||
owner_html_url: https://github.com/iusztinpaul |
|||
- name: authx |
|||
html_url: https://github.com/yezz123/authx |
|||
stars: 850 |
|||
stars: 862 |
|||
owner_login: yezz123 |
|||
owner_html_url: https://github.com/yezz123 |
|||
- name: titiler |
|||
html_url: https://github.com/developmentseed/titiler |
|||
stars: 809 |
|||
stars: 823 |
|||
owner_login: developmentseed |
|||
owner_html_url: https://github.com/developmentseed |
|||
- name: marker-api |
|||
html_url: https://github.com/adithya-s-k/marker-api |
|||
stars: 792 |
|||
stars: 798 |
|||
owner_login: adithya-s-k |
|||
owner_html_url: https://github.com/adithya-s-k |
|||
- name: FastAPI-boilerplate |
|||
html_url: https://github.com/igorbenav/FastAPI-boilerplate |
|||
stars: 774 |
|||
owner_login: igorbenav |
|||
owner_html_url: https://github.com/igorbenav |
|||
- name: fastapi_best_architecture |
|||
html_url: https://github.com/fastapi-practices/fastapi_best_architecture |
|||
stars: 742 |
|||
stars: 766 |
|||
owner_login: fastapi-practices |
|||
owner_html_url: https://github.com/fastapi-practices |
|||
- name: fastapi-mail |
|||
html_url: https://github.com/sabuhish/fastapi-mail |
|||
stars: 728 |
|||
stars: 735 |
|||
owner_login: sabuhish |
|||
owner_html_url: https://github.com/sabuhish |
|||
- name: fastcrud |
|||
html_url: https://github.com/igorbenav/fastcrud |
|||
stars: 727 |
|||
owner_login: igorbenav |
|||
owner_html_url: https://github.com/igorbenav |
|||
- name: annotated-py-projects |
|||
html_url: https://github.com/hhstore/annotated-py-projects |
|||
stars: 722 |
|||
stars: 725 |
|||
owner_login: hhstore |
|||
owner_html_url: https://github.com/hhstore |
|||
- name: FastAPI-boilerplate |
|||
html_url: https://github.com/igorbenav/FastAPI-boilerplate |
|||
stars: 716 |
|||
owner_login: igorbenav |
|||
owner_html_url: https://github.com/igorbenav |
|||
- name: fastapi-do-zero |
|||
html_url: https://github.com/dunossauro/fastapi-do-zero |
|||
stars: 723 |
|||
owner_login: dunossauro |
|||
owner_html_url: https://github.com/dunossauro |
|||
- name: lccn_predictor |
|||
html_url: https://github.com/baoliay2008/lccn_predictor |
|||
stars: 707 |
|||
stars: 718 |
|||
owner_login: baoliay2008 |
|||
owner_html_url: https://github.com/baoliay2008 |
|||
- name: fastapi-observability |
|||
html_url: https://github.com/blueswen/fastapi-observability |
|||
stars: 718 |
|||
owner_login: blueswen |
|||
owner_html_url: https://github.com/blueswen |
|||
- name: chatGPT-web |
|||
html_url: https://github.com/mic1on/chatGPT-web |
|||
stars: 706 |
|||
stars: 708 |
|||
owner_login: mic1on |
|||
owner_html_url: https://github.com/mic1on |
|||
- name: fastapi-do-zero |
|||
html_url: https://github.com/dunossauro/fastapi-do-zero |
|||
stars: 702 |
|||
owner_login: dunossauro |
|||
owner_html_url: https://github.com/dunossauro |
|||
- name: learn-generative-ai |
|||
html_url: https://github.com/panaverse/learn-generative-ai |
|||
stars: 701 |
|||
owner_login: panaverse |
|||
owner_html_url: https://github.com/panaverse |
|||
- name: linbing |
|||
html_url: https://github.com/taomujian/linbing |
|||
stars: 699 |
|||
stars: 700 |
|||
owner_login: taomujian |
|||
owner_html_url: https://github.com/taomujian |
|||
- name: fastapi-observability |
|||
html_url: https://github.com/blueswen/fastapi-observability |
|||
stars: 698 |
|||
owner_login: blueswen |
|||
owner_html_url: https://github.com/blueswen |
|||
- name: FastAPI-Backend-Template |
|||
html_url: https://github.com/Aeternalis-Ingenium/FastAPI-Backend-Template |
|||
stars: 682 |
|||
stars: 692 |
|||
owner_login: Aeternalis-Ingenium |
|||
owner_html_url: https://github.com/Aeternalis-Ingenium |
|||
- name: learn-generative-ai |
|||
html_url: https://github.com/panaverse/learn-generative-ai |
|||
stars: 673 |
|||
owner_login: panaverse |
|||
owner_html_url: https://github.com/panaverse |
|||
- name: starlette-admin |
|||
html_url: https://github.com/jowilf/starlette-admin |
|||
stars: 692 |
|||
owner_login: jowilf |
|||
owner_html_url: https://github.com/jowilf |
|||
- name: fastapi-jwt-auth |
|||
html_url: https://github.com/IndominusByte/fastapi-jwt-auth |
|||
stars: 668 |
|||
stars: 674 |
|||
owner_login: IndominusByte |
|||
owner_html_url: https://github.com/IndominusByte |
|||
- name: pity |
|||
html_url: https://github.com/wuranxu/pity |
|||
stars: 660 |
|||
stars: 663 |
|||
owner_login: wuranxu |
|||
owner_html_url: https://github.com/wuranxu |
|||
- name: starlette-admin |
|||
html_url: https://github.com/jowilf/starlette-admin |
|||
stars: 653 |
|||
owner_login: jowilf |
|||
owner_html_url: https://github.com/jowilf |
|||
- name: fastapi_login |
|||
html_url: https://github.com/MushroomMaula/fastapi_login |
|||
stars: 650 |
|||
stars: 656 |
|||
owner_login: MushroomMaula |
|||
owner_html_url: https://github.com/MushroomMaula |
|||
|
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 38 KiB |
@ -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)にも関連します。 |
@ -0,0 +1,313 @@ |
|||
# 사용자 정의 응답 - HTML, Stream, 파일, 기타 |
|||
|
|||
기본적으로, **FastAPI** 응답을 `JSONResponse`를 사용하여 반환합니다. |
|||
|
|||
이를 재정의 하려면 [응답을 직접 반환하기](response-directly.md){.internal-link target=_blank}에서 본 것처럼 `Response`를 직접 반환하면 됩니다. |
|||
|
|||
그러나 `Response` (또는 `JSONResponse`와 같은 하위 클래스)를 직접 반환하면, 데이터가 자동으로 변환되지 않으며 (심지어 `response_model`을 선언했더라도), 문서화가 자동으로 생성되지 않습니다(예를 들어, 생성된 OpenAPI의 일부로 HTTP 헤더 `Content-Type`에 특정 "미디어 타입"을 포함하는 경우). |
|||
|
|||
하지만 *경로 작업 데코레이터*에서 `response_class` 매개변수를 사용하여 원하는 `Response`(예: 모든 `Response` 하위 클래스)를 선언할 수도 있습니다. |
|||
|
|||
*경로 작업 함수*에서 반환하는 내용은 해당 `Response`안에 포함됩니다. |
|||
|
|||
그리고 만약 그 `Response`가 `JSONResponse`와 `UJSONResponse`의 경우 처럼 JSON 미디어 타입(`application/json`)을 가지고 있다면, *경로 작업 데코레이터*에서 선언한 Pydantic의 `response_model`을 사용해 자동으로 변환(및 필터링) 됩니다. |
|||
|
|||
/// note | 참고 |
|||
|
|||
미디어 타입이 없는 응답 클래스를 사용하는 경우, FastAPI는 응답에 내용이 없을 것으로 예상하므로 생성된 OpenAPI 문서에서 응답 형식을 문서화하지 않습니다. |
|||
|
|||
/// |
|||
|
|||
## `ORJSONResponse` 사용하기 |
|||
|
|||
예를 들어, 성능을 극대화하려는 경우, <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">orjson</a>을 설치하여 사용하고 응답을 `ORJSONResponse`로 설정할 수 있습니다. |
|||
|
|||
사용하고자 하는 `Response` 클래스(하위 클래스)를 임포트한 후, **경로 작업 데코레이터*에서 선언하세요. |
|||
|
|||
대규모 응답의 경우, 딕셔너리를 반환하는 것보다 `Response`를 반환하는 것이 훨씬 빠릅니다. |
|||
|
|||
이유는 기본적으로, FastAPI가 내부의 모든 항목을 검사하고 JSON으로 직렬화할 수 있는지 확인하기 때문입니다. 이는 사용자 안내서에서 설명된 [JSON 호환 가능 인코더](../tutorial/encoder.md){.internal-link target=_blank}를 사용하는 방식과 동일합니다. 이를 통해 데이터베이스 모델과 같은 **임의의 객체**를 반환할 수 있습니다. |
|||
|
|||
하지만 반환하는 내용이 **JSON으로 직렬화 가능**하다고 확신하는 경우, 해당 내용을 응답 클래스에 직접 전달할 수 있으며, FastAPI가 반환 내용을 `jsonable_encoder`를 통해 처리한 뒤 응답 클래스에 전달하는 오버헤드를 피할 수 있습니다. |
|||
|
|||
{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *} |
|||
|
|||
/// info | 정보 |
|||
|
|||
`response_class` 매개변수는 응답의 "미디어 타입"을 정의하는 데에도 사용됩니다. |
|||
|
|||
이 경우, HTTP 헤더 `Content-Type`은 `application/json`으로 설정됩니다. |
|||
|
|||
그리고 이는 OpenAPI에 그대로 문서화됩니다. |
|||
|
|||
/// |
|||
|
|||
/// tip | 팁 |
|||
|
|||
`ORJSONResponse`는 FastAPI에서만 사용할 수 있고 Starlette에서는 사용할 수 없습니다. |
|||
|
|||
/// |
|||
|
|||
## HTML 응답 |
|||
|
|||
**FastAPI**에서 HTML 응답을 직접 반환하려면 `HTMLResponse`를 사용하세요. |
|||
|
|||
* `HTMLResponse`를 임포트 합니다. |
|||
* *경로 작업 데코레이터*의 `response_class` 매개변수로 `HTMLResponse`를 전달합니다. |
|||
|
|||
{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *} |
|||
|
|||
/// info | 정보 |
|||
|
|||
`response_class` 매개변수는 응답의 "미디어 타입"을 정의하는 데에도 사용됩니다. |
|||
|
|||
이 경우, HTTP 헤더 `Content-Type`은 `text/html`로 설정됩니다. |
|||
|
|||
그리고 이는 OpenAPI에 그대로 문서화 됩니다. |
|||
|
|||
/// |
|||
|
|||
### `Response` 반환하기 |
|||
|
|||
[응답을 직접 반환하기](response-directly.md){.internal-link target=_blank}에서 본 것 처럼, *경로 작업*에서 응답을 직접 반환하여 재정의할 수도 있습니다. |
|||
|
|||
위의 예제와 동일하게 `HTMLResponse`를 반환하는 코드는 다음과 같을 수 있습니다: |
|||
|
|||
{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *} |
|||
|
|||
/// warning | 경고 |
|||
|
|||
*경로 작업 함수*에서 직접 반환된 `Response`는 OpenAPI에 문서화되지 않습니다(예를들어, `Content-Type`이 문서화되지 않음) 자동 대화형 문서에서도 표시되지 않습니다. |
|||
|
|||
/// |
|||
|
|||
/// info | 정보 |
|||
|
|||
물론 실제 `Content-Type` 헤더, 상태 코드 등은 반환된 `Response` 객체에서 가져옵니다. |
|||
|
|||
/// |
|||
|
|||
### OpenAPI에 문서화하고 `Response` 재정의 하기 |
|||
|
|||
함수 내부에서 응답을 재정의하면서 동시에 OpenAPI에서 "미디어 타입"을 문서화하고 싶다면, `response_class` 매게변수를 사용하면서 `Response` 객체를 반환할 수 있습니다. |
|||
|
|||
이 경우 `response_class`는 OpenAPI *경로 작업*을 문서화하는 데만 사용되고, 실제로는 여러분이 반환한 `Response`가 그대로 사용됩니다. |
|||
|
|||
### `HTMLResponse`직접 반환하기 |
|||
|
|||
예를 들어, 다음과 같이 작성할 수 있습니다: |
|||
|
|||
{* ../../docs_src/custom_response/tutorial004.py hl[7,21,23] *} |
|||
|
|||
이 예제에서, `generate_html_response()` 함수는 HTML을 `str`로 반환하는 대신 이미 `Response`를 생성하고 반환합니다. |
|||
|
|||
`generate_html_response()`를 호출한 결과를 반환함으로써, 기본적인 **FastAPI** 기본 동작을 재정의 하는 `Response`를 이미 반환하고 있습니다. |
|||
|
|||
하지만 `response_class`에 `HTMLResponse`를 함께 전달했기 때문에, FastAPI는 이를 OpenAPI 및 대화형 문서에서 `text/html`로 HTML을 문서화 하는 방법을 알 수 있습니다. |
|||
|
|||
<img src="/img/tutorial/custom-response/image01.png"> |
|||
|
|||
## 사용 가능한 응답들 |
|||
|
|||
다음은 사용할 수 있는 몇가지 응답들 입니다. |
|||
|
|||
`Response`를 사용하여 다른 어떤 것도 반환 할수 있으며, 직접 하위 클래스를 만들 수도 있습니다. |
|||
|
|||
/// note | 기술 세부사항 |
|||
|
|||
`from starlette.responses import HTMLResponse`를 사용할 수도 있습니다. |
|||
|
|||
**FastAPI**는 개발자인 여러분의 편의를 위해 `starlette.responses`를 `fastapi.responses`로 제공 하지만, 대부분의 사용 가능한 응답은 Starlette에서 직접 가져옵니다. |
|||
|
|||
/// |
|||
|
|||
### `Response` |
|||
|
|||
기본 `Response` 클래스는 다른 모든 응답 클래스의 부모 클래스 입니다. |
|||
|
|||
이 클래스를 직접 반환할 수 있습니다. |
|||
|
|||
다음 매개변수를 받을 수 있습니다: |
|||
|
|||
* `content` - `str` 또는 `bytes`. |
|||
* `status_code` - HTTP 상태코드를 나타내는 `int`. |
|||
* `headers` - 문자열로 이루어진 `dict`. |
|||
* `media_type` - 미디어 타입을 나타내는 `str` 예: `"text/html"`. |
|||
|
|||
FastAPI (실제로는 Starlette)가 자동으로 `Content-Length` 헤더를 포함시킵니다. 또한 `media_type`에 기반하여 `Content-Type` 헤더를 포함하며, 텍스트 타입의 경우 문자 집합을 추가 합니다. |
|||
|
|||
{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *} |
|||
|
|||
### `HTMLResponse` |
|||
|
|||
텍스트 또는 바이트를 받아 HTML 응답을 반환합니다. 위에서 설명한 내용과 같습니다. |
|||
|
|||
### `PlainTextResponse` |
|||
|
|||
텍스트 또는 바이트를 받아 일반 텍스트 응답을 반환합니다. |
|||
|
|||
{* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *} |
|||
|
|||
### `JSONResponse` |
|||
|
|||
데이터를 받아 `application/json`으로 인코딩된 응답을 반환합니다. |
|||
|
|||
이는 위에서 설명했듯이 **FastAPI**에서 기본적으로 사용되는 응답 형식입니다. |
|||
|
|||
### `ORJSONResponse` |
|||
|
|||
<a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>을 사용하여 빠른 JSON 응답을 제공하는 대안입니다. 위에서 설명한 내용과 같습니다. |
|||
|
|||
/// info | 정보 |
|||
|
|||
이를 사용하려면 `orjson`을 설치해야합니다. 예: `pip install orjson`. |
|||
|
|||
/// |
|||
|
|||
### `UJSONResponse` |
|||
|
|||
<a href="https://github.com/ultrajson/ultrajson" class="external-link" target="_blank">`ujson`</a>을 사용한 또 다른 JSON 응답 형식입니다. |
|||
|
|||
/// info | 정보 |
|||
|
|||
이 응답을 사용하려면 `ujson`을 설치해야합니다. 예: 'pip install ujson`. |
|||
|
|||
/// |
|||
|
|||
/// warning | 경고 |
|||
|
|||
`ujson` 은 일부 예외 경우를 처리하는 데 있어 Python 내장 구현보다 덜 엄격합니다. |
|||
|
|||
/// |
|||
|
|||
{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *} |
|||
|
|||
/// tip | 팁 |
|||
|
|||
`ORJSONResponse`가 더 빠른 대안일 가능성이 있습니다. |
|||
|
|||
/// |
|||
|
|||
### `RedirectResponse` |
|||
|
|||
HTTP 리디렉션 응답을 반환합니다. 기본적으로 상태 코드는 307(임시 리디렉션)으로 설정됩니다. |
|||
|
|||
`RedirectResponse`를 직접 반환할 수 있습니다. |
|||
|
|||
{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *} |
|||
|
|||
--- |
|||
|
|||
또는 `response_class` 매개변수에서 사용할 수도 있습니다: |
|||
|
|||
|
|||
{* ../../docs_src/custom_response/tutorial006b.py hl[2,7,9] *} |
|||
|
|||
이 경우, *경로 작업* 함수에서 URL을 직접 반환할 수 있습니다. |
|||
|
|||
이 경우, 사용되는 `status_code`는 `RedirectResponse`의 기본값인 `307` 입니다. |
|||
|
|||
--- |
|||
|
|||
`status_code` 매개변수를 `response_class` 매개변수와 함께 사용할 수도 있습니다: |
|||
|
|||
{* ../../docs_src/custom_response/tutorial006c.py hl[2,7,9] *} |
|||
|
|||
### `StreamingResponse` |
|||
|
|||
비동기 제너레이터 또는 일반 제너레이터/이터레이터를 받아 응답 본문을 스트리밍 합니다. |
|||
|
|||
{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *} |
|||
|
|||
#### 파일과 같은 객체를 사용한 `StreamingResponse` |
|||
|
|||
파일과 같은 객체(예: `open()`으로 반환된 객체)가 있는 경우, 해당 파일과 같은 객체를 반복(iterate)하는 제너레이터 함수를 만들 수 있습니다. |
|||
|
|||
이 방식으로, 파일 전체를 메모리에 먼저 읽어들일 필요 없이, 제너레이터 함수를 `StreamingResponse`에 전달하여 반환할 수 있습니다. |
|||
|
|||
이 방식은 클라우드 스토리지, 비디오 처리 등의 다양한 라이브러리와 함께 사용할 수 있습니다. |
|||
|
|||
{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *} |
|||
|
|||
1. 이것이 제너레이터 함수입니다. `yield` 문을 포함하고 있으므로 "제너레이터 함수"입니다. |
|||
2. `with` 블록을 사용함으로써, 제너레이터 함수가 완료된 후 파일과 같은 객체가 닫히도록 합니다. 즉, 응답 전송이 끝난 후 닫힙니다. |
|||
3. 이 `yield from`은 함수가 `file_like`라는 객체를 반복(iterate)하도록 합니다. 반복된 각 부분은 이 제너레이터 함수(`iterfile`)에서 생성된 것처럼 `yield` 됩니다. |
|||
|
|||
이렇게 하면 "생성(generating)" 작업을 내부적으로 다른 무언가에 위임하는 제너레이터 함수가 됩니다. |
|||
|
|||
이 방식을 사용하면 `with` 블록 안에서 파일을 열 수 있어, 작업이 완료된 후 파일과 같은 객체가 닫히는 것을 보장할 수 있습니다. |
|||
|
|||
/// tip | 팁 |
|||
|
|||
여기서 표준 `open()`을 사용하고 있기 때문에 `async`와 `await`를 지원하지 않습니다. 따라서 경로 작업은 일반 `def`로 선언합니다. |
|||
|
|||
/// |
|||
|
|||
### `FileResponse` |
|||
|
|||
파일을 비동기로 스트리밍하여 응답합니다. |
|||
|
|||
다른 응답 유형과는 다른 인수를 사용하여 객체를 생성합니다: |
|||
|
|||
* `path` - 스트리밍할 파일의 경로. |
|||
* `headers` - 딕셔너리 형식의 사용자 정의 헤더. |
|||
* `media_type` - 미디어 타입을 나타내는 문자열. 설정되지 않은 경우 파일 이름이나 경로를 사용하여 추론합니다. |
|||
* `filename` - 설정된 경우 응답의 `Content-Disposition`에 포함됩니다. |
|||
|
|||
파일 응답에는 적절한 `Content-Length`, `Last-Modified`, 및 `ETag` 헤더가 포함됩니다. |
|||
|
|||
{* ../../docs_src/custom_response/tutorial009.py hl[2,10] *} |
|||
|
|||
또한 `response_class` 매개변수를 사용할 수도 있습니다: |
|||
|
|||
{* ../../docs_src/custom_response/tutorial009b.py hl[2,8,10] *} |
|||
|
|||
이 경우, 경로 작업 함수에서 파일 경로를 직접 반환할 수 있습니다. |
|||
|
|||
## 사용자 정의 응답 클래스 |
|||
|
|||
`Response`를 상속받아 사용자 정의 응답 클래스를 생성하고 사용할 수 있습니다. |
|||
|
|||
예를 들어, 포함된 `ORJSONResponse` 클래스에서 사용되지 않는 설정으로 <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">orjson</a>을 사용하고 싶다고 가정해봅시다. |
|||
|
|||
만약 들여쓰기 및 포맷된 JSON을 반환하고 싶다면, `orjson.OPT_INDENT_2` 옵션을 사용할 수 있습니다. |
|||
|
|||
`CustomORJSONResponse`를 생성할 수 있습니다. 여기서 핵심은 `Response.render(content)` 메서드를 생성하여 내용을 `bytes`로 반환하는 것입니다: |
|||
|
|||
{* ../../docs_src/custom_response/tutorial009c.py hl[9:14,17] *} |
|||
|
|||
이제 다음 대신: |
|||
|
|||
```json |
|||
{"message": "Hello World"} |
|||
``` |
|||
|
|||
이 응답은 이렇게 반환됩니다: |
|||
|
|||
```json |
|||
{ |
|||
"message": "Hello World" |
|||
} |
|||
``` |
|||
|
|||
물론 JSON 포맷팅보다 더 유용하게 활용할 방법을 찾을 수 있을 것입니다. 😉 |
|||
|
|||
## 기본 응답 클래스 |
|||
|
|||
**FastAPI** 클래스 객체 또는 `APIRouter`를 생성할 때 기본적으로 사용할 응답 클래스를 지정할 수 있습니다. |
|||
|
|||
이를 정의하는 매개변수는 `default_response_class`입니다. |
|||
|
|||
아래 예제에서 **FastAPI**는 모든 경로 작업에서 기본적으로 `JSONResponse` 대신 `ORJSONResponse`를 사용합니다. |
|||
|
|||
{* ../../docs_src/custom_response/tutorial010.py hl[2,4] *} |
|||
|
|||
/// tip | 팁 |
|||
|
|||
여전히 이전처럼 *경로 작업*에서 `response_class`를 재정의할 수 있습니다. |
|||
|
|||
/// |
|||
|
|||
## 추가 문서화 |
|||
|
|||
OpenAPI에서 `responses`를 사용하여 미디어 타입 및 기타 세부 정보를 선언할 수도 있습니다: [OpenAPI에서 추가 응답](additional-responses.md){.internal-link target=_blank}. |
@ -1,162 +1,269 @@ |
|||
* # FastAPI 지원 - 도움말 받기 |
|||
# FastAPI 지원 - 도움 받기 |
|||
|
|||
**FastAPI** 가 마음에 드시나요? |
|||
**FastAPI** 가 마음에 드시나요? |
|||
|
|||
FastAPI, 다른 사용자, 개발자를 응원하고 싶으신가요? |
|||
FastAPI, 다른 사용자, 개발자를 응원하고 싶으신가요? |
|||
|
|||
혹은 **FastAPI** 에 대해 도움이 필요하신가요? |
|||
혹은 **FastAPI** 에 대해 도움이 필요하신가요? |
|||
|
|||
아주 간단하게 응원할 수 있습니다 (몇 번의 클릭만으로). |
|||
아주 간단하게 응원할 수 있습니다 (몇 번의 클릭만으로). |
|||
|
|||
또한 도움을 받을 수 있는 방법도 몇 가지 있습니다. |
|||
또한 도움을 받을 수 있는 방법도 몇 가지 있습니다. |
|||
|
|||
## 뉴스레터 구독 |
|||
## 뉴스레터 구독 |
|||
|
|||
[**FastAPI와 친구** 뉴스레터](https://github.com/fastapi/fastapi/blob/master/newsletter)를 구독하여 최신 정보를 유지할 수 있습니다{.internal-link target=_blank}: |
|||
[**FastAPI and friends** 뉴스레터](newsletter.md){.internal-link target=\_blank}를 구독하여 최신 정보를 유지할 수 있습니다: |
|||
|
|||
- FastAPI 와 그 친구들에 대한 뉴스 🚀 |
|||
- 가이드 📝 |
|||
- 특징 ✨ |
|||
- 획기적인 변화 🚨 |
|||
- 팁과 요령 ✅ |
|||
* FastAPI and friends에 대한 뉴스 🚀 |
|||
* 가이드 📝 |
|||
* 기능 ✨ |
|||
* 획기적인 변화 🚨 |
|||
* 팁과 요령 ✅ |
|||
|
|||
## 트위터에서 FastAPI 팔로우하기 |
|||
## 트위터에서 FastAPI 팔로우하기 |
|||
|
|||
[Follow @fastapi on **Twitter**](https://twitter.com/fastapi) 를 팔로우하여 **FastAPI** 에 대한 최신 뉴스를 얻을 수 있습니다. 🐦 |
|||
<a href="https://twitter.com/fastapi" class="external-link" target="_blank">**Twitter**의 @fastapi를 팔로우</a>하여 **FastAPI** 에 대한 최신 뉴스를 얻을 수 있습니다. 🐦 |
|||
|
|||
## Star **FastAPI** in GitHub |
|||
## Star **FastAPI** in GitHub |
|||
|
|||
GitHub에서 FastAPI에 "star"를 붙일 수 있습니다(오른쪽 상단의 star 버튼을 클릭): https://github.com/fastapi/fastapi. ⭐️ |
|||
GitHub에서 FastAPI에 "star"를 붙일 수 있습니다 (오른쪽 상단의 star 버튼을 클릭): <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>. ⭐️ |
|||
|
|||
스타를 늘림으로써, 다른 사용자들이 좀 더 쉽게 찾을 수 있고, 많은 사람들에게 유용한 것임을 나타낼 수 있습니다. |
|||
스타를 늘림으로써, 다른 사용자들이 좀 더 쉽게 찾을 수 있고, 많은 사람들에게 유용한 것임을 나타낼 수 있습니다. |
|||
|
|||
## GitHub 저장소에서 릴리즈 확인 |
|||
## GitHub 저장소에서 릴리즈 확인 |
|||
|
|||
GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 watch 버튼을 클릭): https://github.com/fastapi/fastapi. 👀 |
|||
GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 watch 버튼을 클릭): <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>. 👀 |
|||
|
|||
여기서 "Releases only"을 선택할 수 있습니다. |
|||
여기서 "Releases only"을 선택할 수 있습니다. |
|||
|
|||
이렇게하면, **FastAPI** 의 버그 수정 및 새로운 기능의 구현 등의 새로운 자료 (최신 버전)이 있을 때마다 (이메일) 통지를 받을 수 있습니다. |
|||
이렇게하면, **FastAPI** 의 버그 수정 및 새로운 기능의 구현 등의 새로운 자료 (최신 버전)이 있을 때마다 (이메일) 통지를 받을 수 있습니다. |
|||
|
|||
## 개발자와의 연결 |
|||
## 개발자와의 연결 |
|||
|
|||
개발자인 [me (Sebastián Ramírez / `tiangolo`)](https://tiangolo.com/) 와 연락을 취할 수 있습니다. |
|||
<a href="https://tiangolo.com" class="external-link" target="_blank">개발자(Sebastián Ramírez / `tiangolo`)</a>와 연락을 취할 수 있습니다. |
|||
|
|||
여러분은 할 수 있습니다: |
|||
여러분은 할 수 있습니다: |
|||
|
|||
- [**GitHub**에서 팔로우하기](https://github.com/tiangolo). |
|||
- 당신에게 도움이 될 저의 다른 오픈소스 프로젝트를 확인하십시오. |
|||
- 새로운 오픈소스 프로젝트를 만들었을 때 확인하려면 팔로우 하십시오. |
|||
* <a href="https://github.com/tiangolo" class="external-link" target="_blank">**GitHub**에서 팔로우하기.</a>. |
|||
* 당신에게 도움이 될 저의 다른 오픈소스 프로젝트를 확인하십시오. |
|||
* 새로운 오픈소스 프로젝트를 만들었을 때 확인하려면 팔로우 하십시오. |
|||
* <a href="https://twitter.com/tiangolo" class="external-link" target="_blank">**Twitter**</a> 또는 <a href="https://fosstodon.org/@tiangolo" class="external-link" target="_blank">Mastodon</a>에서 팔로우하기. |
|||
* FastAPI의 사용 용도를 알려주세요 (그것을 듣는 것을 좋아합니다). |
|||
* 발표나 새로운 툴 출시 소식을 받아보십시오. |
|||
* <a href="https://twitter.com/fastapi" class="external-link" target="_blank">**Twitter**의 @fastapi를 팔로우</a> (별도 계정에서) 할 수 있습니다. |
|||
* <a href="https://www.linkedin.com/in/tiangolo/" class="external-link" target="_blank">**LinkedIn**에서 팔로우하기.</a>. |
|||
* 새로운 툴의 발표나 출시 소식을 받아보십시오. (단, Twitter를 더 자주 사용합니다 🤷♂). |
|||
* <a href="https://dev.to/tiangolo" class="external-link" target="_blank">**Dev.to**</a> 또는 <a href="https://medium.com/@tiangolo" class="external-link" target="_blank">**Medium**</a>에서 제가 작성한 내용을 읽어 보십시오 (또는 팔로우). |
|||
* 다른 기사나 아이디어들을 읽고, 제가 만들어왔던 툴에 대해서도 읽으십시오. |
|||
* 새로운 기사를 읽기 위해 팔로우 하십시오. |
|||
|
|||
- [**Twitter**에서 팔로우하기](https://twitter.com/tiangolo). |
|||
- FastAPI의 사용 용도를 알려주세요 (그것을 듣는 것을 좋아합니다). |
|||
- 발표 또는 새로운 툴 출시할 때 들으십시오. |
|||
- [follow @fastapi on Twitter](https://twitter.com/fastapi) (별도 계정에서) 할 수 있습니다. |
|||
## **FastAPI**에 대한 트윗 |
|||
|
|||
- [**Linkedin**에서의 연결](https://www.linkedin.com/in/tiangolo/). |
|||
- 새로운 툴의 발표나 릴리스를 들을 수 있습니다 (단, Twitter를 더 자주 사용합니다 🤷♂). |
|||
<a href="https://twitter.com/compose/tweet?text=I'm loving @fastapi because... https://github.com/fastapi/fastapi" class="external-link" target="_blank">**FastAPI**에 대해 트윗</a> 하고 FastAPI가 마음에 드는 이유를 알려주세요. 🎉 |
|||
|
|||
- [**Dev.to**](https://dev.to/tiangolo) 또는 [**Medium**](https://medium.com/@tiangolo)에서 제가 작성한 내용을 읽어 보십시오(또는 팔로우). |
|||
- 다른 기사나 아이디어들을 읽고, 제가 만들어왔던 툴에 대해서도 읽으십시오. |
|||
- 새로운 기사를 읽기 위해 팔로우 하십시오. |
|||
**FastAPI**가 어떻게 사용되고 있는지, 어떤 점이 마음에 들었는지, 어떤 프로젝트/회사에서 사용하고 있는지 등에 대해 듣고 싶습니다. |
|||
|
|||
## **FastAPI**에 대한 트윗 |
|||
## FastAPI에 투표하기 |
|||
|
|||
[**FastAPI**에 대해 트윗](https://twitter.com/compose/tweet?text=I'm loving @fastapi because... https://github.com/fastapi/fastapi) 하고 FastAPI가 마음에 드는 이유를 알려주세요. 🎉 |
|||
* <a href="https://www.slant.co/options/34241/~fastapi-review" class="external-link" target="_blank">Slant에서 **FastAPI** 에 대해 투표하십시오</a>. |
|||
* <a href="https://alternativeto.net/software/fastapi/about/" class="external-link" target="_blank">AlternativeTo에서 **FastAPI** 에 대해 투표하십시오</a>. |
|||
* <a href="https://stackshare.io/pypi-fastapi" class="external-link" target="_blank">StackShare에서 **FastAPI** 에 대해 투표하십시오</a>. |
|||
|
|||
**FastAPI**가 어떻게 사용되고 있는지, 어떤 점이 마음에 들었는지, 어떤 프로젝트/회사에서 사용하고 있는지 등에 대해 듣고 싶습니다. |
|||
## GitHub의 이슈로 다른사람 돕기 |
|||
|
|||
## FastAPI에 투표하기 |
|||
다른 사람들의 질문에 도움을 줄 수 있습니다: |
|||
|
|||
- [Slant에서 **FastAPI** 에 대해 투표하십시오](https://www.slant.co/options/34241/~fastapi-review). |
|||
- [AlternativeTo**FastAPI** 에 대해 투표하십시오](https://alternativeto.net/software/fastapi/). |
|||
* <a href="https://github.com/fastapi/fastapi/discussions/categories/questions?discussions_q=category%3AQuestions+is%3Aunanswered" class="external-link" target="_blank">GitHub 디스커션</a> |
|||
* <a href="https://github.com/fastapi/fastapi/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Aquestion+-label%3Aanswered+" class="external-link" target="_blank">GitHub 이슈</a> |
|||
|
|||
## GitHub의 이슈로 다른사람 돕기 |
|||
많은 경우, 여러분은 이미 그 질문에 대한 답을 알고 있을 수도 있습니다. 🤓 |
|||
|
|||
[존재하는 이슈](https://github.com/fastapi/fastapi/issues)를 확인하고 그것을 시도하고 도와줄 수 있습니다. 대부분의 경우 이미 답을 알고 있는 질문입니다. 🤓 |
|||
만약 많은 사람들의 문제를 도와준다면, 공식적인 [FastAPI 전문가](fastapi-people.md#fastapi-experts){.internal-link target=\_blank} 가 될 것입니다. 🎉 |
|||
|
|||
많은 사람들의 문제를 도와준다면, 공식적인 [FastAPI 전문가](https://github.com/fastapi/fastapi/blob/master/docs/en/docs/fastapi-people.md#experts) 가 될 수 있습니다{.internal-link target=_blank}. 🎉 |
|||
가장 중요한 점은: 친절하려고 노력하는 것입니다. 사람들은 좌절감을 안고 오며, 많은 경우 최선의 방식으로 질문하지 않을 수도 있습니다. 하지만 최대한 친절하게 대하려고 노력하세요. 🤗 |
|||
|
|||
## GitHub 저장소 보기 |
|||
**FastAPI** 커뮤니티의 목표는 친절하고 환영하는 것입니다. 동시에, 괴롭힘이나 무례한 행동을 받아들이지 마세요. 우리는 서로를 돌봐야 합니다. |
|||
|
|||
GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 watch 버튼을 클릭): https://github.com/fastapi/fastapi. 👀 |
|||
--- |
|||
|
|||
"Releases only" 대신 "Watching"을 선택하면 다른 사용자가 새로운 issue를 생성할 때 알림이 수신됩니다. |
|||
다른 사람들의 질문 (디스커션 또는 이슈에서) 해결을 도울 수 있는 방법은 다음과 같습니다. |
|||
|
|||
그런 다음 이런 issues를 해결 할 수 있도록 도움을 줄 수 있습니다. |
|||
### 질문 이해하기 |
|||
|
|||
## 이슈 생성하기 |
|||
* 질문하는 사람이 가진 **목적**과 사용 사례를 이해할 수 있는지 확인하세요. |
|||
|
|||
GitHub 저장소에 [새로운 이슈 생성](https://github.com/fastapi/fastapi/issues/new/choose) 을 할 수 있습니다, 예를들면 다음과 같습니다: |
|||
* 질문 (대부분은 질문입니다)이 **명확**한지 확인하세요. |
|||
|
|||
- **질문**을 하거나 **문제**에 대해 질문합니다. |
|||
- 새로운 **기능**을 제안 합니다. |
|||
* 많은 경우, 사용자가 가정한 해결책에 대한 질문을 하지만, 더 **좋은** 해결책이 있을 수 있습니다. 문제와 사용 사례를 더 잘 이해하면 더 나은 **대안적인 해결책**을 제안할 수 있습니다. |
|||
|
|||
**참고**: 만약 이슈를 생성한다면, 저는 여러분에게 다른 사람들을 도와달라고 부탁할 것입니다. 😉 |
|||
* 질문을 이해할 수 없다면, 더 **자세한 정보**를 요청하세요. |
|||
|
|||
## Pull Request를 만드십시오 |
|||
### 문제 재현하기 |
|||
|
|||
Pull Requests를 이용하여 소스코드에 [컨트리뷰트](https://github.com/fastapi/fastapi/blob/master/docs/en/docs/contributing.md){.internal-link target=_blank} 할 수 있습니다. 예를 들면 다음과 같습니다: |
|||
대부분의 경우, 질문은 질문자의 **원본 코드**와 관련이 있습니다. |
|||
|
|||
- 문서에서 찾은 오타를 수정할 때. |
|||
많은 경우, 코드의 일부만 복사해서 올리지만, 그것만으로는 **문제를 재현**하기에 충분하지 않습니다. |
|||
|
|||
- FastAPI를 [편집하여](https://github.com/fastapi/fastapi/edit/master/docs/en/data/external_links.yml) 작성했거나 찾은 문서, 비디오 또는 팟캐스트를 공유할 때. |
|||
* 질문자에게 <a href="https://stackoverflow.com/help/minimal-reproducible-example" class="external-link" target="_blank">최소한의 재현 가능한 예제</a>를 제공해달라고 요청하세요. 이렇게 하면 코드를 **복사-붙여넣기**하여 직접 실행하고, 동일한 오류나 동작을 확인하거나 사용 사례를 더 잘 이해할 수 있습니다. |
|||
|
|||
- 해당 섹션의 시작 부분에 링크를 추가했는지 확인하십시오. |
|||
* 너그러운 마음이 든다면, 문제 설명만을 기반으로 직접 **예제를 만들어**볼 수도 있습니다. 하지만, 이는 시간이 많이 걸릴 수 있으므로, 먼저 질문을 명확히 해달라고 요청하는 것이 좋습니다. |
|||
|
|||
- 당신의 언어로 [문서 번역하는데](https://github.com/fastapi/fastapi/blob/master/docs/en/docs/contributing.md#translations){.internal-link target=_blank} 기여할 때. |
|||
### 해결책 제안하기 |
|||
|
|||
- 또한 다른 사용자가 만든 번역을 검토하는데 도움을 줄 수도 있습니다. |
|||
* 질문을 충분히 이해한 후에는 가능한 **답변**을 제공할 수 있습니다. |
|||
|
|||
- 새로운 문서의 섹션을 제안할 때. |
|||
* 많은 경우, 질문자의 **근본적인 문제나 사용 사례**를 이해하는 것이 중요합니다. 그들이 시도하는 방법보다 더 나은 해결책이 있을 수 있기 때문입니다. |
|||
|
|||
- 기존 문제/버그를 수정할 때. |
|||
### 해결 요청하기 |
|||
|
|||
- 새로운 feature를 추가할 때. |
|||
질문자가 답변을 확인하고 나면, 당신이 문제를 해결했을 가능성이 높습니다. 축하합니다, **당신은 영웅입니다**! 🦸 |
|||
|
|||
## 채팅에 참여하십시오 |
|||
* 이제 문제를 해결했다면, 질문자에게 다음을 요청할 수 있습니다. |
|||
|
|||
👥 [디스코드 채팅 서버](https://discord.gg/VQjSZaeJmf) 👥 에 가입하고 FastAPI 커뮤니티에서 다른 사람들과 어울리세요. |
|||
* GitHub 디스커션에서: 댓글을 **답변**으로 표시하도록 요청하세요. |
|||
* GitHub 이슈에서: 이슈를 **닫아달라고** 요청하세요. |
|||
|
|||
/// tip |
|||
## GitHub 저장소 보기 |
|||
|
|||
질문이 있는 경우, [GitHub 이슈 ](https://github.com/fastapi/fastapi/issues/new/choose) 에서 질문하십시오, [FastAPI 전문가](https://github.com/fastapi/fastapi/blob/master/docs/en/docs/fastapi-people.md#experts) 의 도움을 받을 가능성이 높습니다{.internal-link target=_blank} . |
|||
GitHub에서 FastAPI를 "watch"할 수 있습니다 (오른쪽 상단 watch 버튼을 클릭): <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>. 👀 |
|||
|
|||
/// |
|||
"Releases only" 대신 "Watching"을 선택하면, 새로운 이슈나 질문이 생성될 때 알림을 받을 수 있습니다. 또한, 특정하게 새로운 이슈, 디스커션, PR 등만 알림 받도록 설정할 수도 있습니다. |
|||
|
|||
``` |
|||
다른 일반적인 대화에서만 채팅을 사용하십시오. |
|||
``` |
|||
그런 다음 이런 이슈들을 해결 할 수 있도록 도움을 줄 수 있습니다. |
|||
|
|||
기존 [지터 채팅](https://gitter.im/fastapi/fastapi) 이 있지만 채널과 고급기능이 없어서 대화를 하기가 조금 어렵기 때문에 지금은 디스코드가 권장되는 시스템입니다. |
|||
## 이슈 생성하기 |
|||
|
|||
### 질문을 위해 채팅을 사용하지 마십시오 |
|||
GitHub 저장소에 <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">새로운 이슈 생성</a>을 할 수 있습니다, 예를들면 다음과 같습니다: |
|||
|
|||
채팅은 더 많은 "자유로운 대화"를 허용하기 때문에, 너무 일반적인 질문이나 대답하기 어려운 질문을 쉽게 질문을 할 수 있으므로, 답변을 받지 못할 수 있습니다. |
|||
* **질문**을 하거나 **문제**에 대해 질문합니다. |
|||
* 새로운 **기능**을 제안 합니다. |
|||
|
|||
GitHub 이슈에서의 템플릿은 올바른 질문을 작성하도록 안내하여 더 쉽게 좋은 답변을 얻거나 질문하기 전에 스스로 문제를 해결할 수도 있습니다. 그리고 GitHub에서는 시간이 조금 걸리더라도 항상 모든 것에 답할 수 있습니다. 채팅 시스템에서는 개인적으로 그렇게 할 수 없습니다. 😅 |
|||
**참고**: 만약 이슈를 생성한다면, 저는 여러분에게 다른 사람들을 도와달라고 부탁할 것입니다. 😉 |
|||
|
|||
채팅 시스템에서의 대화 또한 GitHub에서 처럼 쉽게 검색할 수 없기 때문에 대화 중에 질문과 답변이 손실될 수 있습니다. 그리고 GitHub 이슈에 있는 것만 [FastAPI 전문가](https://github.com/fastapi/fastapi/blob/master/docs/en/docs/fastapi-people.md#experts)가 되는 것으로 간주되므로{.internal-link target=_blank} , GitHub 이슈에서 더 많은 관심을 받을 것입니다. |
|||
## Pull Requests 리뷰하기 |
|||
|
|||
반면, 채팅 시스템에는 수천 명의 사용자가 있기 때문에, 거의 항상 대화 상대를 찾을 가능성이 높습니다. 😄 |
|||
다른 사람들의 pull request를 리뷰하는 데 도움을 줄 수 있습니다. |
|||
|
|||
## 개발자 스폰서가 되십시오 |
|||
다시 한번 말하지만, 최대한 친절하게 리뷰해 주세요. 🤗 |
|||
|
|||
[GitHub 스폰서](https://github.com/sponsors/tiangolo) 를 통해 개발자를 경제적으로 지원할 수 있습니다. |
|||
--- |
|||
|
|||
감사하다는 말로 커피를 ☕️ 한잔 사줄 수 있습니다. 😄 |
|||
Pull Rrquest를 리뷰할 때 고려해야 할 사항과 방법은 다음과 같습니다: |
|||
|
|||
또한 FastAPI의 실버 또는 골드 스폰서가 될 수 있습니다. 🏅🎉 |
|||
### 문제 이해하기 |
|||
|
|||
## FastAPI를 강화하는 도구의 스폰서가 되십시오 |
|||
* 먼저, 해당 pull request가 해결하려는 **문제를 이해하는지** 확인하세요. GitHub 디스커션 또는 이슈에서 더 긴 논의가 있었을 수도 있습니다. |
|||
|
|||
문서에서 보았듯이, FastAPI는 Starlette과 Pydantic 라는 거인의 어깨에 타고 있습니다. |
|||
* Pull request가 필요하지 않을 가능성도 있습니다. **다른 방식**으로 문제를 해결할 수 있다면, 그 방법을 제안하거나 질문할 수 있습니다. |
|||
|
|||
다음의 스폰서가 될 수 있습니다 |
|||
### 스타일에 너무 신경 쓰지 않기 |
|||
|
|||
- [Samuel Colvin (Pydantic)](https://github.com/sponsors/samuelcolvin) |
|||
- [Encode (Starlette, Uvicorn)](https://github.com/sponsors/encode) |
|||
* 커밋 메시지 스타일 같은 것에 너무 신경 쓰지 않아도 됩니다. 저는 직접 커밋을 수정하여 squash and merge를 수행할 것입니다. |
|||
|
|||
------ |
|||
* 코드 스타일 규칙도 걱정할 필요 없습니다. 이미 자동화된 도구들이 이를 검사하고 있습니다. |
|||
|
|||
감사합니다! 🚀 |
|||
스타일이나 일관성 관련 요청이 필요한 경우, 제가 직접 요청하거나 필요한 변경 사항을 추가 커밋으로 수정할 것입니다. |
|||
|
|||
### 코드 확인하기 |
|||
|
|||
* 코드를 읽고, **논리적으로 타당**한지 확인한 후 로컬에서 실행하여 문제가 해결되는지 확인하세요. |
|||
|
|||
* 그런 다음, 확인했다고 **댓글**을 남겨 주세요. 그래야 제가 검토했음을 알 수 있습니다. |
|||
|
|||
/// info |
|||
|
|||
불행히도, 제가 단순히 여러 개의 승인만으로 PR을 신뢰할 수는 없습니다. |
|||
|
|||
3개, 5개 이상의 승인이 달린 PR이 실제로는 깨져 있거나, 버그가 있거나, 주장하는 문제를 해결하지 못하는 경우가 여러 번 있었습니다. 😅 |
|||
|
|||
따라서, 정말로 코드를 읽고 실행한 뒤, 댓글로 확인 내용을 남겨 주는 것이 매우 중요합니다. 🤓 |
|||
|
|||
/// |
|||
|
|||
* PR을 더 단순하게 만들 수 있다면 그렇게 요청할 수 있지만, 너무 까다로울 필요는 없습니다. 주관적인 견해가 많이 있을 수 있기 때문입니다 (그리고 저도 제 견해가 있을 거예요 🙈). 따라서 핵심적인 부분에 집중하는 것이 좋습니다. |
|||
|
|||
### 테스트 |
|||
|
|||
* PR에 **테스트**가 포함되어 있는지 확인하는 데 도움을 주세요. |
|||
|
|||
* PR을 적용하기 전에 테스트가 **실패**하는지 확인하세요. 🚨 |
|||
|
|||
* PR을 적용한 후 테스트가 **통과**하는지 확인하세요. ✅ |
|||
|
|||
* 많은 PR에는 테스트가 없습니다. 테스트를 추가하도록 **상기**시켜줄 수도 있고, 직접 테스트를 **제안**할 수도 있습니다. 이는 시간이 많이 소요되는 부분 중 하나이며, 그 부분을 많이 도와줄 수 있습니다. |
|||
|
|||
* 그리고 시도한 내용을 댓글로 남겨주세요. 그러면 제가 확인했다는 걸 알 수 있습니다. 🤓 |
|||
|
|||
## Pull Request를 만드십시오 |
|||
|
|||
Pull Requests를 이용하여 소스코드에 [컨트리뷰트](contributing.md){.internal-link target=\_blank} 할 수 있습니다. 예를 들면 다음과 같습니다: |
|||
|
|||
* 문서에서 발견한 오타를 수정할 때. |
|||
* FastAPI 관련 문서, 비디오 또는 팟캐스트를 작성했거나 발견하여 <a href="https://github.com/fastapi/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">이 파일을 편집하여</a> 공유할 때. |
|||
* 해당 섹션의 시작 부분에 링크를 추가해야 합니다. |
|||
* 당신의 언어로 [문서 번역하는데](contributing.md#translations){.internal-link target=\_blank} 기여할 때. |
|||
* 다른 사람이 작성한 번역을 검토하는 것도 도울 수 있습니다. |
|||
* 새로운 문서의 섹션을 제안할 때. |
|||
* 기존 문제/버그를 수정할 때. |
|||
* 테스트를 반드시 추가해야 합니다. |
|||
* 새로운 feature를 추가할 때. |
|||
* 테스트를 반드시 추가해야 합니다. |
|||
* 관련 문서가 필요하다면 반드시 추가해야 합니다. |
|||
|
|||
## FastAPI 유지 관리에 도움 주기 |
|||
|
|||
**FastAPI**의 유지 관리를 도와주세요! 🤓 |
|||
|
|||
할 일이 많고, 그 중 대부분은 **여러분**이 할 수 있습니다. |
|||
|
|||
지금 할 수 있는 주요 작업은: |
|||
|
|||
* [GitHub에서 다른 사람들의 질문에 도움 주기](#github_1){.internal-link target=_blank} (위의 섹션을 참조하세요). |
|||
* [Pull Request 리뷰하기](#pull-requests){.internal-link target=_blank} (위의 섹션을 참조하세요). |
|||
|
|||
이 두 작업이 **가장 많은 시간을 소모**하는 일입니다. 그것이 FastAPI 유지 관리의 주요 작업입니다. |
|||
|
|||
이 작업을 도와주신다면, **FastAPI 유지 관리에 도움을 주는 것**이며 그것이 **더 빠르고 더 잘 발전하는 것**을 보장하는 것입니다. 🚀 |
|||
|
|||
## 채팅에 참여하십시오 |
|||
|
|||
👥 <a href="https://discord.gg/VQjSZaeJmf" class="external-link" target="_blank">디스코드 채팅 서버</a> 👥 에 가입하고 FastAPI 커뮤니티에서 다른 사람들과 어울리세요. |
|||
|
|||
/// tip |
|||
|
|||
질문이 있는 경우, <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">GitHub 디스커션</a> 에서 질문하십시오, [FastAPI Experts](fastapi-people.md#fastapi-experts){.internal-link target=_blank} 의 도움을 받을 가능성이 높습니다. |
|||
|
|||
다른 일반적인 대화에서만 채팅을 사용하십시오. |
|||
|
|||
/// |
|||
|
|||
### 질문을 위해 채팅을 사용하지 마십시오 |
|||
|
|||
채팅은 더 많은 "자유로운 대화"를 허용하기 때문에, 너무 일반적인 질문이나 대답하기 어려운 질문을 쉽게 질문을 할 수 있으므로, 답변을 받지 못할 수 있습니다. |
|||
|
|||
GitHub 이슈에서의 템플릿은 올바른 질문을 작성하도록 안내하여 더 쉽게 좋은 답변을 얻거나 질문하기 전에 스스로 문제를 해결할 수도 있습니다. 그리고 GitHub에서는 시간이 조금 걸리더라도 항상 모든 것에 답할 수 있습니다. 채팅 시스템에서는 개인적으로 그렇게 할 수 없습니다. 😅 |
|||
|
|||
채팅 시스템에서의 대화 또한 GitHub에서 처럼 쉽게 검색할 수 없기 때문에 대화 중에 질문과 답변이 손실될 수 있습니다. 그리고 GitHub 이슈에 있는 것만 [FastAPI Expert](fastapi-people.md#fastapi-experts){.internal-link target=_blank}가 되는 것으로 간주되므로, GitHub 이슈에서 더 많은 관심을 받을 것입니다. |
|||
|
|||
반면, 채팅 시스템에는 수천 명의 사용자가 있기 때문에, 거의 항상 대화 상대를 찾을 가능성이 높습니다. 😄 |
|||
|
|||
## 개발자 스폰서가 되십시오 |
|||
|
|||
<a href="https://github.com/sponsors/tiangolo" class="external-link" target="_blank">GitHub 스폰서</a> 를 통해 개발자를 경제적으로 지원할 수 있습니다. |
|||
|
|||
감사하다는 말로 커피를 ☕️ 한잔 사줄 수 있습니다. 😄 |
|||
|
|||
또한 FastAPI의 실버 또는 골드 스폰서가 될 수 있습니다. 🏅🎉 |
|||
|
|||
## FastAPI를 강화하는 도구의 스폰서가 되십시오 |
|||
|
|||
문서에서 보았듯이, FastAPI는 Starlette과 Pydantic 라는 거인의 어깨에 타고 있습니다. |
|||
|
|||
다음의 스폰서가 될 수 있습니다 |
|||
|
|||
* <a href="https://github.com/sponsors/samuelcolvin" class="external-link" target="_blank">Samuel Colvin (Pydantic)</a> |
|||
* <a href="https://github.com/sponsors/encode" class="external-link" target="_blank">Encode (Starlette, Uvicorn)</a> |
|||
|
|||
--- |
|||
|
|||
감사합니다! 🚀 |
|||
|
@ -0,0 +1,275 @@ |
|||
# yield를 사용하는 의존성 |
|||
|
|||
FastAPI는 <abbr title='때로는 "종료 코드", "정리 코드", "종료 처리 코드", "닫기 코드", "컨텍스트 관리자 종료 코드" 등으로도 불립니다'>작업 완료 후 추가 단계를 수행하는</abbr> 의존성을 지원합니다. |
|||
|
|||
이를 구현하려면 `return` 대신 `yield`를 사용하고, 추가로 실행할 단계 (코드)를 그 뒤에 작성하세요. |
|||
|
|||
/// tip | 팁 |
|||
|
|||
각 의존성마다 `yield`는 한 번만 사용해야 합니다. |
|||
|
|||
/// |
|||
|
|||
/// note | 기술 세부사항 |
|||
|
|||
다음과 함께 사용할 수 있는 모든 함수: |
|||
|
|||
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" class="external-link" target="_blank">`@contextlib.contextmanager`</a> 또는 |
|||
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" class="external-link" target="_blank">`@contextlib.asynccontextmanager`</a> |
|||
|
|||
는 **FastAPI**의 의존성으로 사용할 수 있습니다. |
|||
|
|||
사실, FastAPI는 내부적으로 이 두 데코레이터를 사용합니다. |
|||
|
|||
/// |
|||
|
|||
## `yield`를 사용하는 데이터베이스 의존성 |
|||
|
|||
예를 들어, 이 기능을 사용하면 데이터베이스 세션을 생성하고 작업이 끝난 후에 세션을 종료할 수 있습니다. |
|||
|
|||
응답을 생성하기 전에는 `yield`문을 포함하여 그 이전의 코드만이 실행됩니다: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial007.py hl[2:4] *} |
|||
|
|||
yield된 값은 *경로 작업* 및 다른 의존성들에 주입되는 값 입니다: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial007.py hl[4] *} |
|||
|
|||
`yield`문 다음의 코드는 응답을 생성한 후 보내기 전에 실행됩니다: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial007.py hl[5:6] *} |
|||
|
|||
/// tip | 팁 |
|||
|
|||
`async` 함수와 일반 함수 모두 사용할 수 있습니다. |
|||
|
|||
**FastAPI**는 일반 의존성과 마찬가지로 각각의 함수를 올바르게 처리할 것입니다. |
|||
|
|||
/// |
|||
|
|||
## `yield`와 `try`를 사용하는 의존성 |
|||
|
|||
`yield`를 사용하는 의존성에서 `try` 블록을 사용한다면, 의존성을 사용하는 도중 발생한 모든 예외를 받을 수 있습니다. |
|||
|
|||
예를 들어, 다른 의존성이나 *경로 작업*의 중간에 데이터베이스 트랜잭션 "롤백"이 발생하거나 다른 오류가 발생한다면, 해당 예외를 의존성에서 받을 수 있습니다. |
|||
|
|||
따라서, 의존성 내에서 `except SomeException`을 사용하여 특정 예외를 처리할 수 있습니다. |
|||
|
|||
마찬가지로, `finally`를 사용하여 예외 발생 여부와 관계 없이 종료 단계까 실행되도록 할 수 있습니다. |
|||
|
|||
{* ../../docs_src/dependencies/tutorial007.py hl[3,5] *} |
|||
|
|||
## `yield`를 사용하는 하위 의존성 |
|||
|
|||
모든 크기와 형태의 하위 의존성과 하위 의존성의 "트리"도 가질 수 있으며, 이들 모두가 `yield`를 사용할 수 있습니다. |
|||
|
|||
**FastAPI**는 `yield`를 사용하는 각 의존성의 "종료 코드"가 올바른 순서로 실행되도록 보장합니다. |
|||
|
|||
예를 들어, `dependency_c`는 `dependency_b`에 의존할 수 있고, `dependency_b`는 `dependency_a`에 의존할 수 있습니다. |
|||
|
|||
{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[6,14,22] *} |
|||
|
|||
이들 모두는 `yield`를 사용할 수 있습니다. |
|||
|
|||
이 경우 `dependency_c`는 종료 코드를 실행하기 위해, `dependency_b`의 값 (여기서는 `dep_b`로 명명)이 여전히 사용 가능해야 합니다. |
|||
|
|||
그리고, `dependency_b`는 종료 코드를 위해 `dependency_a`의 값 (여기서는 `dep_a`로 명명) 이 사용 가능해야 합니다. |
|||
|
|||
{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[18:19,26:27] *} |
|||
|
|||
같은 방식으로, `yield`를 사용하는 의존성과 `return`을 사용하는 의존성을 함께 사용할 수 있으며, 이들 중 일부가 다른 것들에 의존할 수 있습니다. |
|||
|
|||
그리고 `yield`를 사용하는 다른 여러 의존성을 필요로 하는 단일 의존성을 가질 수도 있습니다. |
|||
|
|||
원하는 의존성을 원하는 대로 조합할 수 있습니다. |
|||
|
|||
**FastAPI**는 모든 것이 올바른 순서로 실행되도록 보장합니다. |
|||
|
|||
/// note | 기술 세부사항 |
|||
|
|||
파이썬의 <a href=“https://docs.python.org/3/library/contextlib.html” class=“external-link” target=“_blank”>Context Managers</a> 덕분에 이 기능이 작동합니다. |
|||
|
|||
**FastAPI**는 이를 내부적으로 컨텍스트 관리자를 사용하여 구현합니다. |
|||
|
|||
/// |
|||
|
|||
## `yield`와 `HTTPException`를 사용하는 의존성 |
|||
|
|||
`yield`와 `try` 블록이 있는 의존성을 사용하여 예외를 처리할 수 있다는 것을 알게 되었습니다. |
|||
|
|||
같은 방식으로, `yield` 이후의 종료 코드에서 `HTTPException`이나 유사한 예외를 발생시킬 수 있습니다. |
|||
|
|||
/// tip | 팁 |
|||
|
|||
이는 다소 고급 기술이며, 대부분의 경우 경로 연산 함수 등 나머지 애플리케이션 코드 내부에서 예외 (`HTTPException` 포함)를 발생시킬 수 있으므로 실제로는 필요하지 않을 것입니다. |
|||
|
|||
하지만 필요한 경우 사용할 수 있습니다. 🤓 |
|||
|
|||
/// |
|||
|
|||
{* ../../docs_src/dependencies/tutorial008b_an_py39.py hl[18:22,31] *} |
|||
|
|||
예외를 처리하고(또는 추가로 다른 `HTTPException`을 발생시키기 위해) 사용할 수 있는 또 다른 방법은 [사용자 정의 예외 처리기](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}를 생성하는 것 입니다. |
|||
|
|||
## `yield`와 `except`를 사용하는 의존성 |
|||
|
|||
`yield`를 사용하는 의존성에서 `except`를 사용하여 예외를 포착하고 예외를 다시 발생시키지 않거나 (또는 새 예외를 발생시키지 않으면), FastAPI는 해당 예외가 발생했는지 알 수 없습니다. 이는 일반적인 Python 방식과 동일합니다: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial008c_an_py39.py hl[15:16] *} |
|||
|
|||
이 경우, `HTTPException`이나 유사한 예외를 발생시키지 않기 때문에 클라이언트는 HTTP 500 Internal Server Error 응답을 보게 되지만, 서버는 어떤 오류가 발생했는지에 대한 **로그**나 다른 표시를 전혀 가지지 않게 됩니다. 😱 |
|||
|
|||
### `yield`와 `except`를 사용하는 의존성에서 항상 `raise` 하기 |
|||
|
|||
`yield`가 있는 의존성에서 예외를 잡았을 때는 `HTTPException`이나 유사한 예외를 새로 발생시키지 않는 한, 반드시 원래의 예외를 다시 발생시켜야 합니다. |
|||
|
|||
`raise`를 사용하여 동일한 예외를 다시 발생시킬 수 있습니다: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial008d_an_py39.py hl[17] *} |
|||
|
|||
이제 클라이언트는 동일한 *HTTP 500 Internal Server Error* 오류 응답을 받게 되지만, 서버 로그에는 사용자 정의 예외인 `InternalError"가 기록됩니다. 😎 |
|||
|
|||
## `yield`를 사용하는 의존성의 실행 순서 |
|||
|
|||
실행 순서는 아래 다이어그램과 거의 비슷합니다. 시간은 위에서 아래로 흐릅니다. 그리고 각 열은 상호 작용하거나 코드를 실행하는 부분 중 하나입니다. |
|||
|
|||
```mermaid |
|||
sequenceDiagram |
|||
|
|||
participant client as Client |
|||
participant handler as Exception handler |
|||
participant dep as Dep with yield |
|||
participant operation as Path Operation |
|||
participant tasks as Background tasks |
|||
|
|||
Note over client,operation: Can raise exceptions, including HTTPException |
|||
client ->> dep: Start request |
|||
Note over dep: Run code up to yield |
|||
opt raise Exception |
|||
dep -->> handler: Raise Exception |
|||
handler -->> client: HTTP error response |
|||
end |
|||
dep ->> operation: Run dependency, e.g. DB session |
|||
opt raise |
|||
operation -->> dep: Raise Exception (e.g. HTTPException) |
|||
opt handle |
|||
dep -->> dep: Can catch exception, raise a new HTTPException, raise other exception |
|||
end |
|||
handler -->> client: HTTP error response |
|||
end |
|||
|
|||
operation ->> client: Return response to client |
|||
Note over client,operation: Response is already sent, can't change it anymore |
|||
opt Tasks |
|||
operation -->> tasks: Send background tasks |
|||
end |
|||
opt Raise other exception |
|||
tasks -->> tasks: Handle exceptions in the background task code |
|||
end |
|||
``` |
|||
|
|||
/// info | 정보 |
|||
|
|||
클라이언트에 **하나의 응답** 만 전송됩니다. 이는 오류 응답 중 하나일 수도 있고,*경로 작업*에서 생성된 응답일 수도 있습니다. |
|||
|
|||
이러한 응답 중 하나가 전송된 후에는 다른 응답을 보낼 수 없습니다. |
|||
|
|||
/// |
|||
|
|||
/// tip | 팁 |
|||
|
|||
이 다이어그램은 `HTTPException`을 보여주지만, `yield`를 사용하는 의존성에서 처리한 예외나 [사용자 정의 예외처리기](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}.를 사용하여 처리한 다른 예외도 발생시킬 수 있습니다. |
|||
|
|||
어떤 예외가 발생하든, `HTTPException`을 포함하여 yield를 사용하는 의존성으로 전달됩니다. 대부분의 경우 예외를 다시 발생시키거나 새로운 예외를 발생시켜야 합니다. |
|||
|
|||
/// |
|||
|
|||
## `yield`, `HTTPException`, `except` 및 백그라운드 작업을 사용하는 의존성 |
|||
|
|||
/// warning | 경고 |
|||
|
|||
이러한 기술적 세부 사항은 대부분 필요하지 않으므로 이 섹션을 건너뛰고 아래에서 계속 진행해도 됩니다. |
|||
|
|||
이러한 세부 정보는 주로 FastAPI 0.106.0 이전 버전에서 `yield`가 있는 의존성의 리소스를 백그라운드 작업에서 사용했던 경우메 유용합니다. |
|||
|
|||
/// |
|||
|
|||
### `yield`와 `except`를 사용하는 의존성, 기술 세부사항 |
|||
|
|||
FastAPI 0.110.0 이전에는 `yield`가 포함된 의존성을 사용한 후 해당 의존성에서 `except`가 포함된 예외를 캡처하고 다시 예외를 발생시키지 않으면 예외가 자동으로 예외 핸들러 또는 내부 서버 오류 핸들러로 발생/전달되었습니다. |
|||
|
|||
이는 처리기 없이 전달된 예외(내부 서버 오류)에서 처리되지 않은 메모리 소비를 수정하고 일반 파이썬 코드의 동작과 일치하도록 하기 위해 0.110.0 버전에서 변경되었습니다. |
|||
|
|||
### 백그라운드 작업과 `yield`를 사용하는 의존성, 기술 세부사항 |
|||
|
|||
FastAPI 0.106.0 이전에는 `yield` 이후에 예외를 발생시키는 것이 불가능했습니다. `yield`가 있는 의존성 종료 코드는 응답이 전송된 이후에 실행되었기 때문에, [예외 처리기](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}가 이미 실행된 상태였습니다. |
|||
|
|||
이는 주로 백그라운드 작업 내에서 의존성에서 "yield된" 동일한 객체를 사용할 수 있도록 하기 위해 이런 방식으로 설계되었습니다. 종료 코드는 백그라운드 작업이 완료된 후에 실행되었기 때문입니다 |
|||
|
|||
하지만 이렇게 하면 리소스를 불필요하게 양보한 의존성(예: 데이터베이스 연결)에서 보유하면서 응답이 네트워크를 통해 이동할 때까지 기다리는 것을 의미하기 때문에 FastAPI 0.106.0에서 변경되었습니다. |
|||
|
|||
/// tip | 팁 |
|||
|
|||
또한 백그라운드 작업은 일반적으로 자체 리소스(예: 자체 데이터베이스 연결)를 사용하여 별도로 처리해야 하는 독립적인 로직 집합입니다. |
|||
|
|||
따라서 이렇게 하면 코드가 더 깔끔해집니다. |
|||
|
|||
/// |
|||
|
|||
만약 이전에 이러한 동작에 의존했다면, 이제는 백그라운드 작업 내부에서 백그라운드 작업을 위한 리소스를 생성하고, `yield`가 있는 의존성의 리소스에 의존하지 않는 데이터만 내부적으로 사용해야합니다. |
|||
|
|||
예를 들어, 동일한 데이터베이스 세션을 사용하는 대신, 백그라운드 작업 내부에서 새로운 데이터베이스 세션을 생성하고 이 새로운 세션을 사용하여 데이터베이스에서 객체를 가져와야 합니다. 그리고 데이터베이스 객체를 백그라운드 작업 함수의 매개변수로 직접 전달하는 대신, 해당 객체의 ID를 전달한 다음 백그라운드 작업 함수 내부에서 객체를 다시 가져와야 합니다 |
|||
|
|||
## 컨텍스트 관리자 |
|||
|
|||
### "컨텍스트 관리자"란? |
|||
|
|||
"컨텍스트 관리자"는 Python에서 `with` 문에서 사용할 수 있는 모든 객체를 의미합니다. |
|||
|
|||
예를 들어, <a href="https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files" class="external-link" target="_blank"> `with`를 사용하여 파일을 읽을 수 있습니다</a>: |
|||
|
|||
```Python |
|||
with open("./somefile.txt") as f: |
|||
contents = f.read() |
|||
print(contents) |
|||
``` |
|||
|
|||
내부적으로 `open("./somefile.txt")` 는 "컨텍스트 관리자(Context Manager)"라고 불리는 객체를 생성합니다. |
|||
|
|||
`with` 블록이 끝나면, 예외가 발생했더라도 파일을 닫도록 보장합니다. |
|||
|
|||
`yield`가 있는 의존성을 생성하면 **FastAPI**는 내부적으로 이를 위한 컨텍스트 매니저를 생성하고 다른 관련 도구들과 결합합니다. |
|||
|
|||
### `yield`를 사용하는 의존성에서 컨텍스트 관리자 사용하기 |
|||
|
|||
/// warning | 경고 |
|||
|
|||
이것은 어느 정도 "고급" 개념입니다. |
|||
|
|||
**FastAPI**를 처음 시작하는 경우 지금은 이 부분을 건너뛰어도 좋습니다. |
|||
|
|||
/// |
|||
|
|||
Python에서는 다음을 통해 컨텍스트 관리자를 생성할 수 있습니다. <a href="https://docs.python.org/3/reference/datamodel.html#context-managers" class="external-link" target="_blank"> 두 가지 메서드가 있는 클래스를 생성합니다: `__enter__()` and `__exit__()`</a>. |
|||
|
|||
**FastAPI**의 `yield`가 있는 의존성 내에서 |
|||
`with` 또는 `async with`문을 사용하여 이들을 활용할 수 있습니다: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial010.py hl[1:9,13] *} |
|||
|
|||
/// tip | 팁 |
|||
|
|||
컨텍스트 관리자를 생성하는 또 다른 방법은 다음과 같습니다: |
|||
|
|||
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" class="external-link" target="_blank">`@contextlib.contextmanager`</a> 또는 |
|||
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" class="external-link" target="_blank">`@contextlib.asynccontextmanager`</a> |
|||
|
|||
이들은 단일 `yield`가 있는 함수를 꾸미는 데 사용합니다. |
|||
|
|||
이것이 **FastAPI**가 `yield`가 있는 의존성을 위해 내부적으로 사용하는 방식입니다. |
|||
|
|||
하지만 FastAPI 의존성에는 이러한 데코레이터를 사용할 필요가 없습니다(그리고 사용해서도 안됩니다). |
|||
|
|||
FastAPI가 내부적으로 이를 처리해 줄 것입니다. |
|||
|
|||
/// |
@ -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,48 @@ |
|||
|
|||
# Cookies в ответе |
|||
|
|||
## Использование параметра `Response` |
|||
|
|||
Вы можете объявить параметр типа `Response` в вашей функции эндпоинта. |
|||
|
|||
Затем установить cookies в этом временном объекте ответа. |
|||
|
|||
{* ../../docs_src/response_cookies/tutorial002.py hl[1, 8:9] *} |
|||
|
|||
После этого можно вернуть любой объект, как и раньше (например, `dict`, объект модели базы данных и так далее). |
|||
|
|||
Если вы указали `response_model`, он всё равно будет использоваться для фильтрации и преобразования возвращаемого объекта. |
|||
|
|||
**FastAPI** извлечет cookies (а также заголовки и коды состояния) из временного ответа и включит их в окончательный ответ, содержащий ваше возвращаемое значение, отфильтрованное через `response_model`. |
|||
|
|||
Вы также можете объявить параметр типа Response в зависимостях и устанавливать cookies (и заголовки) там. |
|||
|
|||
## Возвращение `Response` напрямую |
|||
|
|||
Вы также можете установить cookies, если возвращаете `Response` напрямую в вашем коде. |
|||
|
|||
Для этого создайте объект `Response`, как описано в разделе [Возвращение ответа напрямую](response-directly.md){.target=_blank}. |
|||
|
|||
Затем установите cookies и верните этот объект: |
|||
|
|||
{* ../../docs_src/response_cookies/tutorial001.py hl[10:12] *} |
|||
|
|||
/// tip | Примечание |
|||
Имейте в виду, что если вы возвращаете ответ напрямую, вместо использования параметра `Response`, **FastAPI** отправит его без дополнительной обработки. |
|||
|
|||
Убедитесь, что ваши данные имеют корректный тип. Например, они должны быть совместимы с JSON, если вы используете `JSONResponse`. |
|||
|
|||
Также убедитесь, что вы не отправляете данные, которые должны были быть отфильтрованы через `response_model`. |
|||
/// |
|||
|
|||
### Дополнительная информация |
|||
|
|||
/// note | Технические детали |
|||
Вы также можете использовать `from starlette.responses import Response` или `from starlette.responses import JSONResponse`. |
|||
|
|||
**FastAPI** предоставляет `fastapi.responses`, которые являются теми же объектами, что и `starlette.responses`, просто для удобства. Однако большинство доступных типов ответов поступает непосредственно из **Starlette**. |
|||
|
|||
Для установки заголовков и cookies `Response` используется часто, поэтому **FastAPI** также предоставляет его через `fastapi.responses`. |
|||
/// |
|||
|
|||
Чтобы увидеть все доступные параметры и настройки, ознакомьтесь с <a href="https://www.starlette.io/responses/#set-cookie" class="external-link" target="_blank">документацией Starlette</a>. |
@ -0,0 +1,186 @@ |
|||
# Веб-сокеты |
|||
|
|||
Вы можете использовать <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API" class="external-link" target="_blank">веб-сокеты</a> в **FastAPI**. |
|||
|
|||
## Установка `WebSockets` |
|||
|
|||
Убедитесь, что [виртуальная среда](../virtual-environments.md){.internal-link target=_blank} создана, активируйте её и установите `websockets`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install websockets |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Клиент WebSockets |
|||
|
|||
### Рабочее приложение |
|||
|
|||
Скорее всего, в вашей реальной продуктовой системе есть фронтенд, реализованный при помощи современных фреймворков React, Vue.js или Angular. |
|||
|
|||
И наверняка для взаимодействия с бекендом через веб-сокеты вы будете использовать средства фронтенда. |
|||
|
|||
Также у вас может быть нативное мобильное приложение, коммуницирующее непосредственно с веб-сокетами на бекенд-сервере. |
|||
|
|||
Либо вы можете сделать какой-либо другой способ взаимодействия с веб-сокетами. |
|||
|
|||
--- |
|||
|
|||
Но для этого примера мы воспользуемся очень простым HTML документом с небольшими вставками JavaScript кода. |
|||
|
|||
Конечно же это неоптимально, и на практике так делать не стоит. |
|||
|
|||
В реальных приложениях стоит воспользоваться одним из вышеупомянутых способов. |
|||
|
|||
Для примера нам нужен наиболее простой способ, который позволит сосредоточиться на серверной части веб-сокетов и получить рабочий код: |
|||
|
|||
{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} |
|||
|
|||
## Создание `websocket` |
|||
|
|||
Создайте `websocket` в своем **FastAPI** приложении: |
|||
|
|||
{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *} |
|||
|
|||
/// note | Технические детали |
|||
|
|||
Вы также можете использовать `from starlette.websockets import WebSocket`. |
|||
|
|||
**FastAPI** напрямую предоставляет тот же самый `WebSocket` просто для удобства. На самом деле это `WebSocket` из Starlette. |
|||
|
|||
/// |
|||
|
|||
## Ожидание и отправка сообщений |
|||
|
|||
Через эндпоинт веб-сокета вы можете получать и отправлять сообщения. |
|||
|
|||
{* ../../docs_src/websockets/tutorial001.py hl[48:52] *} |
|||
|
|||
Вы можете получать и отправлять двоичные, текстовые и JSON данные. |
|||
|
|||
## Проверка в действии |
|||
|
|||
Если ваш файл называется `main.py`, то запустите приложение командой: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev 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" class="external-link" target="_blank">http://127.0.0.1:8000</a>. |
|||
|
|||
Вы увидите следующую простенькую страницу: |
|||
|
|||
<img src="/img/tutorial/websockets/image01.png"> |
|||
|
|||
Вы можете набирать сообщения в поле ввода и отправлять их: |
|||
|
|||
<img src="/img/tutorial/websockets/image02.png"> |
|||
|
|||
И ваше **FastAPI** приложение с веб-сокетами ответит: |
|||
|
|||
<img src="/img/tutorial/websockets/image03.png"> |
|||
|
|||
Вы можете отправлять и получать множество сообщений: |
|||
|
|||
<img src="/img/tutorial/websockets/image04.png"> |
|||
|
|||
И все они будут использовать одно и то же веб-сокет соединение. |
|||
|
|||
## Использование `Depends` и не только |
|||
|
|||
Вы можете импортировать из `fastapi` и использовать в эндпоинте вебсокета: |
|||
|
|||
* `Depends` |
|||
* `Security` |
|||
* `Cookie` |
|||
* `Header` |
|||
* `Path` |
|||
* `Query` |
|||
|
|||
Они работают так же, как и в других FastAPI эндпоинтах/*операциях пути*: |
|||
|
|||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} |
|||
|
|||
/// info | Примечание |
|||
|
|||
В веб-сокете вызывать `HTTPException` не имеет смысла. Вместо этого нужно использовать `WebSocketException`. |
|||
|
|||
Закрывающий статус код можно использовать из <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1" class="external-link" target="_blank">valid codes defined in the specification</a>. |
|||
|
|||
/// |
|||
|
|||
### Веб-сокеты с зависимостями: проверка в действии |
|||
|
|||
Если ваш файл называется `main.py`, то запустите приложение командой: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev 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" class="external-link" target="_blank">http://127.0.0.1:8000</a>. |
|||
|
|||
Там вы можете задать: |
|||
|
|||
* "Item ID", используемый в пути. |
|||
* "Token", используемый как query-параметр. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Обратите внимание, что query-параметр `token` будет обработан в зависимости. |
|||
|
|||
/// |
|||
|
|||
Теперь вы можете подключиться к веб-сокету и начинать отправку и получение сообщений: |
|||
|
|||
<img src="/img/tutorial/websockets/image05.png"> |
|||
|
|||
## Обработка отключений и работа с несколькими клиентами |
|||
|
|||
Если веб-сокет соединение закрыто, то `await websocket.receive_text()` вызовет исключение `WebSocketDisconnect`, которое можно поймать и обработать как в этом примере: |
|||
|
|||
{* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *} |
|||
|
|||
Чтобы воспроизвести пример: |
|||
|
|||
* Откройте приложение в нескольких вкладках браузера. |
|||
* Отправьте из них сообщения. |
|||
* Затем закройте одну из вкладок. |
|||
|
|||
Это вызовет исключение `WebSocketDisconnect`, и все остальные клиенты получат следующее сообщение: |
|||
|
|||
``` |
|||
Client #1596980209979 left the chat |
|||
``` |
|||
|
|||
/// tip | Примечание |
|||
|
|||
Приложение выше - это всего лишь простой минимальный пример, демонстрирующий обработку и передачу сообщений нескольким веб-сокет соединениям. |
|||
|
|||
Но имейте в виду, что это будет работать только в одном процессе и только пока он активен, так как всё обрабатывается в простом списке в оперативной памяти. |
|||
|
|||
Если нужно что-то легко интегрируемое с FastAPI, но более надежное и с поддержкой Redis, PostgreSQL или другого, то можно воспользоваться <a href="https://github.com/encode/broadcaster" class="external-link" target="_blank">encode/broadcaster</a>. |
|||
|
|||
/// |
|||
|
|||
## Дополнительная информация |
|||
|
|||
Для более глубокого изучения темы воспользуйтесь документацией Starlette: |
|||
|
|||
* <a href="https://www.starlette.io/websockets/" class="external-link" target="_blank">The `WebSocket` class</a>. |
|||
* <a href="https://www.starlette.io/endpoints/#websocketendpoint" class="external-link" target="_blank">Class-based WebSocket handling</a>. |
@ -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` были также подключены. |
@ -0,0 +1,189 @@ |
|||
# Функціональні можливості |
|||
|
|||
## Функціональні можливості FastAPI |
|||
|
|||
**FastAPI** надає вам такі можливості: |
|||
|
|||
### Використання відкритих стандартів |
|||
|
|||
* <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank"><strong>OpenAPI</strong></a> для створення API, включаючи оголошення <abbr title="також відомі як: endpoints, маршрути">шляхів</abbr>, <abbr title="також відомі як HTTP-методи, наприклад, POST, GET, PUT, DELETE">операцій</abbr>, параметрів, тіл запитів, безпеки тощо. |
|||
* Автоматична документація моделей даних за допомогою <a href="https://json-schema.org/" class="external-link" target="_blank"><strong>JSON Schema</strong></a> (оскільки OpenAPI базується саме на JSON Schema). |
|||
* Розроблено на основі цих стандартів після ретельного аналізу, а не як додатковий рівень поверх основної архітектури. |
|||
* Це також дає змогу автоматично **генерувати код клієнта** багатьма мовами. |
|||
|
|||
### Автоматична генерація документації |
|||
|
|||
Інтерактивна документація API та вебінтерфейс для його дослідження. Оскільки фреймворк базується на OpenAPI, є кілька варіантів, два з яких включені за замовчуванням. |
|||
|
|||
* <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank"><strong>Swagger UI</strong></a> — дозволяє інтерактивно переглядати API, викликати та тестувати його прямо у браузері. |
|||
|
|||
 |
|||
|
|||
* Альтернативна документація API за допомогою <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank"><strong>ReDoc</strong></a>. |
|||
|
|||
 |
|||
|
|||
### Тільки сучасний Python |
|||
|
|||
FastAPI використовує стандартні **типи Python** (завдяки Pydantic). Вам не потрібно вивчати новий синтаксис — лише стандартний сучасний Python. |
|||
|
|||
Якщо вам потрібне коротке нагадування про використання типів у Python (навіть якщо ви не використовуєте FastAPI), перегляньте короткий підручник: [Вступ до типів Python](python-types.md){.internal-link target=_blank}. |
|||
|
|||
Ось приклад стандартного Python-коду з типами: |
|||
|
|||
```Python |
|||
from datetime import date |
|||
from pydantic import BaseModel |
|||
|
|||
# Оголошення змінної як str |
|||
# з підтримкою автодоповнення у редакторі |
|||
def main(user_id: str): |
|||
return user_id |
|||
|
|||
# Модель Pydantic |
|||
class User(BaseModel): |
|||
id: int |
|||
name: str |
|||
joined: date |
|||
``` |
|||
|
|||
Приклад використання цієї моделі: |
|||
|
|||
```Python |
|||
my_user: User = User(id=3, name="John Doe", joined="2018-07-19") |
|||
|
|||
second_user_data = { |
|||
"id": 4, |
|||
"name": "Mary", |
|||
"joined": "2018-11-30", |
|||
} |
|||
|
|||
my_second_user: User = User(**second_user_data) |
|||
``` |
|||
|
|||
/// info | Інформація |
|||
|
|||
`**second_user_data` означає: |
|||
|
|||
Передати ключі та значення словника `second_user_data` як аргументи у вигляді "ключ-значення", еквівалентно `User(id=4, name="Mary", joined="2018-11-30")`. |
|||
|
|||
/// |
|||
|
|||
### Підтримка редакторів (IDE) |
|||
|
|||
Фреймворк спроєктований так, щоб бути легким і інтуїтивно зрозумілим. Усі рішення тестувалися у різних редакторах ще до початку розробки, щоб забезпечити найкращий досвід програмування. |
|||
|
|||
За результатами опитувань розробників Python <a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank">однією з найпопулярніших функцій є "автодоповнення"</a>. |
|||
|
|||
**FastAPI** повністю підтримує автодоповнення у всіх місцях, тому вам рідко доведеться повертатися до документації. |
|||
|
|||
Приклад автодоповнення у редакторах: |
|||
|
|||
* у <a href="https://code.visualstudio.com/" class="external-link" target="_blank">Visual Studio Code</a>: |
|||
|
|||
 |
|||
|
|||
* у <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a>: |
|||
|
|||
 |
|||
|
|||
### Короткий код |
|||
FastAPI має розумні налаштування **за замовчуванням**, але всі параметри можна налаштовувати відповідно до ваших потреб. Однак за замовчуванням все "просто працює". |
|||
|
|||
### Валідація |
|||
* Підтримка валідації для більшості (або всіх?) **типів даних Python**, зокрема: |
|||
* JSON-об'єктів (`dict`). |
|||
* JSON-списків (`list`) з визначенням типів елементів. |
|||
* Рядків (`str`) із мінімальною та максимальною довжиною. |
|||
* Чисел (`int`, `float`) з обмеженнями мінімальних та максимальних значень тощо. |
|||
|
|||
* Валідація складніших типів, таких як: |
|||
* URL. |
|||
* Email. |
|||
* UUID. |
|||
* ...та інші. |
|||
|
|||
Уся валідація виконується через надійний та перевірений **Pydantic**. |
|||
|
|||
### Безпека та автентифікація |
|||
|
|||
**FastAPI** підтримує вбудовану автентифікацію та авторизацію, без прив’язки до конкретних баз даних чи моделей даних. |
|||
|
|||
Підтримуються всі схеми безпеки OpenAPI, включаючи: |
|||
|
|||
* HTTP Basic. |
|||
* **OAuth2** (також із підтримкою **JWT-токенів**). Див. підручник: [OAuth2 із JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. |
|||
* Ключі API в: |
|||
* Заголовках. |
|||
* Параметрах запиту. |
|||
* Cookies тощо. |
|||
|
|||
А також усі можливості безпеки від Starlette (зокрема **сесійні cookies**). |
|||
|
|||
Усі вони створені як багаторазові інструменти та компоненти, які легко інтегруються з вашими системами, сховищами даних, реляційними та NoSQL базами даних тощо. |
|||
|
|||
### Впровадження залежностей |
|||
|
|||
**FastAPI** містить надзвичайно просту у використанні, але потужну систему впровадження залежностей. |
|||
|
|||
* Залежності можуть мати власні залежності, утворюючи ієрархію або **"граф залежностей"**. |
|||
* Усі залежності автоматично керуються фреймворком. |
|||
* Усі залежності можуть отримувати дані з запитів і розширювати **обмеження операції за шляхом** та автоматичну документацію. |
|||
* **Автоматична валідація** навіть для параметрів *операцій шляху*, визначених у залежностях. |
|||
* Підтримка складних систем автентифікації користувачів, **з'єднань із базами даних** тощо. |
|||
* **Жодних обмежень** щодо використання баз даних, фронтендів тощо, але водночас проста інтеграція з усіма ними. |
|||
|
|||
### Немає обмежень на "плагіни" |
|||
|
|||
Або іншими словами, вони не потрібні – просто імпортуйте та використовуйте необхідний код. |
|||
|
|||
Будь-яка інтеграція спроєктована настільки просто (з використанням залежностей), що ви можете створити "плагін" для свого застосунку всього у 2 рядках коду, використовуючи ту саму структуру та синтаксис, що й для ваших *операцій шляху*. |
|||
|
|||
### Протестовано |
|||
|
|||
* 100% <abbr title="Обсяг коду, що автоматично тестується">покриття тестами</abbr>. |
|||
* 100% <abbr title="Анотації типів у Python, завдяки яким ваш редактор і зовнішні інструменти можуть надавати кращу підтримку">анотована типами</abbr> кодова база. |
|||
* Використовується у робочих середовищах. |
|||
|
|||
## Можливості Starlette |
|||
|
|||
**FastAPI** повністю сумісний із (та побудований на основі) <a href="https://www.starlette.io/" class="external-link" target="_blank"><strong>Starlette</strong></a>. Тому будь-який додатковий код Starlette, який ви маєте, також працюватиме. |
|||
|
|||
**FastAPI** фактично є підкласом **Starlette**. Тому, якщо ви вже знайомі зі Starlette або використовуєте його, більшість функціональності працюватиме так само. |
|||
|
|||
З **FastAPI** ви отримуєте всі можливості **Starlette** (адже FastAPI — це, по суті, Starlette на стероїдах): |
|||
|
|||
* Разюча продуктивність. Це <a href="https://github.com/encode/starlette#performance" class="external-link" target="_blank">один із найшвидших фреймворків на Python</a>, на рівні з **NodeJS** і **Go**. |
|||
* Підтримка **WebSocket**. |
|||
* Фонові задачі у процесі. |
|||
* Події запуску та завершення роботи. |
|||
* Клієнт для тестування, побудований на HTTPX. |
|||
* Підтримка **CORS**, **GZip**, статичних файлів, потокових відповідей. |
|||
* Підтримка **сесій** і **cookie**. |
|||
* 100% покриття тестами. |
|||
* 100% анотована типами кодова база. |
|||
|
|||
## Можливості Pydantic |
|||
|
|||
**FastAPI** повністю сумісний із (та побудований на основі) <a href="https://docs.pydantic.dev/" class="external-link" target="_blank"><strong>Pydantic</strong></a>. Тому будь-який додатковий код Pydantic, який ви маєте, також працюватиме. |
|||
|
|||
Включаючи зовнішні бібліотеки, побудовані також на Pydantic, такі як <abbr title="Object-Relational Mapper">ORM</abbr>, <abbr title="Object-Document Mapper">ODM</abbr> для баз даних. |
|||
|
|||
Це також означає, що в багатьох випадках ви можете передати той самий об'єкт, який отримуєте з запиту, **безпосередньо в базу даних**, оскільки все автоматично перевіряється. |
|||
|
|||
Те ж саме відбувається й у зворотному напрямку — у багатьох випадках ви можете просто передати об'єкт, який отримуєте з бази даних, **безпосередньо клієнту**. |
|||
|
|||
З **FastAPI** ви отримуєте всі можливості **Pydantic** (адже FastAPI базується на Pydantic для обробки всіх даних): |
|||
|
|||
* **Ніякої плутанини** : |
|||
* Не потрібно вчити нову мову для визначення схем. |
|||
* Якщо ви знаєте типи Python, ви знаєте, як використовувати Pydantic. |
|||
* Легко працює з вашим **<abbr title="Інтегроване середовище розробки, схоже на редактор коду">IDE</abbr>/<abbr title="Програма, яка перевіряє помилки в коді">лінтером</abbr>/мозком**: |
|||
* Оскільки структури даних Pydantic є просто екземплярами класів, які ви визначаєте; автодоповнення, лінтинг, mypy і ваша інтуїція повинні добре працювати з вашими перевіреними даними. |
|||
* Валідація **складних структур**: |
|||
* Використання ієрархічних моделей Pydantic. Python `typing`, `List` і `Dict` тощо. |
|||
* Валідатори дозволяють чітко і просто визначати, перевіряти й документувати складні схеми даних у вигляді JSON-схеми. |
|||
* Ви можете мати глибоко **вкладені JSON об'єкти** та перевірити та анотувати їх всі. |
|||
* **Розширюваність**: |
|||
* Pydantic дозволяє визначати користувацькі типи даних або розширювати валідацію методами в моделі декоратором `validator`. |
|||
* 100% покриття тестами. |
@ -0,0 +1,5 @@ |
|||
# Навчання |
|||
|
|||
У цьому розділі надані вступні та навчальні матеріали для вивчення FastAPI. |
|||
|
|||
Це можна розглядати як **книгу**, **курс**, або **офіційний** та рекомендований спосіб освоїти FastAPI. 😎 |
@ -0,0 +1,40 @@ |
|||
# Статичні файли |
|||
|
|||
Ви можете автоматично надавати статичні файли з каталогу, використовуючи `StaticFiles`. |
|||
|
|||
## Використання `StaticFiles` |
|||
|
|||
* Імпортуйте `StaticFiles`. |
|||
* "Під'єднати" екземпляр `StaticFiles()` з вказанням необхідного шляху. |
|||
|
|||
{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} |
|||
|
|||
/// note | Технічні деталі |
|||
|
|||
Ви також можете використовувати `from starlette.staticfiles import StaticFiles`. |
|||
|
|||
**FastAPI** надає той самий `starlette.staticfiles`, що й `fastapi.staticfiles` для зручності розробників. Але фактично він безпосередньо походить із Starlette. |
|||
|
|||
/// |
|||
|
|||
### Що таке "Під'єднання" |
|||
|
|||
"Під'єднання" означає додавання повноцінного "незалежного" застосунку за певним шляхом, який потім обробляє всі під шляхи. |
|||
|
|||
Це відрізняється від використання `APIRouter`, оскільки під'єднаний застосунок є повністю незалежним. OpenAPI та документація вашого основного застосунку не будуть знати нічого про ваш під'єднаний застосунок. |
|||
|
|||
Ви можете дізнатися більше про це в [Посібнику для просунутих користувачів](../advanced/index.md){.internal-link target=_blank}. |
|||
|
|||
## Деталі |
|||
|
|||
Перше `"/static"` вказує на під шлях, за яким буде "під'єднано" цей новий "застосунок". Тому будь-який шлях, який починається з `"/static"`, буде оброблятися ним. |
|||
|
|||
`directory="static"` визначає каталог, що містить ваші статичні файли. |
|||
|
|||
`name="static"` це ім'я, яке можна використовувати всередині **FastAPI**. |
|||
|
|||
Усі ці параметри можуть бути змінені відповідно до потреб і особливостей вашого застосунку. |
|||
|
|||
## Додаткова інформація |
|||
|
|||
Детальніше про налаштування та можливості можна дізнатися в <a href="https://www.starlette.io/staticfiles/" class="external-link" target="_blank">документації Starlette про статичні файли</a>. |
@ -0,0 +1,300 @@ |
|||
# Biến môi trường (Environment Variables) |
|||
|
|||
/// tip |
|||
|
|||
Nếu bạn đã biết về "biến môi trường" và cách sử dụng chúng, bạn có thể bỏ qua phần này. |
|||
|
|||
/// |
|||
|
|||
Một biến môi trường (còn được gọi là "**env var**") là một biến mà tồn tại **bên ngoài** đoạn mã Python, ở trong **hệ điều hành**, và có thể được đọc bởi đoạn mã Python của bạn (hoặc bởi các chương trình khác). |
|||
|
|||
Các biến môi trường có thể được sử dụng để xử lí **các thiết lập** của ứng dụng, như một phần của **các quá trình cài đặt** Python, v.v. |
|||
|
|||
## Tạo và Sử dụng các Biến Môi Trường |
|||
|
|||
Bạn có thể **tạo** và sử dụng các biến môi trường trong **shell (terminal)**, mà không cần sử dụng Python: |
|||
|
|||
//// tab | Linux, macOS, Windows Bash |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Bạn có thể tạo một biến môi trường MY_NAME với |
|||
$ export MY_NAME="Wade Wilson" |
|||
|
|||
// Sau đó bạn có thể sử dụng nó với các chương trình khác, như |
|||
$ echo "Hello $MY_NAME" |
|||
|
|||
Hello Wade Wilson |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Tạo một biến môi trường MY_NAME |
|||
$ $Env:MY_NAME = "Wade Wilson" |
|||
|
|||
// Sử dụng nó với các chương trình khác, như là |
|||
$ echo "Hello $Env:MY_NAME" |
|||
|
|||
Hello Wade Wilson |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
## Đọc các Biến Môi Trường trong Python |
|||
|
|||
Bạn cũng có thể tạo các biến môi trường **bên ngoài** đoạn mã Python, trong terminal (hoặc bằng bất kỳ phương pháp nào khác), và sau đó **đọc chúng trong Python**. |
|||
|
|||
Ví dụ, bạn có một file `main.py` với: |
|||
|
|||
```Python hl_lines="3" |
|||
import os |
|||
|
|||
name = os.getenv("MY_NAME", "World") |
|||
print(f"Hello {name} from Python") |
|||
``` |
|||
|
|||
/// tip |
|||
|
|||
Tham số thứ hai cho <a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> là giá trị mặc định để trả về. |
|||
|
|||
Nếu không được cung cấp, nó mặc định là `None`, ở đây chúng ta cung cấp `"World"` là giá trị mặc định để sử dụng. |
|||
|
|||
/// |
|||
|
|||
Sau đó bạn có thể gọi chương trình Python: |
|||
|
|||
//// tab | Linux, macOS, Windows Bash |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Ở đây chúng ta chưa cài đặt biến môi trường |
|||
$ python main.py |
|||
|
|||
// Vì chúng ta chưa cài đặt biến môi trường, chúng ta nhận được giá trị mặc định |
|||
|
|||
Hello World from Python |
|||
|
|||
// Nhưng nếu chúng ta tạo một biến môi trường trước đó |
|||
$ export MY_NAME="Wade Wilson" |
|||
|
|||
// Và sau đó gọi chương trình lại |
|||
$ python main.py |
|||
|
|||
// Bây giờ nó có thể đọc biến môi trường |
|||
|
|||
Hello Wade Wilson from Python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Ở đây chúng ta chưa cài đặt biến môi trường |
|||
$ python main.py |
|||
|
|||
// Vì chúng ta chưa cài đặt biến môi trường, chúng ta nhận được giá trị mặc định |
|||
|
|||
Hello World from Python |
|||
|
|||
// Nhưng nếu chúng ta tạo một biến môi trường trước đó |
|||
$ $Env:MY_NAME = "Wade Wilson" |
|||
|
|||
// Và sau đó gọi chương trình lại |
|||
$ python main.py |
|||
|
|||
// Bây giờ nó có thể đọc biến môi trường |
|||
|
|||
Hello Wade Wilson from Python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
Vì các biến môi trường có thể được tạo bên ngoài đoạn mã Python, nhưng có thể được đọc bởi đoạn mã Python, và không cần được lưu trữ (commit vào `git`) cùng với các file khác, nên chúng thường được sử dụng để lưu các thiết lập hoặc **cấu hình**. |
|||
|
|||
Bạn cũng có thể tạo ra một biến môi trường dành riêng cho một **lần gọi chương trình**, chỉ có thể được sử dụng bởi chương trình đó, và chỉ trong thời gian chạy của chương trình. |
|||
|
|||
Để làm điều này, tạo nó ngay trước chương trình đó, trên cùng một dòng: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Tạo một biến môi trường MY_NAME cho lần gọi chương trình này |
|||
$ MY_NAME="Wade Wilson" python main.py |
|||
|
|||
// Bây giờ nó có thể đọc biến môi trường |
|||
|
|||
Hello Wade Wilson from Python |
|||
|
|||
// Biến môi trường không còn tồn tại sau đó |
|||
$ python main.py |
|||
|
|||
Hello World from Python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// tip |
|||
|
|||
Bạn có thể đọc thêm về điều này tại <a href="https://12factor.net/config" class="external-link" target="_blank">The Twelve-Factor App: Config</a>. |
|||
|
|||
/// |
|||
|
|||
## Các Kiểu (Types) và Kiểm tra (Validation) |
|||
|
|||
Các biến môi trường có thể chỉ xử lí **chuỗi ký tự**, vì chúng nằm bên ngoài đoạn mã Python và phải tương thích với các chương trình khác và phần còn lại của hệ thống (và thậm chí với các hệ điều hành khác, như Linux, Windows, macOS). |
|||
|
|||
Điều này có nghĩa là **bất kỳ giá trị nào** được đọc trong Python từ một biến môi trường **sẽ là một `str`**, và bất kỳ hành động chuyển đổi sang kiểu dữ liệu khác hoặc hành động kiểm tra nào cũng phải được thực hiện trong đoạn mã. |
|||
|
|||
Bạn sẽ học thêm về việc sử dụng biến môi trường để xử lí **các thiết lập ứng dụng** trong [Hướng dẫn nâng cao - Các thiết lập và biến môi trường](./advanced/settings.md){.internal-link target=_blank}. |
|||
|
|||
## Biến môi trường `PATH` |
|||
|
|||
Có một biến môi trường **đặc biệt** được gọi là **`PATH`** được sử dụng bởi các hệ điều hành (Linux, macOS, Windows) nhằm tìm các chương trình để thực thi. |
|||
|
|||
Giá trị của biến môi trường `PATH` là một chuỗi dài được tạo bởi các thư mục được phân tách bởi dấu hai chấm `:` trên Linux và macOS, và bởi dấu chấm phẩy `;` trên Windows. |
|||
|
|||
Ví dụ, biến môi trường `PATH` có thể có dạng như sau: |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
```plaintext |
|||
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin |
|||
``` |
|||
|
|||
Điều này có nghĩa là hệ thống sẽ tìm kiếm các chương trình trong các thư mục: |
|||
|
|||
* `/usr/local/bin` |
|||
* `/usr/bin` |
|||
* `/bin` |
|||
* `/usr/sbin` |
|||
* `/sbin` |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
```plaintext |
|||
C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 |
|||
``` |
|||
|
|||
Điều này có nghĩa là hệ thống sẽ tìm kiếm các chương trình trong các thư mục: |
|||
|
|||
* `C:\Program Files\Python312\Scripts` |
|||
* `C:\Program Files\Python312` |
|||
* `C:\Windows\System32` |
|||
|
|||
//// |
|||
|
|||
Khi bạn gõ một **lệnh** trong terminal, hệ điều hành **tìm kiếm** chương trình trong **mỗi thư mục** được liệt kê trong biến môi trường `PATH`. |
|||
|
|||
Ví dụ, khi bạn gõ `python` trong terminal, hệ điều hành tìm kiếm một chương trình được gọi `python` trong **thư mục đầu tiên** trong danh sách đó. |
|||
|
|||
Nếu tìm thấy, nó sẽ **sử dụng** nó. Nếu không tìm thấy, nó sẽ tiếp tục tìm kiếm trong **các thư mục khác**. |
|||
|
|||
### Cài đặt Python và cập nhật biến môi trường `PATH` |
|||
|
|||
Khi bạn cài đặt Python, bạn có thể được hỏi nếu bạn muốn cập nhật biến môi trường `PATH`. |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
Giả sử bạn cài đặt Python vào thư mục `/opt/custompython/bin`. |
|||
|
|||
Nếu bạn chọn cập nhật biến môi trường `PATH`, thì cài đặt sẽ thêm `/opt/custompython/bin` vào biến môi trường `PATH`. |
|||
|
|||
Nó có thể có dạng như sau: |
|||
|
|||
```plaintext |
|||
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin |
|||
``` |
|||
|
|||
Như vậy, khi bạn gõ `python` trong terminal, hệ thống sẽ tìm thấy chương trình Python trong `/opt/custompython/bin` (thư mục cuối) và sử dụng nó. |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
Giả sử bạn cài đặt Python vào thư mục `C:\opt\custompython\bin`. |
|||
|
|||
Nếu bạn chọn cập nhật biến môi trường `PATH`, thì cài đặt sẽ thêm `C:\opt\custompython\bin` vào biến môi trường `PATH`. |
|||
|
|||
Nó có thể có dạng như sau: |
|||
|
|||
```plaintext |
|||
C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin |
|||
``` |
|||
|
|||
Như vậy, khi bạn gõ `python` trong terminal, hệ thống sẽ tìm thấy chương trình Python trong `C:\opt\custompython\bin` (thư mục cuối) và sử dụng nó. |
|||
|
|||
//// |
|||
|
|||
Vậy, nếu bạn gõ: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
Hệ thống sẽ **tìm kiếm** chương trình `python` trong `/opt/custompython/bin` và thực thi nó. |
|||
|
|||
Nó tương đương với việc bạn gõ: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ /opt/custompython/bin/python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
Hệ thống sẽ **tìm kiếm** chương trình `python` trong `C:\opt\custompython\bin\python` và thực thi nó. |
|||
|
|||
Nó tương đương với việc bạn gõ: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ C:\opt\custompython\bin\python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
Thông tin này sẽ hữu ích khi bạn học về [Môi trường ảo](virtual-environments.md){.internal-link target=_blank}. |
|||
|
|||
## Kết luận |
|||
|
|||
Với những thông tin này, bạn có thể hiểu được **các biến môi trường là gì** và **cách sử dụng chúng trong Python**. |
|||
|
|||
Bạn có thể đọc thêm về chúng tại <a href="https://en.wikipedia.org/wiki/Environment_variable" class="external-link" target="_blank">Wikipedia cho Biến môi trường</a>. |
|||
|
|||
Trong nhiều trường hợp, cách các biến môi trường trở nên hữu ích và có thể áp dụng không thực sự rõ ràng ngay từ đầu, nhưng chúng sẽ liên tục xuất hiện trong rất nhiều tình huống khi bạn phát triển ứng dụng, vì vậy việc hiểu biết về chúng là hữu ích. |
|||
|
|||
Chẳng hạn, bạn sẽ cần những thông tin này khi bạn học về [Môi trường ảo](virtual-environments.md). |
@ -0,0 +1,75 @@ |
|||
# FastAPI CLI |
|||
|
|||
**FastAPI CLI** là một chương trình dòng lệnh có thể được sử dụng để phục vụ ứng dụng FastAPI của bạn, quản lý dự án FastAPI của bạn và nhiều hoạt động khác. |
|||
|
|||
Khi bạn cài đặt FastAPI (vd với `pip install "fastapi[standard]"`), nó sẽ bao gồm một gói được gọi là `fastapi-cli`, gói này cung cấp lệnh `fastapi` trong terminal. |
|||
|
|||
Để chạy ứng dụng FastAPI của bạn cho quá trình phát triển (development), bạn có thể sử dụng lệnh `fastapi dev`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:solid">main.py</u> |
|||
|
|||
<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting development server 🚀 |
|||
|
|||
Searching for package file structure from directories with |
|||
<font color="#3465A4">__init__.py</font> files |
|||
Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with the |
|||
following code: |
|||
|
|||
<u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000/docs</u></font> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> tip </font></span> Running in development mode, for production use: |
|||
<b>fastapi run</b> |
|||
|
|||
Logs: |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Will watch for changes in these directories: |
|||
<b>[</b><font color="#4E9A06">'/home/user/code/awesomeapp'</font><b>]</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> <b>(</b>Press CTRL+C to |
|||
quit<b>)</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started reloader process <b>[</b><font color="#34E2E2"><b>383138</b></font><b>]</b> using WatchFiles |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>383153</b></font><b>]</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Chương trình dòng lệnh `fastapi` là **FastAPI CLI**. |
|||
|
|||
FastAPI CLI nhận đường dẫn đến chương trình Python của bạn (vd `main.py`) và tự động phát hiện đối tượng `FastAPI` (thường được gọi là `app`), xác định quá trình nhập đúng, và sau đó chạy nó (serve). |
|||
|
|||
Đối với vận hành thực tế (production), bạn sẽ sử dụng `fastapi run` thay thế. 🚀 |
|||
|
|||
Ở bên trong, **FastAPI CLI** sử dụng <a href="https://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a>, một server ASGI có hiệu suất cao, sẵn sàng cho vận hành thực tế (production). 😎 |
|||
|
|||
## `fastapi dev` |
|||
|
|||
Chạy `fastapi dev` sẽ khởi động quá trình phát triển. |
|||
|
|||
Mặc định, **auto-reload** được bật, tự động tải lại server khi bạn thay đổi code của bạn. Điều này tốn nhiều tài nguyên và có thể kém ổn định hơn khi nó bị tắt. Bạn nên sử dụng nó cho quá trình phát triển. Nó cũng lắng nghe địa chỉ IP `127.0.0.1`, đó là địa chỉ IP của máy tính để tự giao tiếp với chính nó (`localhost`). |
|||
|
|||
## `fastapi run` |
|||
|
|||
Chạy `fastapi run` mặc định sẽ khởi động FastAPI cho quá trình vận hành thực tế. |
|||
|
|||
Mặc định, **auto-reload** bị tắt. Nó cũng lắng nghe địa chỉ IP `0.0.0.0`, đó là tất cả các địa chỉ IP có sẵn, như vậy nó sẽ được truy cập công khai bởi bất kỳ ai có thể giao tiếp với máy tính. Đây là cách bạn thường chạy nó trong sản phẩm hoàn thiện, ví dụ trong một container. |
|||
|
|||
Trong hầu hết các trường hợp, bạn sẽ (và nên) có một "proxy điểm cuối (termination proxy)" xử lý HTTPS cho bạn, điều này sẽ phụ thuộc vào cách bạn triển khai ứng dụng của bạn, nhà cung cấp có thể làm điều này cho bạn, hoặc bạn có thể cần thiết lập nó. |
|||
|
|||
/// tip |
|||
|
|||
Bạn có thể tìm hiểu thêm về FastAPI CLI trong [tài liệu triển khai](deployment/index.md){.internal-link target=_blank}. |
|||
|
|||
/// |
@ -0,0 +1,40 @@ |
|||
# Tệp tĩnh (Static Files) |
|||
|
|||
Bạn có thể triển khai tệp tĩnh tự động từ một thư mục bằng cách sử dụng StaticFiles. |
|||
|
|||
## Sử dụng `Tệp tĩnh` |
|||
|
|||
- Nhập `StaticFiles`. |
|||
- "Mount" a `StaticFiles()` instance in a specific path. |
|||
|
|||
{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} |
|||
|
|||
/// note | Chi tiết kỹ thuật |
|||
|
|||
Bạn cũng có thể sử dụng `from starlette.staticfiles import StaticFiles`. |
|||
|
|||
**FastAPI** cung cấp cùng `starlette.staticfiles` như `fastapi.staticfiles` giúp đơn giản hóa việc sử dụng, nhưng nó thực sự đến từ Starlette. |
|||
|
|||
/// |
|||
|
|||
### "Mounting" là gì |
|||
|
|||
"Mounting" có nghĩa là thêm một ứng dụng "độc lập" hoàn chỉnh vào một đường dẫn cụ thể, sau đó ứng dụng đó sẽ chịu trách nhiệm xử lý tất cả các đường dẫn con. |
|||
|
|||
Điều này khác với việc sử dụng `APIRouter` vì một ứng dụng được gắn kết là hoàn toàn độc lập. OpenAPI và tài liệu từ ứng dụng chính của bạn sẽ không bao gồm bất kỳ thứ gì từ ứng dụng được gắn kết, v.v. |
|||
|
|||
Bạn có thể đọc thêm về điều này trong [Hướng dẫn Người dùng Nâng cao](../advanced/index.md){.internal-link target=\_blank}. |
|||
|
|||
## Chi tiết |
|||
|
|||
Đường dẫn đầu tiên `"/static"` là đường dẫn con mà "ứng dụng con" này sẽ được "gắn" vào. Vì vậy, bất kỳ đường dẫn nào bắt đầu bằng `"/static"` sẽ được xử lý bởi nó. |
|||
|
|||
Đường dẫn `directory="static"` là tên của thư mục chứa tệp tĩnh của bạn. |
|||
|
|||
Tham số `name="static"` đặt tên cho nó để có thể được sử dụng bên trong **FastAPI**. |
|||
|
|||
Tất cả các tham số này có thể khác với `static`, điều chỉnh chúng với phù hợp với ứng dụng của bạn. |
|||
|
|||
## Thông tin thêm |
|||
|
|||
Để biết thêm chi tiết và tùy chọn, hãy xem <a href="https://www.starlette.io/staticfiles/" class="external-link" target="_blank">Starlette's docs about Static Files</a>. |
@ -0,0 +1,842 @@ |
|||
# Môi trường ảo (Virtual Environments) |
|||
|
|||
Khi bạn làm việc trong các dự án Python, bạn có thể sử dụng một **môi trường ảo** (hoặc một cơ chế tương tự) để cách ly các gói bạn cài đặt cho mỗi dự án. |
|||
|
|||
/// info |
|||
Nếu bạn đã biết về các môi trường ảo, cách tạo chúng và sử dụng chúng, bạn có thể bỏ qua phần này. 🤓 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
Một **môi trường ảo** khác với một **biến môi trường (environment variable)**. |
|||
|
|||
Một **biến môi trường** là một biến trong hệ thống có thể được sử dụng bởi các chương trình. |
|||
|
|||
Một **môi trường ảo** là một thư mục với một số tệp trong đó. |
|||
|
|||
/// |
|||
|
|||
/// info |
|||
|
|||
Trang này sẽ hướng dẫn bạn cách sử dụng các **môi trường ảo** và cách chúng hoạt động. |
|||
|
|||
Nếu bạn đã sẵn sàng sử dụng một **công cụ có thể quản lý tất cả mọi thứ** cho bạn (bao gồm cả việc cài đặt Python), hãy thử <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>. |
|||
|
|||
/// |
|||
|
|||
## Tạo một Dự án |
|||
|
|||
Đầu tiên, tạo một thư mục cho dự án của bạn. |
|||
|
|||
Cách tôi thường làm là tạo một thư mục có tên `code` trong thư mục `home/user`. |
|||
|
|||
Và trong thư mục đó, tôi tạo một thư mục cho mỗi dự án. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Đi đến thư mục home |
|||
$ cd |
|||
// Tạo một thư mục cho tất cả các dự án của bạn |
|||
$ mkdir code |
|||
// Vào thư mục code |
|||
$ cd code |
|||
// Tạo một thư mục cho dự án này |
|||
$ mkdir awesome-project |
|||
// Vào thư mục dự án |
|||
$ cd awesome-project |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Tạo một Môi trường ảo |
|||
|
|||
Khi bạn bắt đầu làm việc với một dự án Python **trong lần đầu**, hãy tạo một môi trường ảo **<abbr title="có nhiều cách thực hiện khác nhau, đây là một hướng dẫn đơn giản">trong thư mục dự án của bạn</abbr>**. |
|||
|
|||
/// tip |
|||
|
|||
Bạn cần làm điều này **một lần cho mỗi dự án**, không phải mỗi khi bạn làm việc. |
|||
/// |
|||
|
|||
//// tab | `venv` |
|||
|
|||
Để tạo một môi trường ảo, bạn có thể sử dụng module `venv` có sẵn của Python. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python -m venv .venv |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// details | Cách các lệnh hoạt động |
|||
|
|||
* `python`: sử dụng chương trình `python` |
|||
* `-m`: gọi một module như một script, chúng ta sẽ nói về module đó sau |
|||
* `venv`: sử dụng module `venv` được cài đặt sẵn của Python |
|||
* `.venv`: tạo môi trường ảo trong thư mục mới `.venv` |
|||
|
|||
/// |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
Nếu bạn có <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> được cài đặt, bạn có thể sử dụng nó để tạo một môi trường ảo. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ uv venv |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// tip |
|||
|
|||
Mặc định, `uv` sẽ tạo một môi trường ảo trong một thư mục có tên `.venv`. |
|||
|
|||
Nhưng bạn có thể tùy chỉnh nó bằng cách thêm một đối số với tên thư mục. |
|||
|
|||
/// |
|||
|
|||
//// |
|||
|
|||
Lệnh này tạo một môi trường ảo mới trong một thư mục có tên `.venv`. |
|||
|
|||
/// details | `.venv` hoặc tên khác |
|||
|
|||
Bạn có thể tạo môi trường ảo trong một thư mục khác, nhưng thường người ta quy ước đặt nó là `.venv`. |
|||
|
|||
/// |
|||
|
|||
## Kích hoạt Môi trường ảo |
|||
|
|||
Kích hoạt môi trường ảo mới để bất kỳ lệnh Python nào bạn chạy hoặc gói nào bạn cài đặt sẽ sử dụng nó. |
|||
|
|||
/// tip |
|||
|
|||
Làm điều này **mỗi khi** bạn bắt đầu một **phiên terminal mới** để làm việc trên dự án. |
|||
|
|||
/// |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ source .venv/bin/activate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ .venv\Scripts\Activate.ps1 |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows Bash |
|||
|
|||
Nếu bạn sử dụng Bash cho Windows (ví dụ: <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>): |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ source .venv/Scripts/activate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
/// tip |
|||
|
|||
Mỗi khi bạn cài đặt thêm một **package mới** trong môi trường đó, hãy **kích hoạt** môi trường đó lại. |
|||
|
|||
Điều này đảm bảo rằng khi bạn sử dụng một **chương trình dòng lệnh (<abbr title="command line interface">CLI</abbr>)** được cài đặt từ gói đó, bạn sẽ dùng bản cài đặt từ môi trường ảo của mình thay vì bản được cài đặt toàn cục khác có thể có phiên bản khác với phiên bản bạn cần. |
|||
|
|||
/// |
|||
|
|||
## Kiểm tra xem Môi trường ảo đã được Kích hoạt chưa |
|||
|
|||
Kiểm tra xem môi trường ảo đã được kích hoạt chưa (lệnh trước đó đã hoạt động). |
|||
|
|||
/// tip |
|||
|
|||
Điều này là **không bắt buộc**, nhưng nó là một cách tốt để **kiểm tra** rằng mọi thứ đang hoạt động như mong đợi và bạn đang sử dụng đúng môi trường ảo mà bạn đã định. |
|||
|
|||
/// |
|||
|
|||
//// tab | Linux, macOS, Windows Bash |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ which python |
|||
|
|||
/home/user/code/awesome-project/.venv/bin/python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Nếu nó hiển thị `python` binary tại `.venv/bin/python`, trong dự án của bạn (trong trường hợp `awesome-project`), thì tức là nó hoạt động. 🎉 |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ Get-Command python |
|||
|
|||
C:\Users\user\code\awesome-project\.venv\Scripts\python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Nếu nó hiển thị `python` binary tại `.venv\Scripts\python`, trong dự án của bạn (trong trường hợp `awesome-project`), thì tức là nó hoạt động. 🎉 |
|||
|
|||
//// |
|||
|
|||
## Nâng cấp `pip` |
|||
|
|||
/// tip |
|||
|
|||
Nếu bạn sử dụng <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> bạn sử dụng nó để cài đặt thay vì `pip`, thì bạn không cần cập nhật `pip`. 😎 |
|||
|
|||
/// |
|||
|
|||
Nếu bạn sử dụng `pip` để cài đặt gói (nó được cài đặt mặc định với Python), bạn nên **nâng cấp** nó lên phiên bản mới nhất. |
|||
|
|||
Nhiều lỗi khác nhau trong khi cài đặt gói được giải quyết chỉ bằng cách nâng cấp `pip` trước. |
|||
|
|||
/// tip |
|||
|
|||
Bạn thường làm điều này **một lần**, ngay sau khi bạn tạo môi trường ảo. |
|||
|
|||
/// |
|||
|
|||
Đảm bảo rằng môi trường ảo đã được kích hoạt (với lệnh trên) và sau đó chạy: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python -m pip install --upgrade pip |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Thêm `.gitignore` |
|||
|
|||
Nếu bạn sử dụng **Git** (nên làm), hãy thêm một file `.gitignore` để Git bỏ qua mọi thứ trong `.venv`. |
|||
|
|||
/// tip |
|||
|
|||
Nếu bạn sử dụng <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> để tạo môi trường ảo, nó đã tự động làm điều này cho bạn, bạn có thể bỏ qua bước này. 😎 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
Làm điều này **một lần**, ngay sau khi bạn tạo môi trường ảo. |
|||
|
|||
/// |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ echo "*" > .venv/.gitignore |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// details | Cách lệnh hoạt động |
|||
|
|||
* `echo "*"`: sẽ "in" văn bản `*` trong terminal (phần tiếp theo sẽ thay đổi điều đó một chút) |
|||
* `>`: bất kỳ văn bản nào được in ra terminal bởi lệnh trước `>` không được in ra mà thay vào đó được viết vào file ở phía bên phải của `>` |
|||
* `.gitignore`: tên của file mà văn bản sẽ được viết vào |
|||
|
|||
Và `*` với Git có nghĩa là "mọi thứ". Vì vậy, nó sẽ bỏ qua mọi thứ trong thư mục `.venv`. |
|||
|
|||
Lệnh này sẽ tạo một file `.gitignore` với nội dung: |
|||
|
|||
```gitignore |
|||
* |
|||
``` |
|||
|
|||
/// |
|||
|
|||
## Cài đặt gói (packages) |
|||
|
|||
Sau khi kích hoạt môi trường, bạn có thể cài đặt các gói trong đó. |
|||
|
|||
/// tip |
|||
|
|||
Thực hiện điều này **một lần** khi cài đặt hoặc cập nhật gói cần thiết cho dự án của bạn. |
|||
|
|||
Nếu bạn cần cập nhật phiên bản hoặc thêm một gói mới, bạn sẽ **thực hiện điều này lại**. |
|||
|
|||
/// |
|||
|
|||
### Cài đặt gói trực tiếp |
|||
|
|||
Nếu bạn cần cập nhật phiên bản hoặc thêm một gói mới, bạn sẽ **thực hiện điều này lại**. |
|||
|
|||
/// tip |
|||
Để quản lý dự án tốt hơn, hãy liệt kê tất cả các gói và phiên bản cần thiết trong một file (ví dụ `requirements.txt` hoặc `pyproject.toml`). |
|||
|
|||
/// |
|||
|
|||
//// tab | `pip` |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "fastapi[standard]" |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
Nếu bạn có <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ uv pip install "fastapi[standard]" |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
### Cài đặt từ `requirements.txt` |
|||
|
|||
Nếu bạn có một tệp `requirements.txt`, bạn có thể sử dụng nó để cài đặt các gói. |
|||
|
|||
//// tab | `pip` |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install -r requirements.txt |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
Nếu bạn có <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ uv pip install -r requirements.txt |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
/// details | `requirements.txt` |
|||
|
|||
Một tệp `requirements.txt` với một số gói sẽ trông như thế này: |
|||
|
|||
```requirements.txt |
|||
fastapi[standard]==0.113.0 |
|||
pydantic==2.8.0 |
|||
``` |
|||
|
|||
/// |
|||
|
|||
## Chạy Chương trình của bạn |
|||
|
|||
Sau khi kích hoạt môi trường ảo, bạn có thể chạy chương trình của mình, nó sẽ sử dụng Python trong môi trường ảo của bạn với các gói bạn đã cài đặt. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python main.py |
|||
|
|||
Hello World |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Cấu hình Trình soạn thảo của bạn |
|||
|
|||
Nếu bạn sử dụng một trình soạn thảo, hãy đảm bảo bạn cấu hình nó để sử dụng cùng môi trường ảo mà bạn đã tạo (trình soạn thảo sẽ tự động phát hiện môi trường ảo) để bạn có thể nhận được tính năng tự động hoàn thành câu lệnh (autocomplete) và in lỗi trực tiếp trong trình soạn thảo (inline errors). |
|||
|
|||
Ví dụ: |
|||
|
|||
* <a href="https://code.visualstudio.com/docs/python/environments#_select-and-activate-an-environment" class="external-link" target="_blank">VS Code</a> |
|||
* <a href="https://www.jetbrains.com/help/pycharm/creating-virtual-environment.html" class="external-link" target="_blank">PyCharm</a> |
|||
|
|||
/// tip |
|||
|
|||
Bạn thường chỉ cần làm điều này **một lần**, khi bạn tạo môi trường ảo. |
|||
|
|||
/// |
|||
|
|||
## Huỷ kích hoạt Môi trường ảo |
|||
|
|||
Khi bạn hoàn tất việc làm trên dự án của bạn, bạn có thể **huỷ kích hoạt** môi trường ảo. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ deactivate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Như vậy, khi bạn chạy `python`, nó sẽ không chạy từ môi trường ảo đó với các gói đã cài đặt. |
|||
|
|||
## Sẵn sàng để Làm việc |
|||
|
|||
Bây giờ bạn đã sẵn sàng để làm việc trên dự án của mình rồi đấy. |
|||
|
|||
/// tip |
|||
|
|||
Bạn muốn hiểu tất cả những gì ở trên? |
|||
|
|||
Tiếp tục đọc. 👇🤓 |
|||
|
|||
/// |
|||
|
|||
## Tại sao cần Môi trường ảo |
|||
|
|||
Để làm việc với FastAPI, bạn cần cài đặt <a href="https://www.python.org/" class="external-link" target="_blank">Python</a>. |
|||
|
|||
Sau đó, bạn sẽ cần **cài đặt** FastAPI và bất kỳ **gói** nào mà bạn muốn sử dụng. |
|||
|
|||
Để cài đặt gói, bạn thường sử dụng lệnh `pip` có sẵn với Python (hoặc các phiên bản tương tự). |
|||
|
|||
Tuy nhiên, nếu bạn sử dụng `pip` trực tiếp, các gói sẽ được cài đặt trong **môi trường Python toàn cục** của bạn (phần cài đặt toàn cục của Python). |
|||
|
|||
### Vấn đề |
|||
|
|||
Vậy, vấn đề gì khi cài đặt gói trong môi trường Python toàn cục? |
|||
|
|||
Trong một vài thời điểm, bạn sẽ phải viết nhiều chương trình khác nhau phụ thuộc vào **các gói khác nhau**. Và một số dự án bạn thực hiện lại phụ thuộc vào **các phiên bản khác nhau** của cùng một gói. 😱 |
|||
|
|||
Ví dụ, bạn có thể tạo một dự án được gọi là `philosophers-stone`, chương trình này phụ thuộc vào một gói khác được gọi là **`harry`, sử dụng phiên bản `1`**. Vì vậy, bạn cần cài đặt `harry`. |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
stone(philosophers-stone) -->|phụ thuộc| harry-1[harry v1] |
|||
``` |
|||
|
|||
Sau đó, vào một vài thời điểm sau, bạn tạo một dự án khác được gọi là `prisoner-of-azkaban`, và dự án này cũng phụ thuộc vào `harry`, nhưng dự án này cần **`harry` phiên bản `3`**. |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
azkaban(prisoner-of-azkaban) --> |phụ thuộc| harry-3[harry v3] |
|||
``` |
|||
|
|||
Bây giờ, vấn đề là, nếu bạn cài đặt các gói toàn cục (trong môi trường toàn cục) thay vì trong một **môi trường ảo cục bộ**, bạn sẽ phải chọn phiên bản `harry` nào để cài đặt. |
|||
|
|||
Nếu bạn muốn chạy `philosophers-stone` bạn sẽ cần phải cài đặt `harry` phiên bản `1`, ví dụ với: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "harry==1" |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Và sau đó bạn sẽ có `harry` phiên bản `1` được cài đặt trong môi trường Python toàn cục của bạn. |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
subgraph global[môi trường toàn cục] |
|||
harry-1[harry v1] |
|||
end |
|||
subgraph stone-project[dự án philosophers-stone ] |
|||
stone(philosophers-stone) -->|phụ thuộc| harry-1 |
|||
end |
|||
``` |
|||
|
|||
Nhưng sau đó, nếu bạn muốn chạy `prisoner-of-azkaban`, bạn sẽ cần phải gỡ bỏ `harry` phiên bản `1` và cài đặt `harry` phiên bản `3` (hoặc chỉ cần cài đặt phiên bản `3` sẽ tự động gỡ bỏ phiên bản `1`). |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "harry==3" |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Và sau đó bạn sẽ có `harry` phiên bản `3` được cài đặt trong môi trường Python toàn cục của bạn. |
|||
|
|||
Và nếu bạn cố gắng chạy `philosophers-stone` lại, có khả năng nó sẽ **không hoạt động** vì nó cần `harry` phiên bản `1`. |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
subgraph global[môi trường toàn cục] |
|||
harry-1[<strike>harry v1</strike>] |
|||
style harry-1 fill:#ccc,stroke-dasharray: 5 5 |
|||
harry-3[harry v3] |
|||
end |
|||
subgraph stone-project[dự án philosophers-stone ] |
|||
stone(philosophers-stone) -.-x|⛔️| harry-1 |
|||
end |
|||
subgraph azkaban-project[dự án prisoner-of-azkaban ] |
|||
azkaban(prisoner-of-azkaban) --> |phụ thuộc| harry-3 |
|||
end |
|||
``` |
|||
|
|||
/// tip |
|||
|
|||
Mặc dù các gói Python thường cố gắng **tránh các thay đổi làm hỏng code** trong **phiên bản mới**, nhưng để đảm bảo an toàn, bạn nên chủ động cài đặt phiên bản mới và chạy kiểm thử để xác nhận mọi thứ vẫn hoạt động đúng. |
|||
|
|||
/// |
|||
|
|||
Bây giờ, hãy hình dung về **nhiều** gói khác nhau mà tất cả các dự án của bạn phụ thuộc vào. Rõ ràng rất khó để quản lý. Điều này dẫn tới việc là bạn sẽ có nhiều dự án với **các phiên bản không tương thích** của các gói, và bạn có thể không biết tại sao một số thứ không hoạt động. |
|||
|
|||
Hơn nữa, tuỳ vào hệ điều hành của bạn (vd Linux, Windows, macOS), có thể đã có Python được cài đặt sẵn. Trong trường hợp ấy, một vài gói nhiều khả năng đã được cài đặt trước với các phiên bản **cần thiết cho hệ thống của bạn**. Nếu bạn cài đặt các gói trong môi trường Python toàn cục, bạn có thể sẽ **phá vỡ** một số chương trình đã được cài đặt sẵn cùng hệ thống. |
|||
|
|||
## Nơi các Gói được Cài đặt |
|||
|
|||
Khi bạn cài đặt Python, nó sẽ tạo ra một vài thư mục và tệp trong máy tính của bạn. |
|||
|
|||
Một vài thư mục này là những thư mục chịu trách nhiệm có tất cả các gói bạn cài đặt. |
|||
|
|||
Khi bạn chạy: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Đừng chạy lệnh này ngay, đây chỉ là một ví dụ 🤓 |
|||
$ pip install "fastapi[standard]" |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Lệnh này sẽ tải xuống một tệp nén với mã nguồn FastAPI, thường là từ <a href="https://pypi.org/project/fastapi/" class="external-link" target="_blank">PyPI</a>. |
|||
|
|||
Nó cũng sẽ **tải xuống** các tệp cho các gói khác mà FastAPI phụ thuộc vào. |
|||
|
|||
Sau đó, nó sẽ **giải nén** tất cả các tệp đó và đưa chúng vào một thư mục trong máy tính của bạn. |
|||
|
|||
Mặc định, nó sẽ đưa các tệp đã tải xuống và giải nén vào thư mục được cài đặt cùng Python của bạn, đó là **môi trường toàn cục**. |
|||
|
|||
## Những Môi trường ảo là gì? |
|||
|
|||
Cách giải quyết cho vấn đề có tất cả các gói trong môi trường toàn cục là sử dụng một **môi trường ảo cho mỗi dự án** bạn làm việc. |
|||
|
|||
Một môi trường ảo là một **thư mục**, rất giống với môi trường toàn cục, trong đó bạn có thể cài đặt các gói cho một dự án. |
|||
|
|||
Vì vậy, mỗi dự án sẽ có một môi trường ảo riêng của nó (thư mục `.venv`) với các gói riêng của nó. |
|||
|
|||
```mermaid |
|||
flowchart TB |
|||
subgraph stone-project[dự án philosophers-stone ] |
|||
stone(philosophers-stone) --->|phụ thuộc| harry-1 |
|||
subgraph venv1[.venv] |
|||
harry-1[harry v1] |
|||
end |
|||
end |
|||
subgraph azkaban-project[dự án prisoner-of-azkaban ] |
|||
azkaban(prisoner-of-azkaban) --->|phụ thuộc| harry-3 |
|||
subgraph venv2[.venv] |
|||
harry-3[harry v3] |
|||
end |
|||
end |
|||
stone-project ~~~ azkaban-project |
|||
``` |
|||
|
|||
## Kích hoạt Môi trường ảo nghĩa là gì |
|||
|
|||
Khi bạn kích hoạt một môi trường ảo, ví dụ với: |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ source .venv/bin/activate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ .venv\Scripts\Activate.ps1 |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows Bash |
|||
|
|||
Nếu bạn sử dụng Bash cho Windows (ví dụ <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>): |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ source .venv/Scripts/activate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
Lệnh này sẽ tạo hoặc sửa đổi một số [biến môi trường](environment-variables.md){.internal-link target=_blank} mà sẽ được sử dụng cho các lệnh tiếp theo. |
|||
|
|||
Một trong số đó là biến `PATH`. |
|||
|
|||
/// tip |
|||
|
|||
Bạn có thể tìm hiểu thêm về biến `PATH` trong [Biến môi trường](environment-variables.md#path-environment-variable){.internal-link target=_blank} section. |
|||
|
|||
/// |
|||
|
|||
Kích hoạt môi trường ảo thêm đường dẫn `.venv/bin` (trên Linux và macOS) hoặc `.venv\Scripts` (trên Windows) vào biến `PATH`. |
|||
|
|||
Giả sử rằng trước khi kích hoạt môi trường, biến `PATH` như sau: |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
```plaintext |
|||
/usr/bin:/bin:/usr/sbin:/sbin |
|||
``` |
|||
|
|||
Nghĩa là hệ thống sẽ tìm kiếm chương trình trong: |
|||
|
|||
* `/usr/bin` |
|||
* `/bin` |
|||
* `/usr/sbin` |
|||
* `/sbin` |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
```plaintext |
|||
C:\Windows\System32 |
|||
``` |
|||
|
|||
Nghĩa là hệ thống sẽ tìm kiếm chương trình trong: |
|||
|
|||
* `C:\Windows\System32` |
|||
|
|||
//// |
|||
|
|||
Sau khi kích hoạt môi trường ảo, biến `PATH` sẽ như sau: |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin |
|||
``` |
|||
|
|||
Nghĩa là hệ thống sẽ bắt đầu tìm kiếm chương trình trong: |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin |
|||
``` |
|||
|
|||
trước khi tìm kiếm trong các thư mục khác. |
|||
|
|||
Vì vậy, khi bạn gõ `python` trong terminal, hệ thống sẽ tìm thấy chương trình Python trong: |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin/python |
|||
``` |
|||
|
|||
và sử dụng chương trình đó. |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 |
|||
``` |
|||
|
|||
Nghĩa là hệ thống sẽ bắt đầu tìm kiếm chương trình trong: |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts |
|||
``` |
|||
|
|||
trước khi tìm kiếm trong các thư mục khác. |
|||
|
|||
Vì vậy, khi bạn gõ `python` trong terminal, hệ thống sẽ tìm thấy chương trình Python trong: |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts\python |
|||
``` |
|||
|
|||
và sử dụng chương trình đó. |
|||
|
|||
//// |
|||
|
|||
Một chi tiết quan trọng là nó sẽ đưa địa chỉ của môi trường ảo vào **đầu** của biến `PATH`. Hệ thống sẽ tìm kiếm nó **trước** khi tìm kiếm bất kỳ Python nào khác có sẵn. Vì vậy, khi bạn chạy `python`, nó sẽ sử dụng Python **từ môi trường ảo** thay vì bất kỳ Python nào khác (ví dụ, Python từ môi trường toàn cục). |
|||
|
|||
Kích hoạt một môi trường ảo cũng thay đổi một vài thứ khác, nhưng đây là một trong những điều quan trọng nhất mà nó thực hiện. |
|||
|
|||
## Kiểm tra một Môi trường ảo |
|||
|
|||
Khi bạn kiểm tra một môi trường ảo đã được kích hoạt chưa, ví dụ với: |
|||
|
|||
//// tab | Linux, macOS, Windows Bash |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ which python |
|||
|
|||
/home/user/code/awesome-project/.venv/bin/python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ Get-Command python |
|||
|
|||
C:\Users\user\code\awesome-project\.venv\Scripts\python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
|
|||
Điều đó có nghĩa là chương trình `python` sẽ được sử dụng là chương trình **trong môi trường ảo**. |
|||
|
|||
Bạn sử dụng `which` trên Linux và macOS và `Get-Command` trên Windows PowerShell. |
|||
|
|||
Cách hoạt động của lệnh này là nó sẽ đi và kiểm tra biến `PATH`, đi qua **mỗi đường dẫn theo thứ tự**, tìm kiếm chương trình được gọi là `python`. Khi nó tìm thấy nó, nó sẽ **hiển thị cho bạn đường dẫn** đến chương trình đó. |
|||
|
|||
Điều quan trọng nhất là khi bạn gọi `python`, đó chính là chương trình `python` được thực thi. |
|||
|
|||
Vì vậy, bạn có thể xác nhận nếu bạn đang ở trong môi trường ảo đúng. |
|||
|
|||
/// tip |
|||
|
|||
Dễ dàng kích hoạt một môi trường ảo, cài đặt Python, và sau đó **chuyển đến một dự án khác**. |
|||
|
|||
Và dự án thứ hai **sẽ không hoạt động** vì bạn đang sử dụng **Python không đúng**, từ một môi trường ảo cho một dự án khác. |
|||
|
|||
Thật tiện lợi khi có thể kiểm tra `python` nào đang được sử dụng 🤓 |
|||
|
|||
/// |
|||
|
|||
## Tại sao lại Huỷ kích hoạt một Môi trường ảo |
|||
|
|||
Ví dụ, bạn có thể làm việc trên một dự án `philosophers-stone`, **kích hoạt môi trường ảo**, cài đặt các gói và làm việc với môi trường ảo đó. |
|||
|
|||
Sau đó, bạn muốn làm việc trên **dự án khác** `prisoner-of-azkaban`. |
|||
|
|||
Bạn đi đến dự án đó: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Nếu bạn không tắt môi trường ảo cho `philosophers-stone`, khi bạn chạy `python` trong terminal, nó sẽ cố gắng sử dụng Python từ `philosophers-stone`. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
|
|||
$ python main.py |
|||
|
|||
// Lỗi khi import sirius, nó không được cài đặt 😱 |
|||
Traceback (most recent call last): |
|||
File "main.py", line 1, in <module> |
|||
import sirius |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Nếu bạn huỷ kích hoạt môi trường ảo hiện tại và kích hoạt môi trường ảo mới cho `prisoner-of-azkaban`, khi bạn chạy `python`, nó sẽ sử dụng Python từ môi trường ảo trong `prisoner-of-azkaban`. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
|
|||
// Bạn không cần phải ở trong thư mục trước để huỷ kích hoạt, bạn có thể làm điều đó ở bất kỳ đâu, ngay cả sau khi đi đến dự án khác 😎 |
|||
$ deactivate |
|||
|
|||
// Kích hoạt môi trường ảo trong prisoner-of-azkaban/.venv 🚀 |
|||
$ source .venv/bin/activate |
|||
|
|||
// Bây giờ khi bạn chạy python, nó sẽ tìm thấy gói sirius được cài đặt trong môi trường ảo này ✨ |
|||
$ python main.py |
|||
|
|||
I solemnly swear 🐺 |
|||
|
|||
(Tôi long trọng thề 🐺 - câu này được lấy từ Harry Potter, chú thích của người dịch) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Các cách làm tương tự |
|||
|
|||
Đây là một hướng dẫn đơn giản để bạn có thể bắt đầu và hiểu cách mọi thứ hoạt động **bên trong**. |
|||
|
|||
Có nhiều **cách khác nhau** để quản lí các môi trường ảo, các gói phụ thuộc (requirements), và các dự án. |
|||
|
|||
Một khi bạn đã sẵn sàng và muốn sử dụng một công cụ để **quản lí cả dự án**, các gói phụ thuộc, các môi trường ảo, v.v. Tôi sẽ khuyên bạn nên thử <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>. |
|||
|
|||
`uv` có thể làm nhiều thứ, chẳng hạn: |
|||
|
|||
* **Cài đặt Python** cho bạn, bao gồm nhiều phiên bản khác nhau |
|||
* Quản lí **các môi trường ảo** cho các dự án của bạn |
|||
* Cài đặt **các gói (packages)** |
|||
* Quản lí **các thành phần phụ thuộc và phiên bản** của các gói cho dự án của bạn |
|||
* Đảm bảo rằng bạn có một **tập hợp chính xác** các gói và phiên bản để cài đặt, bao gồm các thành phần phụ thuộc của chúng, để bạn có thể đảm bảo rằng bạn có thể chạy dự án của bạn trong sản xuất chính xác như trong máy tính của bạn trong khi phát triển, điều này được gọi là **locking** |
|||
* Và còn nhiều thứ khác nữa |
|||
|
|||
## Kết luận |
|||
|
|||
Nếu bạn đã đọc và hiểu hết những điều này, khá chắc là bây giờ bạn đã **biết nhiều hơn** về môi trường ảo so với kha khá lập trình viên khác đấy. 🤓 |
|||
|
|||
Những hiểu biết chi tiết này có thể sẽ hữu ích với bạn trong tương lai khi mà bạn cần gỡ lỗi một vài thứ phức tạp, và bạn đã có những hiểu biết về **ngọn ngành gốc rễ cách nó hoạt động**. 😎 |
@ -1,11 +0,0 @@ |
|||
from fastapi import FastAPI, Query |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(q: str = Query(default=..., min_length=3)): |
|||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
if q: |
|||
results.update({"q": q}) |
|||
return results |
@ -1,12 +0,0 @@ |
|||
from fastapi import FastAPI, Query |
|||
from typing_extensions import Annotated |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(q: Annotated[str, Query(min_length=3)] = ...): |
|||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
if q: |
|||
results.update({"q": q}) |
|||
return results |
@ -1,13 +0,0 @@ |
|||
from typing import Annotated |
|||
|
|||
from fastapi import FastAPI, Query |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(q: Annotated[str, Query(min_length=3)] = ...): |
|||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
if q: |
|||
results.update({"q": q}) |
|||
return results |
@ -1,11 +0,0 @@ |
|||
from fastapi import FastAPI, Query |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(q: str = Query(default=..., min_length=3)): |
|||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
if q: |
|||
results.update({"q": q}) |
|||
return results |
@ -1,12 +0,0 @@ |
|||
from fastapi import FastAPI, Query |
|||
from typing_extensions import Annotated |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(q: Annotated[str, Query(min_length=3)] = ...): |
|||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
if q: |
|||
results.update({"q": q}) |
|||
return results |
@ -1,13 +0,0 @@ |
|||
from typing import Annotated |
|||
|
|||
from fastapi import FastAPI, Query |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(q: Annotated[str, Query(min_length=3)] = ...): |
|||
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
if q: |
|||
results.update({"q": q}) |
|||
return results |
@ -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() |
@ -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,168 +0,0 @@ |
|||
import pytest |
|||
from dirty_equals import IsDict |
|||
from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE |
|||
from fastapi.testclient import TestClient |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.query_params_str_validations.tutorial010_an import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
def test_query_params_str_validations_no_query(client: TestClient): |
|||
response = client.get("/items/") |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
|
|||
|
|||
def test_query_params_str_validations_item_query_fixedquery(client: TestClient): |
|||
response = client.get("/items/", params={"item-query": "fixedquery"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == { |
|||
"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], |
|||
"q": "fixedquery", |
|||
} |
|||
|
|||
|
|||
def test_query_params_str_validations_q_fixedquery(client: TestClient): |
|||
response = client.get("/items/", params={"q": "fixedquery"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
|
|||
|
|||
def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): |
|||
response = client.get("/items/", params={"item-query": "nonregexquery"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "string_pattern_mismatch", |
|||
"loc": ["query", "item-query"], |
|||
"msg": "String should match pattern '^fixedquery$'", |
|||
"input": "nonregexquery", |
|||
"ctx": {"pattern": "^fixedquery$"}, |
|||
} |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"ctx": {"pattern": "^fixedquery$"}, |
|||
"loc": ["query", "item-query"], |
|||
"msg": 'string does not match regex "^fixedquery$"', |
|||
"type": "value_error.str.regex", |
|||
} |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Read Items", |
|||
"operationId": "read_items_items__get", |
|||
"parameters": [ |
|||
{ |
|||
"description": "Query string for the items to search in the database that have a good match", |
|||
"required": False, |
|||
"deprecated": True, |
|||
"schema": IsDict( |
|||
{ |
|||
"anyOf": [ |
|||
{ |
|||
"type": "string", |
|||
"minLength": 3, |
|||
"maxLength": 50, |
|||
"pattern": "^fixedquery$", |
|||
}, |
|||
{"type": "null"}, |
|||
], |
|||
"title": "Query string", |
|||
"description": "Query string for the items to search in the database that have a good match", |
|||
# See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34. |
|||
**( |
|||
{"deprecated": True} |
|||
if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) |
|||
else {} |
|||
), |
|||
} |
|||
) |
|||
| IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"title": "Query string", |
|||
"maxLength": 50, |
|||
"minLength": 3, |
|||
"pattern": "^fixedquery$", |
|||
"type": "string", |
|||
"description": "Query string for the items to search in the database that have a good match", |
|||
} |
|||
), |
|||
"name": "item-query", |
|||
"in": "query", |
|||
} |
|||
], |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -1,175 +0,0 @@ |
|||
import pytest |
|||
from dirty_equals import IsDict |
|||
from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from ...utils import needs_py310 |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.query_params_str_validations.tutorial010_an_py310 import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_query_params_str_validations_no_query(client: TestClient): |
|||
response = client.get("/items/") |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_query_params_str_validations_item_query_fixedquery(client: TestClient): |
|||
response = client.get("/items/", params={"item-query": "fixedquery"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == { |
|||
"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], |
|||
"q": "fixedquery", |
|||
} |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_query_params_str_validations_q_fixedquery(client: TestClient): |
|||
response = client.get("/items/", params={"q": "fixedquery"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): |
|||
response = client.get("/items/", params={"item-query": "nonregexquery"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "string_pattern_mismatch", |
|||
"loc": ["query", "item-query"], |
|||
"msg": "String should match pattern '^fixedquery$'", |
|||
"input": "nonregexquery", |
|||
"ctx": {"pattern": "^fixedquery$"}, |
|||
} |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"ctx": {"pattern": "^fixedquery$"}, |
|||
"loc": ["query", "item-query"], |
|||
"msg": 'string does not match regex "^fixedquery$"', |
|||
"type": "value_error.str.regex", |
|||
} |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Read Items", |
|||
"operationId": "read_items_items__get", |
|||
"parameters": [ |
|||
{ |
|||
"description": "Query string for the items to search in the database that have a good match", |
|||
"required": False, |
|||
"deprecated": True, |
|||
"schema": IsDict( |
|||
{ |
|||
"anyOf": [ |
|||
{ |
|||
"type": "string", |
|||
"minLength": 3, |
|||
"maxLength": 50, |
|||
"pattern": "^fixedquery$", |
|||
}, |
|||
{"type": "null"}, |
|||
], |
|||
"title": "Query string", |
|||
"description": "Query string for the items to search in the database that have a good match", |
|||
# See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34. |
|||
**( |
|||
{"deprecated": True} |
|||
if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) |
|||
else {} |
|||
), |
|||
} |
|||
) |
|||
| IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"title": "Query string", |
|||
"maxLength": 50, |
|||
"minLength": 3, |
|||
"pattern": "^fixedquery$", |
|||
"type": "string", |
|||
"description": "Query string for the items to search in the database that have a good match", |
|||
} |
|||
), |
|||
"name": "item-query", |
|||
"in": "query", |
|||
} |
|||
], |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -1,175 +0,0 @@ |
|||
import pytest |
|||
from dirty_equals import IsDict |
|||
from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from ...utils import needs_py39 |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.query_params_str_validations.tutorial010_an_py39 import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
@needs_py39 |
|||
def test_query_params_str_validations_no_query(client: TestClient): |
|||
response = client.get("/items/") |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
|
|||
|
|||
@needs_py39 |
|||
def test_query_params_str_validations_item_query_fixedquery(client: TestClient): |
|||
response = client.get("/items/", params={"item-query": "fixedquery"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == { |
|||
"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], |
|||
"q": "fixedquery", |
|||
} |
|||
|
|||
|
|||
@needs_py39 |
|||
def test_query_params_str_validations_q_fixedquery(client: TestClient): |
|||
response = client.get("/items/", params={"q": "fixedquery"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
|
|||
|
|||
@needs_py39 |
|||
def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): |
|||
response = client.get("/items/", params={"item-query": "nonregexquery"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "string_pattern_mismatch", |
|||
"loc": ["query", "item-query"], |
|||
"msg": "String should match pattern '^fixedquery$'", |
|||
"input": "nonregexquery", |
|||
"ctx": {"pattern": "^fixedquery$"}, |
|||
} |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"ctx": {"pattern": "^fixedquery$"}, |
|||
"loc": ["query", "item-query"], |
|||
"msg": 'string does not match regex "^fixedquery$"', |
|||
"type": "value_error.str.regex", |
|||
} |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
@needs_py39 |
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Read Items", |
|||
"operationId": "read_items_items__get", |
|||
"parameters": [ |
|||
{ |
|||
"description": "Query string for the items to search in the database that have a good match", |
|||
"required": False, |
|||
"deprecated": True, |
|||
"schema": IsDict( |
|||
{ |
|||
"anyOf": [ |
|||
{ |
|||
"type": "string", |
|||
"minLength": 3, |
|||
"maxLength": 50, |
|||
"pattern": "^fixedquery$", |
|||
}, |
|||
{"type": "null"}, |
|||
], |
|||
"title": "Query string", |
|||
"description": "Query string for the items to search in the database that have a good match", |
|||
# See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34. |
|||
**( |
|||
{"deprecated": True} |
|||
if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) |
|||
else {} |
|||
), |
|||
} |
|||
) |
|||
| IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"title": "Query string", |
|||
"maxLength": 50, |
|||
"minLength": 3, |
|||
"pattern": "^fixedquery$", |
|||
"type": "string", |
|||
"description": "Query string for the items to search in the database that have a good match", |
|||
} |
|||
), |
|||
"name": "item-query", |
|||
"in": "query", |
|||
} |
|||
], |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -1,175 +0,0 @@ |
|||
import pytest |
|||
from dirty_equals import IsDict |
|||
from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from ...utils import needs_py310 |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.query_params_str_validations.tutorial010_py310 import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_query_params_str_validations_no_query(client: TestClient): |
|||
response = client.get("/items/") |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_query_params_str_validations_item_query_fixedquery(client: TestClient): |
|||
response = client.get("/items/", params={"item-query": "fixedquery"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == { |
|||
"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], |
|||
"q": "fixedquery", |
|||
} |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_query_params_str_validations_q_fixedquery(client: TestClient): |
|||
response = client.get("/items/", params={"q": "fixedquery"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): |
|||
response = client.get("/items/", params={"item-query": "nonregexquery"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "string_pattern_mismatch", |
|||
"loc": ["query", "item-query"], |
|||
"msg": "String should match pattern '^fixedquery$'", |
|||
"input": "nonregexquery", |
|||
"ctx": {"pattern": "^fixedquery$"}, |
|||
} |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"ctx": {"pattern": "^fixedquery$"}, |
|||
"loc": ["query", "item-query"], |
|||
"msg": 'string does not match regex "^fixedquery$"', |
|||
"type": "value_error.str.regex", |
|||
} |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Read Items", |
|||
"operationId": "read_items_items__get", |
|||
"parameters": [ |
|||
{ |
|||
"description": "Query string for the items to search in the database that have a good match", |
|||
"required": False, |
|||
"deprecated": True, |
|||
"schema": IsDict( |
|||
{ |
|||
"anyOf": [ |
|||
{ |
|||
"type": "string", |
|||
"minLength": 3, |
|||
"maxLength": 50, |
|||
"pattern": "^fixedquery$", |
|||
}, |
|||
{"type": "null"}, |
|||
], |
|||
"title": "Query string", |
|||
"description": "Query string for the items to search in the database that have a good match", |
|||
# See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34. |
|||
**( |
|||
{"deprecated": True} |
|||
if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) |
|||
else {} |
|||
), |
|||
} |
|||
) |
|||
| IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"title": "Query string", |
|||
"maxLength": 50, |
|||
"minLength": 3, |
|||
"pattern": "^fixedquery$", |
|||
"type": "string", |
|||
"description": "Query string for the items to search in the database that have a good match", |
|||
} |
|||
), |
|||
"name": "item-query", |
|||
"in": "query", |
|||
} |
|||
], |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -1,108 +0,0 @@ |
|||
from dirty_equals import IsDict |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from docs_src.query_params_str_validations.tutorial011_an import app |
|||
|
|||
client = TestClient(app) |
|||
|
|||
|
|||
def test_multi_query_values(): |
|||
url = "/items/?q=foo&q=bar" |
|||
response = client.get(url) |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == {"q": ["foo", "bar"]} |
|||
|
|||
|
|||
def test_query_no_values(): |
|||
url = "/items/" |
|||
response = client.get(url) |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == {"q": None} |
|||
|
|||
|
|||
def test_openapi_schema(): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Read Items", |
|||
"operationId": "read_items_items__get", |
|||
"parameters": [ |
|||
{ |
|||
"required": False, |
|||
"schema": IsDict( |
|||
{ |
|||
"anyOf": [ |
|||
{"type": "array", "items": {"type": "string"}}, |
|||
{"type": "null"}, |
|||
], |
|||
"title": "Q", |
|||
} |
|||
) |
|||
| IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"title": "Q", |
|||
"type": "array", |
|||
"items": {"type": "string"}, |
|||
} |
|||
), |
|||
"name": "q", |
|||
"in": "query", |
|||
} |
|||
], |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -1,118 +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.query_params_str_validations.tutorial011_an_py310 import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_multi_query_values(client: TestClient): |
|||
url = "/items/?q=foo&q=bar" |
|||
response = client.get(url) |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == {"q": ["foo", "bar"]} |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_query_no_values(client: TestClient): |
|||
url = "/items/" |
|||
response = client.get(url) |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == {"q": None} |
|||
|
|||
|
|||
@needs_py310 |
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Read Items", |
|||
"operationId": "read_items_items__get", |
|||
"parameters": [ |
|||
{ |
|||
"required": False, |
|||
"schema": IsDict( |
|||
{ |
|||
"anyOf": [ |
|||
{"type": "array", "items": {"type": "string"}}, |
|||
{"type": "null"}, |
|||
], |
|||
"title": "Q", |
|||
} |
|||
) |
|||
| IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"title": "Q", |
|||
"type": "array", |
|||
"items": {"type": "string"}, |
|||
} |
|||
), |
|||
"name": "q", |
|||
"in": "query", |
|||
} |
|||
], |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue