Compare commits
No commits in common. "master" and "0.36.0" have entirely different histories.
5
.flake8
|
|
@ -1,5 +0,0 @@
|
||||||
[flake8]
|
|
||||||
max-line-length = 88
|
|
||||||
select = C,E,F,W,B,B9
|
|
||||||
ignore = E203, E501, W503
|
|
||||||
exclude = __init__.py
|
|
||||||
1
.github/FUNDING.yml
vendored
|
|
@ -1 +0,0 @@
|
||||||
github: [tiangolo]
|
|
||||||
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[BUG]"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Create a file with '...'
|
||||||
|
2. Add a path operation function with '....'
|
||||||
|
3. Open the browser and call it with a payload of '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Environment:**
|
||||||
|
- OS: [e.g. Linux / Windows / macOS]
|
||||||
|
- FastAPI Version [e.g. 0.3.0], get it with:
|
||||||
|
|
||||||
|
```Python
|
||||||
|
import fastapi
|
||||||
|
print(fastapi.__version__)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Python version, get it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python --version
|
||||||
|
```
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1 +0,0 @@
|
||||||
blank_issues_enabled: false
|
|
||||||
102
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -1,104 +1,20 @@
|
||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest an idea for this project
|
about: Suggest an idea for this project
|
||||||
title: ""
|
title: "[FEATURE]"
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### First check
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I want to be able to [...] but I can't because [...]
|
||||||
|
|
||||||
* [ ] I added a very descriptive title to this issue.
|
**Describe the solution you'd like**
|
||||||
* [ ] I used the GitHub search to find a similar issue and didn't find it.
|
A clear and concise description of what you want to happen.
|
||||||
* [ ] I searched the FastAPI documentation, with the integrated search.
|
|
||||||
* [ ] I already searched in Google "How to X in FastAPI" and didn't find any information.
|
|
||||||
* [ ] I already read and followed all the tutorial in the docs and didn't find an answer.
|
|
||||||
* [ ] I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/samuelcolvin/pydantic).
|
|
||||||
* [ ] I already checked if it is not related to FastAPI but to [Swagger UI](https://github.com/swagger-api/swagger-ui).
|
|
||||||
* [ ] I already checked if it is not related to FastAPI but to [ReDoc](https://github.com/Redocly/redoc).
|
|
||||||
* [ ] After submitting this, I commit to:
|
|
||||||
* Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there.
|
|
||||||
* Or, I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future.
|
|
||||||
* Implement a Pull Request for a confirmed bug.
|
|
||||||
|
|
||||||
<!--
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
I'm asking all this because answering questions and solving problems in GitHub issues consumes a lot of time. I end up not being able to add new features, fix bugs, review Pull Requests, etc. as fast as I wish because I have to spend too much time handling issues.
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
All that, on top of all the incredible help provided by a bunch of community members that give a lot of their time to come here and help others.
|
|
||||||
|
|
||||||
That's a lot of work they are doing, but if more FastAPI users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅).
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
Here's a self-contained [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with my use case:
|
|
||||||
|
|
||||||
<!-- Replace the code below with your own self-contained, minimal, reproducible, example -->
|
|
||||||
|
|
||||||
```Python
|
|
||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
def read_root():
|
|
||||||
return {"Hello": "World"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
<!-- Replace the content below with your own feature request -->
|
|
||||||
|
|
||||||
* Open the browser and call the endpoint `/`.
|
|
||||||
* It returns a JSON with `{"Hello": "World"}`.
|
|
||||||
* I would like it to have an extra parameter to teleport me to the moon and back.
|
|
||||||
|
|
||||||
### The solution you would like
|
|
||||||
|
|
||||||
<!-- Replace this with your own content -->
|
|
||||||
|
|
||||||
I would like it to have a `teleport_to_moon` parameter that defaults to `False`, and can be set to `True` to teleport me:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/", teleport_to_moon=True)
|
|
||||||
def read_root():
|
|
||||||
return {"Hello": "World"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Describe alternatives you've considered
|
|
||||||
|
|
||||||
<!-- Replace this with your own ideas -->
|
|
||||||
|
|
||||||
To wait for Space X moon travel plans to drop down long after they release them. But I would rather teleport.
|
|
||||||
|
|
||||||
### Environment
|
|
||||||
|
|
||||||
* OS: [e.g. Linux / Windows / macOS]:
|
|
||||||
* FastAPI Version [e.g. 0.3.0]:
|
|
||||||
|
|
||||||
To know the FastAPI version use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -c "import fastapi; print(fastapi.__version__)"
|
|
||||||
```
|
|
||||||
|
|
||||||
* Python version:
|
|
||||||
|
|
||||||
To know the Python version use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python --version
|
|
||||||
```
|
|
||||||
|
|
||||||
### Additional context
|
|
||||||
|
|
||||||
<!-- Add any other context or screenshots about the question here. -->
|
|
||||||
|
|
|
||||||
82
.github/ISSUE_TEMPLATE/question.md
vendored
|
|
@ -1,81 +1,17 @@
|
||||||
---
|
---
|
||||||
name: Question or Problem
|
name: Question
|
||||||
about: Ask a question or ask about a problem
|
about: Ask a question
|
||||||
title: ""
|
title: "[QUESTION]"
|
||||||
labels: question
|
labels: question
|
||||||
assignees: ""
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### First check
|
**Description**
|
||||||
|
|
||||||
* [ ] I added a very descriptive title to this issue.
|
How can I [...]?
|
||||||
* [ ] I used the GitHub search to find a similar issue and didn't find it.
|
|
||||||
* [ ] I searched the FastAPI documentation, with the integrated search.
|
|
||||||
* [ ] I already searched in Google "How to X in FastAPI" and didn't find any information.
|
|
||||||
* [ ] I already read and followed all the tutorial in the docs and didn't find an answer.
|
|
||||||
* [ ] I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/samuelcolvin/pydantic).
|
|
||||||
* [ ] I already checked if it is not related to FastAPI but to [Swagger UI](https://github.com/swagger-api/swagger-ui).
|
|
||||||
* [ ] I already checked if it is not related to FastAPI but to [ReDoc](https://github.com/Redocly/redoc).
|
|
||||||
* [ ] After submitting this, I commit to one of:
|
|
||||||
* Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there.
|
|
||||||
* I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future.
|
|
||||||
* Implement a Pull Request for a confirmed bug.
|
|
||||||
|
|
||||||
<!--
|
Is it possible to [...]?
|
||||||
|
|
||||||
I'm asking all this because answering questions and solving problems in GitHub issues consumes a lot of time. I end up not being able to add new features, fix bugs, review Pull Requests, etc. as fast as I wish because I have to spend too much time handling issues.
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
All that, on top of all the incredible help provided by a bunch of community members that give a lot of their time to come here and help others.
|
|
||||||
|
|
||||||
That's a lot of work they are doing, but if more FastAPI users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅).
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
Here's a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with my use case:
|
|
||||||
|
|
||||||
<!-- Replace the code below with your own self-contained, minimal, reproducible, example, if I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you -->
|
|
||||||
|
|
||||||
```Python
|
|
||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
def read_root():
|
|
||||||
return {"Hello": "World"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
<!-- Replace the content below with your own problem, question, or error -->
|
|
||||||
|
|
||||||
* Open the browser and call the endpoint `/`.
|
|
||||||
* It returns a JSON with `{"Hello": "World"}`.
|
|
||||||
* But I expected it to return `{"Hello": "Sara"}`.
|
|
||||||
|
|
||||||
### Environment
|
|
||||||
|
|
||||||
* OS: [e.g. Linux / Windows / macOS]:
|
|
||||||
* FastAPI Version [e.g. 0.3.0]:
|
|
||||||
|
|
||||||
To know the FastAPI version use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -c "import fastapi; print(fastapi.__version__)"
|
|
||||||
```
|
|
||||||
|
|
||||||
* Python version:
|
|
||||||
|
|
||||||
To know the Python version use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python --version
|
|
||||||
```
|
|
||||||
|
|
||||||
### Additional context
|
|
||||||
|
|
||||||
<!-- Add any other context or screenshots about the question here. -->
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
FROM python:3.7
|
|
||||||
|
|
||||||
RUN pip install httpx "pydantic==1.5.1" pygithub
|
|
||||||
|
|
||||||
COPY ./app /app
|
|
||||||
|
|
||||||
CMD ["python", "/app/main.py"]
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
name: Comment Docs Preview in PR
|
|
||||||
description: Comment with the docs URL preview in the PR
|
|
||||||
author: Sebastián Ramírez <tiangolo@gmail.com>
|
|
||||||
inputs:
|
|
||||||
token:
|
|
||||||
description: Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}
|
|
||||||
required: true
|
|
||||||
deploy_url:
|
|
||||||
description: The deployment URL to comment in the PR
|
|
||||||
required: true
|
|
||||||
runs:
|
|
||||||
using: docker
|
|
||||||
image: Dockerfile
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from github import Github
|
|
||||||
from github.PullRequest import PullRequest
|
|
||||||
from pydantic import BaseModel, BaseSettings, SecretStr, ValidationError
|
|
||||||
|
|
||||||
github_api = "https://api.github.com"
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
|
||||||
github_repository: str
|
|
||||||
github_event_path: Path
|
|
||||||
github_event_name: Optional[str] = None
|
|
||||||
input_token: SecretStr
|
|
||||||
input_deploy_url: str
|
|
||||||
|
|
||||||
|
|
||||||
class PartialGithubEventHeadCommit(BaseModel):
|
|
||||||
id: str
|
|
||||||
|
|
||||||
|
|
||||||
class PartialGithubEventWorkflowRun(BaseModel):
|
|
||||||
head_commit: PartialGithubEventHeadCommit
|
|
||||||
|
|
||||||
|
|
||||||
class PartialGithubEvent(BaseModel):
|
|
||||||
workflow_run: PartialGithubEventWorkflowRun
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
settings = Settings()
|
|
||||||
logging.info(f"Using config: {settings.json()}")
|
|
||||||
g = Github(settings.input_token.get_secret_value())
|
|
||||||
repo = g.get_repo(settings.github_repository)
|
|
||||||
try:
|
|
||||||
event = PartialGithubEvent.parse_file(settings.github_event_path)
|
|
||||||
except ValidationError as e:
|
|
||||||
logging.error(f"Error parsing event file: {e.errors()}")
|
|
||||||
sys.exit(0)
|
|
||||||
use_pr: Optional[PullRequest] = None
|
|
||||||
for pr in repo.get_pulls():
|
|
||||||
if pr.head.sha == event.workflow_run.head_commit.id:
|
|
||||||
use_pr = pr
|
|
||||||
break
|
|
||||||
if not use_pr:
|
|
||||||
logging.error(
|
|
||||||
f"No PR found for hash: {event.workflow_run.head_commit.id}"
|
|
||||||
)
|
|
||||||
sys.exit(0)
|
|
||||||
github_headers = {
|
|
||||||
"Authorization": f"token {settings.input_token.get_secret_value()}"
|
|
||||||
}
|
|
||||||
url = f"{github_api}/repos/{settings.github_repository}/issues/{use_pr.number}/comments"
|
|
||||||
logging.info(f"Using comments URL: {url}")
|
|
||||||
response = httpx.post(
|
|
||||||
url,
|
|
||||||
headers=github_headers,
|
|
||||||
json={
|
|
||||||
"body": f"📝 Docs preview for commit {use_pr.head.sha} at: {settings.input_deploy_url}"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if not (200 <= response.status_code <= 300):
|
|
||||||
logging.error(f"Error posting comment: {response.text}")
|
|
||||||
sys.exit(1)
|
|
||||||
logging.info("Finished")
|
|
||||||
7
.github/actions/people/Dockerfile
vendored
|
|
@ -1,7 +0,0 @@
|
||||||
FROM python:3.7
|
|
||||||
|
|
||||||
RUN pip install httpx PyGithub "pydantic==1.5.1" "pyyaml>=5.3.1,<6.0.0"
|
|
||||||
|
|
||||||
COPY ./app /app
|
|
||||||
|
|
||||||
CMD ["python", "/app/main.py"]
|
|
||||||
13
.github/actions/people/action.yml
vendored
|
|
@ -1,13 +0,0 @@
|
||||||
name: "Generate FastAPI People"
|
|
||||||
description: "Generate the data for the FastAPI People page"
|
|
||||||
author: "Sebastián Ramírez <tiangolo@gmail.com>"
|
|
||||||
inputs:
|
|
||||||
token:
|
|
||||||
description: 'User token, to read the GitHub API. Can be passed in using {{ secrets.ACTION_TOKEN }}'
|
|
||||||
required: true
|
|
||||||
standard_token:
|
|
||||||
description: 'Default GitHub Action token, used for the PR. Can be passed in using {{ secrets.GITHUB_TOKEN }}'
|
|
||||||
required: true
|
|
||||||
runs:
|
|
||||||
using: 'docker'
|
|
||||||
image: 'Dockerfile'
|
|
||||||
529
.github/actions/people/app/main.py
vendored
|
|
@ -1,529 +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 Container, DefaultDict, Dict, List, Optional, Set
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
import yaml
|
|
||||||
from github import Github
|
|
||||||
from pydantic import BaseModel, BaseSettings, SecretStr
|
|
||||||
|
|
||||||
github_graphql_url = "https://api.github.com/graphql"
|
|
||||||
|
|
||||||
issues_query = """
|
|
||||||
query Q($after: String) {
|
|
||||||
repository(name: "fastapi", owner: "tiangolo") {
|
|
||||||
issues(first: 100, after: $after) {
|
|
||||||
edges {
|
|
||||||
cursor
|
|
||||||
node {
|
|
||||||
number
|
|
||||||
author {
|
|
||||||
login
|
|
||||||
avatarUrl
|
|
||||||
url
|
|
||||||
}
|
|
||||||
title
|
|
||||||
createdAt
|
|
||||||
state
|
|
||||||
comments(first: 100) {
|
|
||||||
nodes {
|
|
||||||
createdAt
|
|
||||||
author {
|
|
||||||
login
|
|
||||||
avatarUrl
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
prs_query = """
|
|
||||||
query Q($after: String) {
|
|
||||||
repository(name: "fastapi", owner: "tiangolo") {
|
|
||||||
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: "tiangolo") {
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class CommentsNode(BaseModel):
|
|
||||||
createdAt: datetime
|
|
||||||
author: Optional[Author] = None
|
|
||||||
|
|
||||||
|
|
||||||
class Comments(BaseModel):
|
|
||||||
nodes: List[CommentsNode]
|
|
||||||
|
|
||||||
|
|
||||||
class IssuesNode(BaseModel):
|
|
||||||
number: int
|
|
||||||
author: Optional[Author] = None
|
|
||||||
title: str
|
|
||||||
createdAt: datetime
|
|
||||||
state: str
|
|
||||||
comments: Comments
|
|
||||||
|
|
||||||
|
|
||||||
class IssuesEdge(BaseModel):
|
|
||||||
cursor: str
|
|
||||||
node: IssuesNode
|
|
||||||
|
|
||||||
|
|
||||||
class Issues(BaseModel):
|
|
||||||
edges: List[IssuesEdge]
|
|
||||||
|
|
||||||
|
|
||||||
class IssuesRepository(BaseModel):
|
|
||||||
issues: Issues
|
|
||||||
|
|
||||||
|
|
||||||
class IssuesResponseData(BaseModel):
|
|
||||||
repository: IssuesRepository
|
|
||||||
|
|
||||||
|
|
||||||
class IssuesResponse(BaseModel):
|
|
||||||
data: IssuesResponseData
|
|
||||||
|
|
||||||
|
|
||||||
class LabelNode(BaseModel):
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
class Labels(BaseModel):
|
|
||||||
nodes: List[LabelNode]
|
|
||||||
|
|
||||||
|
|
||||||
class ReviewNode(BaseModel):
|
|
||||||
author: Optional[Author] = None
|
|
||||||
state: str
|
|
||||||
|
|
||||||
|
|
||||||
class Reviews(BaseModel):
|
|
||||||
nodes: List[ReviewNode]
|
|
||||||
|
|
||||||
|
|
||||||
class PullRequestNode(BaseModel):
|
|
||||||
number: int
|
|
||||||
labels: Labels
|
|
||||||
author: Optional[Author] = 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
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
input_standard_token: SecretStr
|
|
||||||
github_repository: str
|
|
||||||
|
|
||||||
|
|
||||||
def get_graphql_response(
|
|
||||||
*, settings: Settings, query: str, after: Optional[str] = None
|
|
||||||
):
|
|
||||||
headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"}
|
|
||||||
variables = {"after": after}
|
|
||||||
response = httpx.post(
|
|
||||||
github_graphql_url,
|
|
||||||
headers=headers,
|
|
||||||
json={"query": query, "variables": variables, "operationName": "Q"},
|
|
||||||
)
|
|
||||||
if not response.status_code == 200:
|
|
||||||
logging.error(f"Response was not 200, after: {after}")
|
|
||||||
logging.error(response.text)
|
|
||||||
raise RuntimeError(response.text)
|
|
||||||
data = response.json()
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def get_graphql_issue_edges(*, settings: Settings, after: Optional[str] = None):
|
|
||||||
data = get_graphql_response(settings=settings, query=issues_query, after=after)
|
|
||||||
graphql_response = IssuesResponse.parse_obj(data)
|
|
||||||
return graphql_response.data.repository.issues.edges
|
|
||||||
|
|
||||||
|
|
||||||
def get_graphql_pr_edges(*, settings: Settings, after: Optional[str] = None):
|
|
||||||
data = get_graphql_response(settings=settings, query=prs_query, after=after)
|
|
||||||
graphql_response = PRsResponse.parse_obj(data)
|
|
||||||
return graphql_response.data.repository.pullRequests.edges
|
|
||||||
|
|
||||||
|
|
||||||
def get_graphql_sponsor_edges(*, settings: Settings, after: Optional[str] = None):
|
|
||||||
data = get_graphql_response(settings=settings, query=sponsors_query, after=after)
|
|
||||||
graphql_response = SponsorsResponse.parse_obj(data)
|
|
||||||
return graphql_response.data.user.sponsorshipsAsMaintainer.edges
|
|
||||||
|
|
||||||
|
|
||||||
def get_experts(settings: Settings):
|
|
||||||
issue_nodes: List[IssuesNode] = []
|
|
||||||
issue_edges = get_graphql_issue_edges(settings=settings)
|
|
||||||
|
|
||||||
while issue_edges:
|
|
||||||
for edge in issue_edges:
|
|
||||||
issue_nodes.append(edge.node)
|
|
||||||
last_edge = issue_edges[-1]
|
|
||||||
issue_edges = get_graphql_issue_edges(settings=settings, after=last_edge.cursor)
|
|
||||||
|
|
||||||
commentors = Counter()
|
|
||||||
last_month_commentors = Counter()
|
|
||||||
authors: Dict[str, Author] = {}
|
|
||||||
|
|
||||||
now = datetime.now(tz=timezone.utc)
|
|
||||||
one_month_ago = now - timedelta(days=30)
|
|
||||||
|
|
||||||
for issue in issue_nodes:
|
|
||||||
issue_author_name = None
|
|
||||||
if issue.author:
|
|
||||||
authors[issue.author.login] = issue.author
|
|
||||||
issue_author_name = issue.author.login
|
|
||||||
issue_commentors = set()
|
|
||||||
for comment in issue.comments.nodes:
|
|
||||||
if comment.author:
|
|
||||||
authors[comment.author.login] = comment.author
|
|
||||||
if comment.author.login == issue_author_name:
|
|
||||||
continue
|
|
||||||
issue_commentors.add(comment.author.login)
|
|
||||||
for author_name in issue_commentors:
|
|
||||||
commentors[author_name] += 1
|
|
||||||
if issue.createdAt > one_month_ago:
|
|
||||||
last_month_commentors[author_name] += 1
|
|
||||||
return commentors, last_month_commentors, authors
|
|
||||||
|
|
||||||
|
|
||||||
def get_contributors(settings: Settings):
|
|
||||||
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)
|
|
||||||
|
|
||||||
contributors = Counter()
|
|
||||||
commentors = Counter()
|
|
||||||
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:
|
|
||||||
commentors[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 reviewer in pr_reviewers:
|
|
||||||
reviewers[reviewer] += 1
|
|
||||||
if pr.state == "MERGED" and pr.author:
|
|
||||||
contributors[pr.author.login] += 1
|
|
||||||
return contributors, commentors, reviewers, 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,
|
|
||||||
min_count: int,
|
|
||||||
authors: Dict[str, Author],
|
|
||||||
skip_users: Container[str],
|
|
||||||
):
|
|
||||||
users = []
|
|
||||||
for commentor, count in counter.most_common(50):
|
|
||||||
if commentor in skip_users:
|
|
||||||
continue
|
|
||||||
if count >= min_count:
|
|
||||||
author = authors[commentor]
|
|
||||||
users.append(
|
|
||||||
{
|
|
||||||
"login": commentor,
|
|
||||||
"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.json()}")
|
|
||||||
g = Github(settings.input_standard_token.get_secret_value())
|
|
||||||
repo = g.get_repo(settings.github_repository)
|
|
||||||
issue_commentors, issue_last_month_commentors, issue_authors = get_experts(
|
|
||||||
settings=settings
|
|
||||||
)
|
|
||||||
contributors, pr_commentors, reviewers, pr_authors = get_contributors(
|
|
||||||
settings=settings
|
|
||||||
)
|
|
||||||
authors = {**issue_authors, **pr_authors}
|
|
||||||
maintainers_logins = {"tiangolo"}
|
|
||||||
bot_names = {"codecov", "github-actions"}
|
|
||||||
maintainers = []
|
|
||||||
for login in maintainers_logins:
|
|
||||||
user = authors[login]
|
|
||||||
maintainers.append(
|
|
||||||
{
|
|
||||||
"login": login,
|
|
||||||
"answers": issue_commentors[login],
|
|
||||||
"prs": contributors[login],
|
|
||||||
"avatarUrl": user.avatarUrl,
|
|
||||||
"url": user.url,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
min_count_expert = 10
|
|
||||||
min_count_last_month = 3
|
|
||||||
min_count_contributor = 4
|
|
||||||
min_count_reviewer = 4
|
|
||||||
skip_users = maintainers_logins | bot_names
|
|
||||||
experts = get_top_users(
|
|
||||||
counter=issue_commentors,
|
|
||||||
min_count=min_count_expert,
|
|
||||||
authors=authors,
|
|
||||||
skip_users=skip_users,
|
|
||||||
)
|
|
||||||
last_month_active = get_top_users(
|
|
||||||
counter=issue_last_month_commentors,
|
|
||||||
min_count=min_count_last_month,
|
|
||||||
authors=authors,
|
|
||||||
skip_users=skip_users,
|
|
||||||
)
|
|
||||||
top_contributors = get_top_users(
|
|
||||||
counter=contributors,
|
|
||||||
min_count=min_count_contributor,
|
|
||||||
authors=authors,
|
|
||||||
skip_users=skip_users,
|
|
||||||
)
|
|
||||||
top_reviewers = get_top_users(
|
|
||||||
counter=reviewers,
|
|
||||||
min_count=min_count_reviewer,
|
|
||||||
authors=authors,
|
|
||||||
skip_users=skip_users,
|
|
||||||
)
|
|
||||||
|
|
||||||
tiers = get_individual_sponsors(settings=settings)
|
|
||||||
sponsors_50 = []
|
|
||||||
for login, sponsor in tiers[50].items():
|
|
||||||
sponsors_50.append(
|
|
||||||
{"login": login, "avatarUrl": sponsor.avatarUrl, "url": sponsor.url}
|
|
||||||
)
|
|
||||||
keys = list(tiers.keys())
|
|
||||||
keys.sort(reverse=True)
|
|
||||||
sponsors = []
|
|
||||||
for key in keys:
|
|
||||||
if key >= 50:
|
|
||||||
continue
|
|
||||||
for login, sponsor in tiers[key].items():
|
|
||||||
sponsors.append(
|
|
||||||
{"login": login, "avatarUrl": sponsor.avatarUrl, "url": sponsor.url}
|
|
||||||
)
|
|
||||||
|
|
||||||
people = {
|
|
||||||
"maintainers": maintainers,
|
|
||||||
"experts": experts,
|
|
||||||
"last_month_active": last_month_active,
|
|
||||||
"top_contributors": top_contributors,
|
|
||||||
"top_reviewers": top_reviewers,
|
|
||||||
"sponsors_50": sponsors_50,
|
|
||||||
"sponsors": sponsors,
|
|
||||||
}
|
|
||||||
people_path = Path("./docs/en/data/people.yml")
|
|
||||||
people_old_content = people_path.read_text(encoding="utf-8")
|
|
||||||
new_content = yaml.dump(people, sort_keys=False, width=200, allow_unicode=True)
|
|
||||||
if people_old_content == new_content:
|
|
||||||
logging.info("The FastAPI People data hasn't changed, finishing.")
|
|
||||||
sys.exit(0)
|
|
||||||
people_path.write_text(new_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", "github-actions@github.com"], 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)], 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")
|
|
||||||
7
.github/actions/watch-previews/Dockerfile
vendored
|
|
@ -1,7 +0,0 @@
|
||||||
FROM python:3.7
|
|
||||||
|
|
||||||
RUN pip install httpx PyGithub "pydantic==1.5.1"
|
|
||||||
|
|
||||||
COPY ./app /app
|
|
||||||
|
|
||||||
CMD ["python", "/app/main.py"]
|
|
||||||
10
.github/actions/watch-previews/action.yml
vendored
|
|
@ -1,10 +0,0 @@
|
||||||
name: "Watch docs previews in PRs"
|
|
||||||
description: "Check PRs and trigger new docs deploys"
|
|
||||||
author: "Sebastián Ramírez <tiangolo@gmail.com>"
|
|
||||||
inputs:
|
|
||||||
token:
|
|
||||||
description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}'
|
|
||||||
required: true
|
|
||||||
runs:
|
|
||||||
using: 'docker'
|
|
||||||
image: 'Dockerfile'
|
|
||||||
101
.github/actions/watch-previews/app/main.py
vendored
|
|
@ -1,101 +0,0 @@
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from github import Github
|
|
||||||
from github.NamedUser import NamedUser
|
|
||||||
from pydantic import BaseModel, BaseSettings, SecretStr
|
|
||||||
|
|
||||||
github_api = "https://api.github.com"
|
|
||||||
netlify_api = "https://api.netlify.com"
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
|
||||||
input_token: SecretStr
|
|
||||||
github_repository: str
|
|
||||||
github_event_path: Path
|
|
||||||
github_event_name: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class Artifact(BaseModel):
|
|
||||||
id: int
|
|
||||||
node_id: str
|
|
||||||
name: str
|
|
||||||
size_in_bytes: int
|
|
||||||
url: str
|
|
||||||
archive_download_url: str
|
|
||||||
expired: bool
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
|
|
||||||
class ArtifactResponse(BaseModel):
|
|
||||||
total_count: int
|
|
||||||
artifacts: List[Artifact]
|
|
||||||
|
|
||||||
|
|
||||||
def get_message(commit: str) -> str:
|
|
||||||
return f"Docs preview for commit {commit} at"
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
settings = Settings()
|
|
||||||
logging.info(f"Using config: {settings.json()}")
|
|
||||||
g = Github(settings.input_token.get_secret_value())
|
|
||||||
repo = g.get_repo(settings.github_repository)
|
|
||||||
owner: NamedUser = repo.owner
|
|
||||||
headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"}
|
|
||||||
prs = list(repo.get_pulls(state="open"))
|
|
||||||
response = httpx.get(
|
|
||||||
f"{github_api}/repos/{settings.github_repository}/actions/artifacts",
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
data = response.json()
|
|
||||||
artifacts_response = ArtifactResponse.parse_obj(data)
|
|
||||||
for pr in prs:
|
|
||||||
logging.info("-----")
|
|
||||||
logging.info(f"Processing PR #{pr.number}: {pr.title}")
|
|
||||||
pr_comments = list(pr.get_issue_comments())
|
|
||||||
pr_commits = list(pr.get_commits())
|
|
||||||
last_commit = pr_commits[0]
|
|
||||||
for pr_commit in pr_commits:
|
|
||||||
if pr_commit.commit.author.date > last_commit.commit.author.date:
|
|
||||||
last_commit = pr_commit
|
|
||||||
commit = last_commit.commit.sha
|
|
||||||
logging.info(f"Last commit: {commit}")
|
|
||||||
message = get_message(commit)
|
|
||||||
notified = False
|
|
||||||
for pr_comment in pr_comments:
|
|
||||||
if message in pr_comment.body:
|
|
||||||
notified = True
|
|
||||||
logging.info(f"Docs preview was notified: {notified}")
|
|
||||||
if not notified:
|
|
||||||
artifact_name = f"docs-zip-{commit}"
|
|
||||||
use_artifact: Optional[Artifact] = None
|
|
||||||
for artifact in artifacts_response.artifacts:
|
|
||||||
if artifact.name == artifact_name:
|
|
||||||
use_artifact = artifact
|
|
||||||
break
|
|
||||||
if not use_artifact:
|
|
||||||
logging.info("Artifact not available")
|
|
||||||
else:
|
|
||||||
logging.info(f"Existing artifact: {use_artifact.name}")
|
|
||||||
response = httpx.post(
|
|
||||||
"https://api.github.com/repos/tiangolo/fastapi/actions/workflows/preview-docs.yml/dispatches",
|
|
||||||
headers=headers,
|
|
||||||
json={
|
|
||||||
"ref": "master",
|
|
||||||
"inputs": {
|
|
||||||
"pr": f"{pr.number}",
|
|
||||||
"name": artifact_name,
|
|
||||||
"commit": commit,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
logging.info(
|
|
||||||
f"Trigger sent, response status: {response.status_code} - content: {response.content}"
|
|
||||||
)
|
|
||||||
logging.info("Finished")
|
|
||||||
50
.github/workflows/build-docs.yml
vendored
|
|
@ -1,50 +0,0 @@
|
||||||
name: Build Docs
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize]
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-18.04
|
|
||||||
steps:
|
|
||||||
- name: Dump GitHub context
|
|
||||||
env:
|
|
||||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
|
||||||
run: echo "$GITHUB_CONTEXT"
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: "3.7"
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: ${{ env.pythonLocation }}
|
|
||||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-docs
|
|
||||||
- name: Install Flit
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: python3.7 -m pip install flit
|
|
||||||
- name: Install docs extras
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: python3.7 -m flit install --deps production --extras doc
|
|
||||||
- name: Install Material for MkDocs Insiders
|
|
||||||
if: github.event.pull_request.head.repo.fork == false && steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git
|
|
||||||
- name: Build Docs
|
|
||||||
run: python3.7 ./scripts/docs.py build-all
|
|
||||||
- name: Zip docs
|
|
||||||
run: bash ./scripts/zip-docs.sh
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: docs-zip
|
|
||||||
path: ./docs.zip
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
uses: nwtgck/actions-netlify@v1.1.5
|
|
||||||
with:
|
|
||||||
publish-dir: './site'
|
|
||||||
production-branch: master
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
enable-commit-comment: false
|
|
||||||
env:
|
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
|
||||||
30
.github/workflows/issue-manager.yml
vendored
|
|
@ -1,30 +0,0 @@
|
||||||
name: Issue Manager
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
issue_comment:
|
|
||||||
types:
|
|
||||||
- created
|
|
||||||
issues:
|
|
||||||
types:
|
|
||||||
- labeled
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- labeled
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
issue-manager:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: tiangolo/issue-manager@0.4.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
config: >
|
|
||||||
{
|
|
||||||
"answered": {
|
|
||||||
"delay": 864000,
|
|
||||||
"message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
.github/workflows/label-approved.yml
vendored
|
|
@ -1,13 +0,0 @@
|
||||||
name: Label Approved
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 12 * * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
label-approved:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: docker://tiangolo/label-approved:0.0.2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
25
.github/workflows/latest-changes.yml
vendored
|
|
@ -1,25 +0,0 @@
|
||||||
name: Latest Changes
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
types:
|
|
||||||
- closed
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
number:
|
|
||||||
description: PR number
|
|
||||||
required: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
latest-changes:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: docker://tiangolo/latest-changes:0.0.3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
latest_changes_file: docs/en/docs/release-notes.md
|
|
||||||
latest_changes_header: '## Latest Changes\n\n'
|
|
||||||
debug_logs: true
|
|
||||||
29
.github/workflows/people.yml
vendored
|
|
@ -1,29 +0,0 @@
|
||||||
name: FastAPI People
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 14 1 * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
debug_enabled:
|
|
||||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
fastapi-people:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
# Allow debugging with tmate
|
|
||||||
- name: Setup tmate session
|
|
||||||
uses: mxschmitt/action-tmate@v3
|
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
|
|
||||||
with:
|
|
||||||
limit-access-to-actor: true
|
|
||||||
token: ${{ secrets.ACTIONS_TOKEN }}
|
|
||||||
standard_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- uses: ./.github/actions/people
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.ACTIONS_TOKEN }}
|
|
||||||
standard_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
41
.github/workflows/preview-docs.yml
vendored
|
|
@ -1,41 +0,0 @@
|
||||||
name: Preview Docs
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- Build Docs
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Download Artifact Docs
|
|
||||||
uses: dawidd6/action-download-artifact@v2.9.0
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
workflow: build-docs.yml
|
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
|
||||||
name: docs-zip
|
|
||||||
- name: Unzip docs
|
|
||||||
run: |
|
|
||||||
rm -rf ./site
|
|
||||||
unzip docs.zip
|
|
||||||
rm -f docs.zip
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
id: netlify
|
|
||||||
uses: nwtgck/actions-netlify@v1.1.5
|
|
||||||
with:
|
|
||||||
publish-dir: './site'
|
|
||||||
production-deploy: false
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
enable-commit-comment: false
|
|
||||||
env:
|
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
|
||||||
- name: Comment Deploy
|
|
||||||
uses: ./.github/actions/comment-docs-preview-in-pr
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
deploy_url: "${{ steps.netlify.outputs.deploy-url }}"
|
|
||||||
46
.github/workflows/publish.yml
vendored
|
|
@ -1,46 +0,0 @@
|
||||||
name: Publish
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types:
|
|
||||||
- created
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Dump GitHub context
|
|
||||||
env:
|
|
||||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
|
||||||
run: echo "$GITHUB_CONTEXT"
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: "3.6"
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: ${{ env.pythonLocation }}
|
|
||||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-publish
|
|
||||||
- name: Install Flit
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: pip install flit
|
|
||||||
- name: Install Dependencies
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: flit install --symlink
|
|
||||||
- name: Publish
|
|
||||||
env:
|
|
||||||
FLIT_USERNAME: ${{ secrets.FLIT_USERNAME }}
|
|
||||||
FLIT_PASSWORD: ${{ secrets.FLIT_PASSWORD }}
|
|
||||||
run: bash scripts/publish.sh
|
|
||||||
- name: Dump GitHub context
|
|
||||||
env:
|
|
||||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
|
||||||
run: echo "$GITHUB_CONTEXT"
|
|
||||||
# - name: Notify
|
|
||||||
# env:
|
|
||||||
# GITTER_TOKEN: ${{ secrets.GITTER_TOKEN }}
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# TAG: ${{ github.event.release.name }}
|
|
||||||
# run: bash scripts/notify.sh
|
|
||||||
36
.github/workflows/test.yml
vendored
|
|
@ -1,36 +0,0 @@
|
||||||
name: Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: [3.6, 3.7, 3.8]
|
|
||||||
fail-fast: false
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
id: cache
|
|
||||||
with:
|
|
||||||
path: ${{ env.pythonLocation }}
|
|
||||||
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test
|
|
||||||
- name: Install Flit
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: pip install flit
|
|
||||||
- name: Install Dependencies
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: flit install --symlink
|
|
||||||
- name: Test
|
|
||||||
run: bash scripts/test.sh
|
|
||||||
- name: Upload coverage
|
|
||||||
uses: codecov/codecov-action@v1
|
|
||||||
10
.gitignore
vendored
|
|
@ -13,13 +13,3 @@ coverage.xml
|
||||||
test.db
|
test.db
|
||||||
log.txt
|
log.txt
|
||||||
Pipfile.lock
|
Pipfile.lock
|
||||||
env3.*
|
|
||||||
env
|
|
||||||
docs_build
|
|
||||||
venv
|
|
||||||
docs.zip
|
|
||||||
archive.zip
|
|
||||||
|
|
||||||
# vim temporary files
|
|
||||||
*~
|
|
||||||
.*.sw?
|
|
||||||
|
|
|
||||||
26
.travis.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
dist: xenial
|
||||||
|
|
||||||
|
language: python
|
||||||
|
|
||||||
|
cache: pip
|
||||||
|
|
||||||
|
python:
|
||||||
|
- "3.6"
|
||||||
|
- "3.7"
|
||||||
|
|
||||||
|
install:
|
||||||
|
- pip install flit
|
||||||
|
- flit install --symlink
|
||||||
|
|
||||||
|
script:
|
||||||
|
- bash scripts/test.sh
|
||||||
|
|
||||||
|
after_script:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
provider: script
|
||||||
|
script: bash scripts/deploy.sh
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
python: "3.6"
|
||||||
37
Pipfile
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
[[source]]
|
||||||
|
name = "pypi"
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
mypy = "*"
|
||||||
|
black = "*"
|
||||||
|
jupyter = "*"
|
||||||
|
better-exceptions = "*"
|
||||||
|
pytest = "*"
|
||||||
|
pytest-cov = "*"
|
||||||
|
isort = "*"
|
||||||
|
requests = "*"
|
||||||
|
flit = "*"
|
||||||
|
mkdocs = "*"
|
||||||
|
mkdocs-material = "*"
|
||||||
|
markdown-include = "*"
|
||||||
|
autoflake = "*"
|
||||||
|
email-validator = "*"
|
||||||
|
ujson = "*"
|
||||||
|
flake8 = "*"
|
||||||
|
python-multipart = "*"
|
||||||
|
sqlalchemy = "*"
|
||||||
|
uvicorn = "*"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
starlette = "==0.12.7"
|
||||||
|
pydantic = "==0.30.0"
|
||||||
|
databases = {extras = ["sqlite"],version = "*"}
|
||||||
|
hypercorn = "*"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.6"
|
||||||
|
|
||||||
|
[pipenv]
|
||||||
|
allow_prereleases = true
|
||||||
159
README.md
|
|
@ -5,14 +5,17 @@
|
||||||
<em>FastAPI framework, high performance, easy to learn, fast to code, ready for production</em>
|
<em>FastAPI framework, high performance, easy to learn, fast to code, ready for production</em>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/tiangolo/fastapi/actions?query=workflow%3ATest" target="_blank">
|
<a href="https://travis-ci.org/tiangolo/fastapi" target="_blank">
|
||||||
<img src="https://github.com/tiangolo/fastapi/workflows/Test/badge.svg" alt="Test">
|
<img src="https://travis-ci.org/tiangolo/fastapi.svg?branch=master" alt="Build Status">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://codecov.io/gh/tiangolo/fastapi" target="_blank">
|
<a href="https://codecov.io/gh/tiangolo/fastapi" target="_blank">
|
||||||
<img src="https://img.shields.io/codecov/c/github/tiangolo/fastapi?color=%2334D058" alt="Coverage">
|
<img src="https://codecov.io/gh/tiangolo/fastapi/branch/master/graph/badge.svg" alt="Coverage">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://pypi.org/project/fastapi" target="_blank">
|
<a href="https://pypi.org/project/fastapi" target="_blank">
|
||||||
<img src="https://img.shields.io/pypi/v/fastapi?color=%2334D058&label=pypi%20package" alt="Package version">
|
<img src="https://badge.fury.io/py/fastapi.svg" alt="Package version">
|
||||||
|
</a>
|
||||||
|
<a href="https://gitter.im/tiangolo/fastapi?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge" target="_blank">
|
||||||
|
<img src="https://badges.gitter.im/tiangolo/fastapi.svg" alt="Join the chat at https://gitter.im/tiangolo/fastapi">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -30,77 +33,38 @@ The key features are:
|
||||||
|
|
||||||
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance).
|
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance).
|
||||||
|
|
||||||
* **Fast to code**: Increase the speed to develop features by about 200% to 300%. *
|
* **Fast to code**: Increase the speed to develop features by about 200% to 300% *.
|
||||||
* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. *
|
* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. *
|
||||||
* **Intuitive**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> everywhere. Less time debugging.
|
* **Intuitive**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> everywhere. Less time debugging.
|
||||||
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
|
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
|
||||||
* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
|
* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
|
||||||
* **Robust**: Get production-ready code. With automatic interactive documentation.
|
* **Robust**: Get production-ready code. With automatic interactive documentation.
|
||||||
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (previously known as Swagger) and <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>.
|
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" target="_blank">OpenAPI</a> (previously known as Swagger) and <a href="http://json-schema.org/" target="_blank">JSON Schema</a>.
|
||||||
|
|
||||||
<small>* estimation based on tests on an internal development team, building production applications.</small>
|
<small>* estimation based on tests on an internal development team, building production applications.</small>
|
||||||
|
|
||||||
## Sponsors
|
|
||||||
|
|
||||||
<!-- sponsors -->
|
|
||||||
|
|
||||||
<a href="https://www.deta.sh/?ref=fastapi" target="_blank" title="The launchpad for all your (team's) ideas"><img src="https://fastapi.tiangolo.com/img/sponsors/deta.svg"></a>
|
|
||||||
<a href="https://bit.ly/2QSouzH" target="_blank" title="Jina: build neural search-as-a-service for any kind of data in just minutes."><img src="https://fastapi.tiangolo.com/img/sponsors/jina.svg"></a>
|
|
||||||
<a href="https://www.investsuite.com/jobs" target="_blank" title="Wealthtech jobs with FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/investsuite.svg"></a>
|
|
||||||
<a href="https://www.vim.so/?utm_source=FastAPI" target="_blank" title="We help you master vim with interactive exercises"><img src="https://fastapi.tiangolo.com/img/sponsors/vimso.png"></a>
|
|
||||||
<a href="https://talkpython.fm/fastapi-sponsor" target="_blank" title="FastAPI video courses on demand from people you trust"><img src="https://fastapi.tiangolo.com/img/sponsors/talkpython.png"></a>
|
|
||||||
|
|
||||||
<!-- /sponsors -->
|
|
||||||
|
|
||||||
<a href="https://fastapi.tiangolo.com/fastapi-people/#sponsors" class="external-link" target="_blank">Other sponsors</a>
|
|
||||||
|
|
||||||
## Opinions
|
## Opinions
|
||||||
|
|
||||||
"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._"
|
"*[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products.*"
|
||||||
|
|
||||||
<div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div>
|
<div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_"
|
"*I’m over the moon excited about **FastAPI**. It’s so fun!*"
|
||||||
|
|
||||||
<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_"
|
|
||||||
|
|
||||||
<div style="text-align: right; margin-right: 10%;">Kevin Glisson, Marc Vilanova, Forest Monsen - <strong>Netflix</strong> <a href="https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072" target="_blank"><small>(ref)</small></a></div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
"_I’m over the moon excited about **FastAPI**. It’s so fun!_"
|
|
||||||
|
|
||||||
<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> podcast host</strong> <a href="https://twitter.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div>
|
<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> podcast host</strong> <a href="https://twitter.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._"
|
"*Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that.*"
|
||||||
|
|
||||||
<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://www.hug.rest/" target="_blank">Hug</a> creator</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div>
|
<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="http://www.hug.rest/" target="_blank">Hug</a> creator</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_"
|
|
||||||
|
|
||||||
"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_"
|
|
||||||
|
|
||||||
<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **Typer**, the FastAPI of CLIs
|
|
||||||
|
|
||||||
<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a>
|
|
||||||
|
|
||||||
If you are building a <abbr title="Command Line Interface">CLI</abbr> app to be used in the terminal instead of a web API, check out <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>.
|
|
||||||
|
|
||||||
**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|
@ -108,33 +72,22 @@ Python 3.6+
|
||||||
|
|
||||||
FastAPI stands on the shoulders of giants:
|
FastAPI stands on the shoulders of giants:
|
||||||
|
|
||||||
* <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a> for the web parts.
|
* <a href="https://www.starlette.io/" target="_blank">Starlette</a> for the web parts.
|
||||||
* <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a> for the data parts.
|
* <a href="https://pydantic-docs.helpmanual.io/" target="_blank">Pydantic</a> for the data parts.
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
<div class="termy">
|
```bash
|
||||||
|
|
||||||
```console
|
|
||||||
$ pip install fastapi
|
$ pip install fastapi
|
||||||
|
|
||||||
---> 100%
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</div>
|
You will also need an ASGI server, for production such as <a href="http://www.uvicorn.org" target="_blank">Uvicorn</a> or <a href="https://gitlab.com/pgjones/hypercorn" target="_blank">Hypercorn</a>.
|
||||||
|
|
||||||
You will also need an ASGI server, for production such as <a href="https://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a> or <a href="https://gitlab.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>.
|
```bash
|
||||||
|
$ pip install uvicorn
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ pip install uvicorn[standard]
|
|
||||||
|
|
||||||
---> 100%
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
### Create it
|
### Create it
|
||||||
|
|
@ -142,8 +95,6 @@ $ pip install uvicorn[standard]
|
||||||
* Create a file `main.py` with:
|
* Create a file `main.py` with:
|
||||||
|
|
||||||
```Python
|
```Python
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
@ -155,18 +106,15 @@ def read_root():
|
||||||
|
|
||||||
|
|
||||||
@app.get("/items/{item_id}")
|
@app.get("/items/{item_id}")
|
||||||
def read_item(item_id: int, q: Optional[str] = None):
|
def read_item(item_id: int, q: str = None):
|
||||||
return {"item_id": item_id, "q": q}
|
return {"item_id": item_id, "q": q}
|
||||||
```
|
```
|
||||||
|
|
||||||
<details markdown="1">
|
<details markdown="1">
|
||||||
<summary>Or use <code>async def</code>...</summary>
|
<summary>Or use <code>async def</code>...</summary>
|
||||||
|
|
||||||
If your code uses `async` / `await`, use `async def`:
|
If your code uses `async` / `await`, use `async def`:
|
||||||
|
|
||||||
```Python hl_lines="9 14"
|
```Python hl_lines="7 12"
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
@ -178,7 +126,7 @@ async def read_root():
|
||||||
|
|
||||||
|
|
||||||
@app.get("/items/{item_id}")
|
@app.get("/items/{item_id}")
|
||||||
async def read_item(item_id: int, q: Optional[str] = None):
|
async def read_item(item_id: int, q: str = None):
|
||||||
return {"item_id": item_id, "q": q}
|
return {"item_id": item_id, "q": q}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -192,20 +140,10 @@ If you don't know, check the _"In a hurry?"_ section about <a href="https://fast
|
||||||
|
|
||||||
Run the server with:
|
Run the server with:
|
||||||
|
|
||||||
<div class="termy">
|
```bash
|
||||||
|
uvicorn main:app --reload
|
||||||
```console
|
|
||||||
$ uvicorn main:app --reload
|
|
||||||
|
|
||||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
INFO: Started reloader process [28720]
|
|
||||||
INFO: Started server process [28722]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
INFO: Application startup complete.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<details markdown="1">
|
<details markdown="1">
|
||||||
<summary>About the command <code>uvicorn main:app --reload</code>...</summary>
|
<summary>About the command <code>uvicorn main:app --reload</code>...</summary>
|
||||||
|
|
||||||
|
|
@ -219,7 +157,7 @@ The command `uvicorn main:app` refers to:
|
||||||
|
|
||||||
### Check it
|
### Check it
|
||||||
|
|
||||||
Open your browser at <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>.
|
Open your browser at <a href="http://127.0.0.1:8000/items/5?q=somequery" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>.
|
||||||
|
|
||||||
You will see the JSON response as:
|
You will see the JSON response as:
|
||||||
|
|
||||||
|
|
@ -236,17 +174,18 @@ You already created an API that:
|
||||||
|
|
||||||
### Interactive API docs
|
### Interactive API docs
|
||||||
|
|
||||||
Now go to <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
|
Now go to <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||||
|
|
||||||
You will see the automatic interactive API documentation (provided by <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>):
|
You will see the automatic interactive API documentation (provided by <a href="https://github.com/swagger-api/swagger-ui" target="_blank">Swagger UI</a>):
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
### Alternative API docs
|
### Alternative API docs
|
||||||
|
|
||||||
And now, go to <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>.
|
And now, go to <a href="http://127.0.0.1:8000/redoc" target="_blank">http://127.0.0.1:8000/redoc</a>.
|
||||||
|
|
||||||
You will see the alternative automatic documentation (provided by <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>):
|
You will see the alternative automatic documentation (provided by <a href="https://github.com/Rebilly/ReDoc" target="_blank">ReDoc</a>):
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -256,9 +195,8 @@ Now modify the file `main.py` to receive a body from a `PUT` request.
|
||||||
|
|
||||||
Declare the body using standard Python types, thanks to Pydantic.
|
Declare the body using standard Python types, thanks to Pydantic.
|
||||||
|
|
||||||
```Python hl_lines="4 9-12 25-27"
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
```Python hl_lines="2 7 8 9 10 24"
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
@ -268,7 +206,7 @@ app = FastAPI()
|
||||||
class Item(BaseModel):
|
class Item(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
price: float
|
price: float
|
||||||
is_offer: Optional[bool] = None
|
is_offer: bool = None
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
|
@ -277,7 +215,7 @@ def read_root():
|
||||||
|
|
||||||
|
|
||||||
@app.get("/items/{item_id}")
|
@app.get("/items/{item_id}")
|
||||||
def read_item(item_id: int, q: Optional[str] = None):
|
def read_item(item_id: int, q: str = None):
|
||||||
return {"item_id": item_id, "q": q}
|
return {"item_id": item_id, "q": q}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -290,7 +228,7 @@ The server should reload automatically (because you added `--reload` to the `uvi
|
||||||
|
|
||||||
### Interactive API docs upgrade
|
### Interactive API docs upgrade
|
||||||
|
|
||||||
Now go to <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
|
Now go to <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||||
|
|
||||||
* The interactive API documentation will be automatically updated, including the new body:
|
* The interactive API documentation will be automatically updated, including the new body:
|
||||||
|
|
||||||
|
|
@ -304,14 +242,16 @@ Now go to <a href="http://127.0.0.1:8000/docs" class="external-link" target="_bl
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
### Alternative API docs upgrade
|
### Alternative API docs upgrade
|
||||||
|
|
||||||
And now, go to <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>.
|
And now, go to <a href="http://127.0.0.1:8000/redoc" target="_blank">http://127.0.0.1:8000/redoc</a>.
|
||||||
|
|
||||||
* The alternative documentation will also reflect the new query parameter and body:
|
* The alternative documentation will also reflect the new query parameter and body:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
### Recap
|
### Recap
|
||||||
|
|
||||||
In summary, you declare **once** the types of parameters, body, etc. as function parameters.
|
In summary, you declare **once** the types of parameters, body, etc. as function parameters.
|
||||||
|
|
@ -372,7 +312,7 @@ Coming back to the previous code example, **FastAPI** will:
|
||||||
* Without the `None` it would be required (as is the body in the case with `PUT`).
|
* Without the `None` it would be required (as is the body in the case with `PUT`).
|
||||||
* For `PUT` requests to `/items/{item_id}`, Read the body as JSON:
|
* For `PUT` requests to `/items/{item_id}`, Read the body as JSON:
|
||||||
* Check that it has a required attribute `name` that should be a `str`.
|
* Check that it has a required attribute `name` that should be a `str`.
|
||||||
* Check that it has a required attribute `price` that has to be a `float`.
|
* Check that is has a required attribute `price` that has to be a `float`.
|
||||||
* Check that it has an optional attribute `is_offer`, that should be a `bool`, if present.
|
* Check that it has an optional attribute `is_offer`, that should be a `bool`, if present.
|
||||||
* All this would also work for deeply nested JSON objects.
|
* All this would also work for deeply nested JSON objects.
|
||||||
* Convert from and to JSON automatically.
|
* Convert from and to JSON automatically.
|
||||||
|
|
@ -381,6 +321,7 @@ Coming back to the previous code example, **FastAPI** will:
|
||||||
* Automatic client code generation systems, for many languages.
|
* Automatic client code generation systems, for many languages.
|
||||||
* Provide 2 interactive documentation web interfaces directly.
|
* Provide 2 interactive documentation web interfaces directly.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
We just scratched the surface, but you already get the idea of how it all works.
|
We just scratched the surface, but you already get the idea of how it all works.
|
||||||
|
|
@ -407,7 +348,8 @@ Try changing the line with:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
For a more complete example including more features, see the <a href="https://fastapi.tiangolo.com/tutorial/">Tutorial - User Guide</a>.
|
|
||||||
|
For a more complete example including more features, see the <a href="https://fastapi.tiangolo.com/tutorial/intro/">Tutorial - User Guide</a>.
|
||||||
|
|
||||||
**Spoiler alert**: the tutorial - user guide includes:
|
**Spoiler alert**: the tutorial - user guide includes:
|
||||||
|
|
||||||
|
|
@ -424,11 +366,12 @@ For a more complete example including more features, see the <a href="https://fa
|
||||||
* **Cookie Sessions**
|
* **Cookie Sessions**
|
||||||
* ...and more.
|
* ...and more.
|
||||||
|
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
||||||
|
|
||||||
To understand more about it, see the section <a href="https://fastapi.tiangolo.com/benchmarks/" class="internal-link" target="_blank">Benchmarks</a>.
|
To understand more about it, see the section <a href="https://fastapi.tiangolo.com/benchmarks/" target="_blank">Benchmarks</a>.
|
||||||
|
|
||||||
## Optional Dependencies
|
## Optional Dependencies
|
||||||
|
|
||||||
|
|
@ -437,23 +380,23 @@ Used by Pydantic:
|
||||||
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - for faster JSON <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
|
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - for faster JSON <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
|
||||||
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
|
* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
|
||||||
|
|
||||||
|
|
||||||
Used by Starlette:
|
Used by Starlette:
|
||||||
|
|
||||||
* <a href="https://requests.readthedocs.io" target="_blank"><code>requests</code></a> - Required if you want to use the `TestClient`.
|
* <a href="http://docs.python-requests.org" target="_blank"><code>requests</code></a> - Required if you want to use the `TestClient`.
|
||||||
* <a href="https://github.com/Tinche/aiofiles" target="_blank"><code>aiofiles</code></a> - Required if you want to use `FileResponse` or `StaticFiles`.
|
* <a href="https://github.com/Tinche/aiofiles" target="_blank"><code>aiofiles</code></a> - Required if you want to use `FileResponse` or `StaticFiles`.
|
||||||
* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - Required if you want to use the default template configuration.
|
* <a href="http://jinja.pocoo.org" target="_blank"><code>jinja2</code></a> - Required if you want to use the default template configuration.
|
||||||
* <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - Required if you want to support form <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>, with `request.form()`.
|
* <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - Required if you want to support form <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>, with `request.form()`.
|
||||||
* <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - Required for `SessionMiddleware` support.
|
* <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - Required for `SessionMiddleware` support.
|
||||||
* <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI).
|
* <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - Required for `SchemaGenerator` support.
|
||||||
* <a href="https://graphene-python.org/" target="_blank"><code>graphene</code></a> - Required for `GraphQLApp` support.
|
* <a href="https://graphene-python.org/" target="_blank"><code>graphene</code></a> - Required for `GraphQLApp` support.
|
||||||
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - Required if you want to use `UJSONResponse`.
|
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - Required if you want to use `UJSONResponse`.
|
||||||
|
|
||||||
Used by FastAPI / Starlette:
|
Used by FastAPI / Starlette:
|
||||||
|
|
||||||
* <a href="https://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - for the server that loads and serves your application.
|
* <a href="http://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - for the server that loads and serves your application.
|
||||||
* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - Required if you want to use `ORJSONResponse`.
|
|
||||||
|
|
||||||
You can install all of these with `pip install fastapi[all]`.
|
You can install all of these with `pip3 install fastapi[all]`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
31
SECURITY.md
|
|
@ -1,31 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
Security is very important for FastAPI and its community. 🔒
|
|
||||||
|
|
||||||
Learn more about it below. 👇
|
|
||||||
|
|
||||||
## Versions
|
|
||||||
|
|
||||||
The latest versions of FastAPI are supported.
|
|
||||||
|
|
||||||
You are encouraged to [write tests](https://fastapi.tiangolo.com/tutorial/testing/) for your application and update your FastAPI version frequently after ensuring that your tests are passing. This way you will benefit from the latest features, bug fixes, and **security fixes**.
|
|
||||||
|
|
||||||
You can learn more about [FastAPI versions and how to pin and upgrade them](https://fastapi.tiangolo.com/deployment/versions/) for your project in the docs.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
If you think you found a vulnerability, and even if you are not sure about it, please report it right away by sending an email to: security@tiangolo.com. Please try to be as explicit as possible, describing all the steps and example code to reproduce the security issue.
|
|
||||||
|
|
||||||
I (the author, [@tiangolo](https://twitter.com/tiangolo)) will review it thoroughly and get back to you.
|
|
||||||
|
|
||||||
## Public Discussions
|
|
||||||
|
|
||||||
Please restrain from publicly discussing a potential security vulnerability. 🙊
|
|
||||||
|
|
||||||
It's better to discuss privately and try to find a solution first, to limit the potential impact as much as possible.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Thanks for your help!
|
|
||||||
|
|
||||||
The FastAPI community and I thank you for that. 🙇
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
# Alternatives, Inspiration and Comparisons
|
|
||||||
|
|
||||||
What inspired **FastAPI**, how it compares to other alternatives and what it learned from them.
|
What inspired **FastAPI**, how it compares to other alternatives and what it learned from them.
|
||||||
|
|
||||||
## Intro
|
## Intro
|
||||||
|
|
@ -14,7 +12,7 @@ But at some point, there was no other option than creating something that provid
|
||||||
|
|
||||||
## Previous tools
|
## Previous tools
|
||||||
|
|
||||||
### <a href="https://www.djangoproject.com/" class="external-link" target="_blank">Django</a>
|
### <a href="https://www.djangoproject.com/" target="_blank">Django</a>
|
||||||
|
|
||||||
It's the most popular Python framework and is widely trusted. It is used to build systems like Instagram.
|
It's the most popular Python framework and is widely trusted. It is used to build systems like Instagram.
|
||||||
|
|
||||||
|
|
@ -22,7 +20,7 @@ It's relatively tightly coupled with relational databases (like MySQL or Postgre
|
||||||
|
|
||||||
It was created to generate the HTML in the backend, not to create APIs used by a modern frontend (like React, Vue.js and Angular) or by other systems (like <abbr title="Internet of Things">IoT</abbr> devices) communicating with it.
|
It was created to generate the HTML in the backend, not to create APIs used by a modern frontend (like React, Vue.js and Angular) or by other systems (like <abbr title="Internet of Things">IoT</abbr> devices) communicating with it.
|
||||||
|
|
||||||
### <a href="https://www.django-rest-framework.org/" class="external-link" target="_blank">Django REST Framework</a>
|
### <a href="https://www.django-rest-framework.org/" target="_blank">Django REST Framework</a>
|
||||||
|
|
||||||
Django REST framework was created to be a flexible toolkit for building Web APIs using Django underneath, to improve its API capabilities.
|
Django REST framework was created to be a flexible toolkit for building Web APIs using Django underneath, to improve its API capabilities.
|
||||||
|
|
||||||
|
|
@ -37,7 +35,7 @@ It was one of the first examples of **automatic API documentation**, and this wa
|
||||||
!!! check "Inspired **FastAPI** to"
|
!!! check "Inspired **FastAPI** to"
|
||||||
Have an automatic API documentation web user interface.
|
Have an automatic API documentation web user interface.
|
||||||
|
|
||||||
### <a href="https://flask.palletsprojects.com" class="external-link" target="_blank">Flask</a>
|
### <a href="http://flask.pocoo.org/" target="_blank">Flask</a>
|
||||||
|
|
||||||
Flask is a "microframework", it doesn't include database integrations nor many of the things that come by default in Django.
|
Flask is a "microframework", it doesn't include database integrations nor many of the things that come by default in Django.
|
||||||
|
|
||||||
|
|
@ -57,7 +55,7 @@ Given the simplicity of Flask, it seemed like a good match for building APIs. Th
|
||||||
Have a simple and easy to use routing system.
|
Have a simple and easy to use routing system.
|
||||||
|
|
||||||
|
|
||||||
### <a href="https://requests.readthedocs.io" class="external-link" target="_blank">Requests</a>
|
### <a href="http://docs.python-requests.org" target="_blank">Requests</a>
|
||||||
|
|
||||||
**FastAPI** is not actually an alternative to **Requests**. Their scope is very different.
|
**FastAPI** is not actually an alternative to **Requests**. Their scope is very different.
|
||||||
|
|
||||||
|
|
@ -81,7 +79,7 @@ The way you use it is very simple. For example, to do a `GET` request, you would
|
||||||
response = requests.get("http://example.com/some/url")
|
response = requests.get("http://example.com/some/url")
|
||||||
```
|
```
|
||||||
|
|
||||||
The FastAPI counterpart API *path operation* could look like:
|
The FastAPI counterpart API path operation could look like:
|
||||||
|
|
||||||
```Python hl_lines="1"
|
```Python hl_lines="1"
|
||||||
@app.get("/some/url")
|
@app.get("/some/url")
|
||||||
|
|
@ -97,7 +95,7 @@ See the similarities in `requests.get(...)` and `@app.get(...)`.
|
||||||
* Have sensible defaults, but powerful customizations.
|
* Have sensible defaults, but powerful customizations.
|
||||||
|
|
||||||
|
|
||||||
### <a href="https://swagger.io/" class="external-link" target="_blank">Swagger</a> / <a href="https://github.com/OAI/OpenAPI-Specification/" class="external-link" target="_blank">OpenAPI</a>
|
### <a href="https://swagger.io/" target="_blank">Swagger</a> / <a href="https://github.com/OAI/OpenAPI-Specification/" target="_blank">OpenAPI</a>
|
||||||
|
|
||||||
The main feature I wanted from Django REST Framework was the automatic API documentation.
|
The main feature I wanted from Django REST Framework was the automatic API documentation.
|
||||||
|
|
||||||
|
|
@ -114,8 +112,8 @@ That's why when talking about version 2.0 it's common to say "Swagger", and for
|
||||||
|
|
||||||
And integrate standards-based user interface tools:
|
And integrate standards-based user interface tools:
|
||||||
|
|
||||||
* <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>
|
* <a href="https://github.com/swagger-api/swagger-ui" target="_blank">Swagger UI</a>
|
||||||
* <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>
|
* <a href="https://github.com/Rebilly/ReDoc" target="_blank">ReDoc</a>
|
||||||
|
|
||||||
These two were chosen for being fairly popular and stable, but doing a quick search, you could find dozens of additional alternative user interfaces for OpenAPI (that you can use with **FastAPI**).
|
These two were chosen for being fairly popular and stable, but doing a quick search, you could find dozens of additional alternative user interfaces for OpenAPI (that you can use with **FastAPI**).
|
||||||
|
|
||||||
|
|
@ -123,7 +121,7 @@ That's why when talking about version 2.0 it's common to say "Swagger", and for
|
||||||
|
|
||||||
There are several Flask REST frameworks, but after investing the time and work into investigating them, I found that many are discontinued or abandoned, with several standing issues that made them unfit.
|
There are several Flask REST frameworks, but after investing the time and work into investigating them, I found that many are discontinued or abandoned, with several standing issues that made them unfit.
|
||||||
|
|
||||||
### <a href="https://marshmallow.readthedocs.io/en/3.0/" class="external-link" target="_blank">Marshmallow</a>
|
### <a href="https://marshmallow.readthedocs.io/en/3.0/" target="_blank">Marshmallow</a>
|
||||||
|
|
||||||
One of the main features needed by API systems is data "<abbr title="also called marshalling, conversion">serialization</abbr>" which is taking data from the code (Python) and converting it into something that can be sent through the network. For example, converting an object containing data from a database into a JSON object. Converting `datetime` objects into strings, etc.
|
One of the main features needed by API systems is data "<abbr title="also called marshalling, conversion">serialization</abbr>" which is taking data from the code (Python) and converting it into something that can be sent through the network. For example, converting an object containing data from a database into a JSON object. Converting `datetime` objects into strings, etc.
|
||||||
|
|
||||||
|
|
@ -138,23 +136,23 @@ But it was created before there existed Python type hints. So, to define every <
|
||||||
!!! check "Inspired **FastAPI** to"
|
!!! check "Inspired **FastAPI** to"
|
||||||
Use code to define "schemas" that provide data types and validation, automatically.
|
Use code to define "schemas" that provide data types and validation, automatically.
|
||||||
|
|
||||||
### <a href="https://webargs.readthedocs.io/en/latest/" class="external-link" target="_blank">Webargs</a>
|
### <a href="https://webargs.readthedocs.io/en/latest/" target="_blank">Webargs</a>
|
||||||
|
|
||||||
Another big feature required by APIs is <abbr title="reading and converting to Python data">parsing</abbr> data from incoming requests.
|
Another big feature required by APIs is <abbr title="reading and converting to Python data">parsing</abbr> data from incoming requests.
|
||||||
|
|
||||||
Webargs is a tool that was made to provide that on top of several frameworks, including Flask.
|
Webargs is a tool that was made to provide that on top of several frameworks, including Flask.
|
||||||
|
|
||||||
It uses Marshmallow underneath to do the data validation. And it was created by the same developers.
|
It uses Marshmallow underneath to do the data validation. And it was created by the same guys.
|
||||||
|
|
||||||
It's a great tool and I have used it a lot too, before having **FastAPI**.
|
It's a great tool and I have used it a lot too, before having **FastAPI**.
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
Webargs was created by the same Marshmallow developers.
|
Webargs was created by the same Marshmallow guys.
|
||||||
|
|
||||||
!!! check "Inspired **FastAPI** to"
|
!!! check "Inspired **FastAPI** to"
|
||||||
Have automatic validation of incoming request data.
|
Have automatic validation of incoming request data.
|
||||||
|
|
||||||
### <a href="https://apispec.readthedocs.io/en/stable/" class="external-link" target="_blank">APISpec</a>
|
### <a href="https://apispec.readthedocs.io/en/stable/" target="_blank">APISpec</a>
|
||||||
|
|
||||||
Marshmallow and Webargs provide validation, parsing and serialization as plug-ins.
|
Marshmallow and Webargs provide validation, parsing and serialization as plug-ins.
|
||||||
|
|
||||||
|
|
@ -173,13 +171,13 @@ But then, we have again the problem of having a micro-syntax, inside of a Python
|
||||||
The editor can't help much with that. And if we modify parameters or Marshmallow schemas and forget to also modify that YAML docstring, the generated schema would be obsolete.
|
The editor can't help much with that. And if we modify parameters or Marshmallow schemas and forget to also modify that YAML docstring, the generated schema would be obsolete.
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
APISpec was created by the same Marshmallow developers.
|
APISpec was created by the same Marshmallow guys.
|
||||||
|
|
||||||
|
|
||||||
!!! check "Inspired **FastAPI** to"
|
!!! check "Inspired **FastAPI** to"
|
||||||
Support the open standard for APIs, OpenAPI.
|
Support the open standard for APIs, OpenAPI.
|
||||||
|
|
||||||
### <a href="https://flask-apispec.readthedocs.io/en/latest/" class="external-link" target="_blank">Flask-apispec</a>
|
### <a href="https://flask-apispec.readthedocs.io/en/latest/" target="_blank">Flask-apispec</a>
|
||||||
|
|
||||||
It's a Flask plug-in, that ties together Webargs, Marshmallow and APISpec.
|
It's a Flask plug-in, that ties together Webargs, Marshmallow and APISpec.
|
||||||
|
|
||||||
|
|
@ -193,19 +191,19 @@ This combination of Flask, Flask-apispec with Marshmallow and Webargs was my fav
|
||||||
|
|
||||||
Using it led to the creation of several Flask full-stack generators. These are the main stack I (and several external teams) have been using up to now:
|
Using it led to the creation of several Flask full-stack generators. These are the main stack I (and several external teams) have been using up to now:
|
||||||
|
|
||||||
* <a href="https://github.com/tiangolo/full-stack" class="external-link" target="_blank">https://github.com/tiangolo/full-stack</a>
|
* <a href="https://github.com/tiangolo/full-stack" target="_blank">https://github.com/tiangolo/full-stack</a>
|
||||||
* <a href="https://github.com/tiangolo/full-stack-flask-couchbase" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-flask-couchbase</a>
|
* <a href="https://github.com/tiangolo/full-stack-flask-couchbase" target="_blank">https://github.com/tiangolo/full-stack-flask-couchbase</a>
|
||||||
* <a href="https://github.com/tiangolo/full-stack-flask-couchdb" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-flask-couchdb</a>
|
* <a href="https://github.com/tiangolo/full-stack-flask-couchdb" target="_blank">https://github.com/tiangolo/full-stack-flask-couchdb</a>
|
||||||
|
|
||||||
And these same full-stack generators were the base of the [**FastAPI** Project Generators](project-generation.md){.internal-link target=_blank}.
|
And these same full-stack generators were the base of the <a href="/project-generation/" target="_blank">**FastAPI** project generator</a>.
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
Flask-apispec was created by the same Marshmallow developers.
|
Flask-apispec was created by the same Marshmallow guys.
|
||||||
|
|
||||||
!!! check "Inspired **FastAPI** to"
|
!!! check "Inspired **FastAPI** to"
|
||||||
Generate the OpenAPI schema automatically, from the same code that defines serialization and validation.
|
Generate the OpenAPI schema automatically, from the same code that defines serialization and validation.
|
||||||
|
|
||||||
### <a href="https://nestjs.com/" class="external-link" target="_blank">NestJS</a> (and <a href="https://angular.io/" class="external-link" target="_blank">Angular</a>)
|
### <a href="https://nestjs.com/" target="_blank">NestJS</a> (and <a href="https://angular.io/" target="_blank">Angular</a>)
|
||||||
|
|
||||||
This isn't even Python, NestJS is a JavaScript (TypeScript) NodeJS framework inspired by Angular.
|
This isn't even Python, NestJS is a JavaScript (TypeScript) NodeJS framework inspired by Angular.
|
||||||
|
|
||||||
|
|
@ -224,21 +222,21 @@ It can't handle nested models very well. So, if the JSON body in the request is
|
||||||
|
|
||||||
Have a powerful dependency injection system. Find a way to minimize code repetition.
|
Have a powerful dependency injection system. Find a way to minimize code repetition.
|
||||||
|
|
||||||
### <a href="https://sanic.readthedocs.io/en/latest/" class="external-link" target="_blank">Sanic</a>
|
### <a href="https://sanic.readthedocs.io/en/latest/" target="_blank">Sanic</a>
|
||||||
|
|
||||||
It was one of the first extremely fast Python frameworks based on `asyncio`. It was made to be very similar to Flask.
|
It was one of the first extremely fast Python frameworks based on `asyncio`. It was made to be very similar to Flask.
|
||||||
|
|
||||||
!!! note "Technical Details"
|
!!! note "Technical Details"
|
||||||
It used <a href="https://github.com/MagicStack/uvloop" class="external-link" target="_blank">`uvloop`</a> instead of the default Python `asyncio` loop. That's what made it so fast.
|
It used <a href="https://github.com/MagicStack/uvloop" target="_blank">`uvloop`</a> instead of the default Python `asyncio` loop. That's what made it so fast.
|
||||||
|
|
||||||
It clearly inspired Uvicorn and Starlette, that are currently faster than Sanic in open benchmarks.
|
It <a href="https://github.com/huge-success/sanic/issues/761" target="_blank">still doesn't implement the ASGI spec for Python asynchronous web development</a>, but it clearly inspired Uvicorn and Starlette, that are currently faster than Sanic in open benchmarks.
|
||||||
|
|
||||||
!!! check "Inspired **FastAPI** to"
|
!!! check "Inspired **FastAPI** to"
|
||||||
Find a way to have a crazy performance.
|
Find a way to have a crazy performance.
|
||||||
|
|
||||||
That's why **FastAPI** is based on Starlette, as it is the fastest framework available (tested by third-party benchmarks).
|
That's why **FastAPI** is based on Starlette, as it is the fastest framework available (tested by third-party benchmarks).
|
||||||
|
|
||||||
### <a href="https://falconframework.org/" class="external-link" target="_blank">Falcon</a>
|
### <a href="https://falconframework.org/" target="_blank">Falcon</a>
|
||||||
|
|
||||||
Falcon is another high performance Python framework, it is designed to be minimal, and work as the foundation of other frameworks like Hug.
|
Falcon is another high performance Python framework, it is designed to be minimal, and work as the foundation of other frameworks like Hug.
|
||||||
|
|
||||||
|
|
@ -255,7 +253,7 @@ So, data validation, serialization, and documentation, have to be done in code,
|
||||||
|
|
||||||
Although in FastAPI it's optional, and is used mainly to set headers, cookies, and alternative status codes.
|
Although in FastAPI it's optional, and is used mainly to set headers, cookies, and alternative status codes.
|
||||||
|
|
||||||
### <a href="https://moltenframework.com/" class="external-link" target="_blank">Molten</a>
|
### <a href="https://moltenframework.com/" target="_blank">Molten</a>
|
||||||
|
|
||||||
I discovered Molten in the first stages of building **FastAPI**. And it has quite similar ideas:
|
I discovered Molten in the first stages of building **FastAPI**. And it has quite similar ideas:
|
||||||
|
|
||||||
|
|
@ -276,7 +274,7 @@ Routes are declared in a single place, using functions declared in other places
|
||||||
|
|
||||||
This actually inspired updating parts of Pydantic, to support the same validation declaration style (all this functionality is now already available in Pydantic).
|
This actually inspired updating parts of Pydantic, to support the same validation declaration style (all this functionality is now already available in Pydantic).
|
||||||
|
|
||||||
### <a href="https://www.hug.rest/" class="external-link" target="_blank">Hug</a>
|
### <a href="http://www.hug.rest/" target="_blank">Hug</a>
|
||||||
|
|
||||||
Hug was one of the first frameworks to implement the declaration of API parameter types using Python type hints. This was a great idea that inspired other tools to do the same.
|
Hug was one of the first frameworks to implement the declaration of API parameter types using Python type hints. This was a great idea that inspired other tools to do the same.
|
||||||
|
|
||||||
|
|
@ -291,7 +289,7 @@ It has an interesting, uncommon feature: using the same framework, it's possible
|
||||||
As it is based on the previous standard for synchronous Python web frameworks (WSGI), it can't handle Websockets and other things, although it still has high performance too.
|
As it is based on the previous standard for synchronous Python web frameworks (WSGI), it can't handle Websockets and other things, although it still has high performance too.
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
Hug was created by Timothy Crosley, the same creator of <a href="https://github.com/timothycrosley/isort" class="external-link" target="_blank">`isort`</a>, a great tool to automatically sort imports in Python files.
|
Hug was created by Timothy Crosley, the same creator of <a href="https://github.com/timothycrosley/isort" target="_blank">`isort`</a>, a great tool to automatically sort imports in Python files.
|
||||||
|
|
||||||
!!! check "Ideas inspired in **FastAPI**"
|
!!! check "Ideas inspired in **FastAPI**"
|
||||||
Hug inspired parts of APIStar, and was one of the tools I found most promising, alongside APIStar.
|
Hug inspired parts of APIStar, and was one of the tools I found most promising, alongside APIStar.
|
||||||
|
|
@ -300,7 +298,7 @@ As it is based on the previous standard for synchronous Python web frameworks (W
|
||||||
|
|
||||||
Hug inspired **FastAPI** to declare a `response` parameter in functions to set headers and cookies.
|
Hug inspired **FastAPI** to declare a `response` parameter in functions to set headers and cookies.
|
||||||
|
|
||||||
### <a href="https://github.com/encode/apistar" class="external-link" target="_blank">APIStar</a> (<= 0.5)
|
### <a href="https://github.com/encode/apistar" target="_blank">APIStar</a> (<= 0.5)
|
||||||
|
|
||||||
Right before deciding to build **FastAPI** I found **APIStar** server. It had almost everything I was looking for and had a great design.
|
Right before deciding to build **FastAPI** I found **APIStar** server. It had almost everything I was looking for and had a great design.
|
||||||
|
|
||||||
|
|
@ -344,7 +342,7 @@ Now APIStar is a set of tools to validate OpenAPI specifications, not a web fram
|
||||||
|
|
||||||
## Used by **FastAPI**
|
## Used by **FastAPI**
|
||||||
|
|
||||||
### <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a>
|
### <a href="https://pydantic-docs.helpmanual.io/" target="_blank">Pydantic</a>
|
||||||
|
|
||||||
Pydantic is a library to define data validation, serialization and documentation (using JSON Schema) based on Python type hints.
|
Pydantic is a library to define data validation, serialization and documentation (using JSON Schema) based on Python type hints.
|
||||||
|
|
||||||
|
|
@ -357,7 +355,7 @@ It is comparable to Marshmallow. Although it's faster than Marshmallow in benchm
|
||||||
|
|
||||||
**FastAPI** then takes that JSON Schema data and puts it in OpenAPI, apart from all the other things it does.
|
**FastAPI** then takes that JSON Schema data and puts it in OpenAPI, apart from all the other things it does.
|
||||||
|
|
||||||
### <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a>
|
### <a href="https://www.starlette.io/" target="_blank">Starlette</a>
|
||||||
|
|
||||||
Starlette is a lightweight <abbr title="The new standard for building asynchronous Python web">ASGI</abbr> framework/toolkit, which is ideal for building high-performance asyncio services.
|
Starlette is a lightweight <abbr title="The new standard for building asynchronous Python web">ASGI</abbr> framework/toolkit, which is ideal for building high-performance asyncio services.
|
||||||
|
|
||||||
|
|
@ -397,7 +395,7 @@ That's one of the main things that **FastAPI** adds on top, all based on Python
|
||||||
|
|
||||||
So, anything that you can do with Starlette, you can do it directly with **FastAPI**, as it is basically Starlette on steroids.
|
So, anything that you can do with Starlette, you can do it directly with **FastAPI**, as it is basically Starlette on steroids.
|
||||||
|
|
||||||
### <a href="https://www.uvicorn.org/" class="external-link" target="_blank">Uvicorn</a>
|
### <a href="https://www.uvicorn.org/" target="_blank">Uvicorn</a>
|
||||||
|
|
||||||
Uvicorn is a lightning-fast ASGI server, built on uvloop and httptools.
|
Uvicorn is a lightning-fast ASGI server, built on uvloop and httptools.
|
||||||
|
|
||||||
|
|
@ -410,8 +408,8 @@ It is the recommended server for Starlette and **FastAPI**.
|
||||||
|
|
||||||
You can combine it with Gunicorn, to have an asynchronous multi-process server.
|
You can combine it with Gunicorn, to have an asynchronous multi-process server.
|
||||||
|
|
||||||
Check more details in the [Deployment](deployment/index.md){.internal-link target=_blank} section.
|
Check more details in the <a href="/deployment/" target="_blank">Deployment</a> section.
|
||||||
|
|
||||||
## Benchmarks and speed
|
## Benchmarks and speed
|
||||||
|
|
||||||
To understand, compare, and see the difference between Uvicorn, Starlette and FastAPI, check the section about [Benchmarks](benchmarks.md){.internal-link target=_blank}.
|
To understand, compare, and see the difference between Uvicorn, Starlette and FastAPI, check the section about [Benchmarks](/benchmarks/).
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
# Concurrency and async / await
|
Details about the `async def` syntax for path operation functions and some background about asynchronous code, concurrency, and parallelism.
|
||||||
|
|
||||||
Details about the `async def` syntax for *path operation functions* and some background about asynchronous code, concurrency, and parallelism.
|
|
||||||
|
|
||||||
## In a hurry?
|
## In a hurry?
|
||||||
|
|
||||||
|
|
@ -8,11 +7,12 @@ Details about the `async def` syntax for *path operation functions* and some bac
|
||||||
|
|
||||||
If you are using third party libraries that tell you to call them with `await`, like:
|
If you are using third party libraries that tell you to call them with `await`, like:
|
||||||
|
|
||||||
|
|
||||||
```Python
|
```Python
|
||||||
results = await some_library()
|
results = await some_library()
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, declare your *path operation functions* with `async def` like:
|
Then, declare your path operation functions with `async def` like:
|
||||||
|
|
||||||
```Python hl_lines="2"
|
```Python hl_lines="2"
|
||||||
@app.get('/')
|
@app.get('/')
|
||||||
|
|
@ -26,7 +26,7 @@ async def read_results():
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
If you are using a third party library that communicates with something (a database, an API, the file system, etc) and doesn't have support for using `await`, (this is currently the case for most database libraries), then declare your *path operation functions* as normally, with just `def`, like:
|
If you are using a third party library that communicates with something (a database, an API, the file system, etc) and doesn't have support for using `await`, (this is currently the case for most database libraries), then declare your path operation functions as normally, with just `def`, like:
|
||||||
|
|
||||||
```Python hl_lines="2"
|
```Python hl_lines="2"
|
||||||
@app.get('/')
|
@app.get('/')
|
||||||
|
|
@ -45,31 +45,33 @@ If you just don't know, use normal `def`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note**: you can mix `def` and `async def` in your *path operation functions* as much as you need and define each one using the best option for you. FastAPI will do the right thing with them.
|
**Note**: you can mix `def` and `async def` in your path operation functions as much as you need and define each one using the best option for you. FastAPI will do the right thing with them.
|
||||||
|
|
||||||
Anyway, in any of the cases above, FastAPI will still work asynchronously and be extremely fast.
|
Anyway, in any of the cases above, FastAPI will still work asynchronously and be extremely fast.
|
||||||
|
|
||||||
But by following the steps above, it will be able to do some performance optimizations.
|
But by following the steps above, it will be able to do some performance optimizations.
|
||||||
|
|
||||||
|
|
||||||
## Technical Details
|
## Technical Details
|
||||||
|
|
||||||
Modern versions of Python have support for **"asynchronous code"** using something called **"coroutines"**, with **`async` and `await`** syntax.
|
Modern versions of Python have support for **"asynchronous code"** using something called **"coroutines"**, with **`async` and `await`** syntax.
|
||||||
|
|
||||||
Let's see that phrase by parts in the sections below:
|
Let's see that phrase by parts in the sections below, below:
|
||||||
|
|
||||||
* **Asynchronous Code**
|
* **Asynchronous Code**
|
||||||
* **`async` and `await`**
|
* **`async` and `await`**
|
||||||
* **Coroutines**
|
* **Coroutines**
|
||||||
|
|
||||||
|
|
||||||
## Asynchronous Code
|
## Asynchronous Code
|
||||||
|
|
||||||
Asynchronous code just means that the language 💬 has a way to tell the computer / program 🤖 that at some point in the code, it 🤖 will have to wait for *something else* to finish somewhere else. Let's say that *something else* is called "slow-file" 📝.
|
Asynchronous code just means that the language has a way to tell the computer / program that at some point in the code, he will have to wait for *something else* to finish somewhere else. Let's say that *something else* is called "slow-file".
|
||||||
|
|
||||||
So, during that time, the computer can go and do some other work, while "slow-file" 📝 finishes.
|
So, during that time, the computer can go and do some other work, while "slow-file" finishes.
|
||||||
|
|
||||||
Then the computer / program 🤖 will come back every time it has a chance because it's waiting again, or whenever it 🤖 finished all the work it had at that point. And it 🤖 will see if any of the tasks it was waiting for have already finished, doing whatever it had to do.
|
Then the computer / program will come back every time it has a chance because it's waiting again, or whenever he finished all the work he had at that point. And it will see if any of the tasks he was waiting for has already finished doing whatever it had to do.
|
||||||
|
|
||||||
Next, it 🤖 takes the first task to finish (let's say, our "slow-file" 📝) and continues whatever it had to do with it.
|
And then it takes the first task to finish (let's say, our "slow-file") and continues whatever it had to do with it.
|
||||||
|
|
||||||
That "wait for something else" normally refers to <abbr title="Input and Output">I/O</abbr> operations that are relatively "slow" (compared to the speed of the processor and the RAM memory), like waiting for:
|
That "wait for something else" normally refers to <abbr title="Input and Output">I/O</abbr> operations that are relatively "slow" (compared to the speed of the processor and the RAM memory), like waiting for:
|
||||||
|
|
||||||
|
|
@ -82,7 +84,7 @@ That "wait for something else" normally refers to <abbr title="Input and Output"
|
||||||
* a database query to return the results
|
* a database query to return the results
|
||||||
* etc.
|
* etc.
|
||||||
|
|
||||||
As the execution time is consumed mostly by waiting for <abbr title="Input and Output">I/O</abbr> operations, they call them "I/O bound" operations.
|
As the execution time is consumed mostly by waiting for <abbr title="Input and Output">I/O</abbr> operations, so they call them "I/O bound".
|
||||||
|
|
||||||
It's called "asynchronous" because the computer / program doesn't have to be "synchronized" with the slow task, waiting for the exact moment that the task finishes, while doing nothing, to be able to take the task result and continue the work.
|
It's called "asynchronous" because the computer / program doesn't have to be "synchronized" with the slow task, waiting for the exact moment that the task finishes, while doing nothing, to be able to take the task result and continue the work.
|
||||||
|
|
||||||
|
|
@ -90,6 +92,7 @@ Instead of that, by being an "asynchronous" system, once finished, the task can
|
||||||
|
|
||||||
For "synchronous" (contrary to "asynchronous") they commonly also use the term "sequential", because the computer / program follows all the steps in sequence before switching to a different task, even if those steps involve waiting.
|
For "synchronous" (contrary to "asynchronous") they commonly also use the term "sequential", because the computer / program follows all the steps in sequence before switching to a different task, even if those steps involve waiting.
|
||||||
|
|
||||||
|
|
||||||
### Concurrency and Burgers
|
### Concurrency and Burgers
|
||||||
|
|
||||||
This idea of **asynchronous** code described above is also sometimes called **"concurrency"**. It is different from **"parallelism"**.
|
This idea of **asynchronous** code described above is also sometimes called **"concurrency"**. It is different from **"parallelism"**.
|
||||||
|
|
@ -100,121 +103,123 @@ But the details between *concurrency* and *parallelism* are quite different.
|
||||||
|
|
||||||
To see the difference, imagine the following story about burgers:
|
To see the difference, imagine the following story about burgers:
|
||||||
|
|
||||||
|
|
||||||
### Concurrent Burgers
|
### Concurrent Burgers
|
||||||
|
|
||||||
You go with your crush 😍 to get fast food 🍔, you stand in line while the cashier 💁 takes the orders from the people in front of you.
|
You go with your crush to get fast food, you stand in line while the cashier takes the orders from the people in front of you.
|
||||||
|
|
||||||
Then it's your turn, you place your order of 2 very fancy burgers 🍔 for your crush 😍 and you.
|
Then it's your turn, you place your order of 2 very fancy burgers for your crush and you.
|
||||||
|
|
||||||
You pay 💸.
|
You pay.
|
||||||
|
|
||||||
The cashier 💁 says something to the guy in the kitchen 👨🍳 so he knows he has to prepare your burgers 🍔 (even though he is currently preparing the ones for the previous clients).
|
The cashier says something to the guy in the kitchen so he knows he has to prepare your burgers (even though he is currently preparing the ones for the previous clients).
|
||||||
|
|
||||||
The cashier 💁 gives you the number of your turn.
|
The cashier gives you the number of your turn.
|
||||||
|
|
||||||
While you are waiting, you go with your crush 😍 and pick a table, you sit and talk with your crush 😍 for a long time (as your burgers are very fancy and take some time to prepare ✨🍔✨).
|
While you are waiting, you go with your crush and pick a table, you sit and talk with your crush for a long time (as your burgers are very fancy and take some time to prepare).
|
||||||
|
|
||||||
As you are sitting on the table with your crush 😍, while you wait for the burgers 🍔, you can spend that time admiring how awesome, cute and smart your crush is ✨😍✨.
|
As you are seating on the table with your crush, while you wait for the burgers, you can spend that time admiring how awesome, cute and smart your crush is.
|
||||||
|
|
||||||
While waiting and talking to your crush 😍, from time to time, you check the number displayed on the counter to see if it's your turn already.
|
While waiting and talking to your crush, from time to time, you check the number displayed on the counter to see if it's your turn already.
|
||||||
|
|
||||||
Then at some point, it finally is your turn. You go to the counter, get your burgers 🍔 and come back to the table.
|
Then at some point, it finally is your turn. You go to the counter, get your burgers and come back to the table.
|
||||||
|
|
||||||
You and your crush 😍 eat the burgers 🍔 and have a nice time ✨.
|
You and your crush eat the burgers and have a nice time.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Imagine you are the computer / program 🤖 in that story.
|
Imagine you are the computer / program in that story.
|
||||||
|
|
||||||
While you are at the line, you are just idle 😴, waiting for your turn, not doing anything very "productive". But the line is fast because the cashier 💁 is only taking the orders (not preparing them), so that's fine.
|
While you are at the line, you are just idle, waiting for your turn, not doing anything very "productive". But the line is fast because the cashier is only taking the orders, so that's fine.
|
||||||
|
|
||||||
Then, when it's your turn, you do actual "productive" work 🤓, you process the menu, decide what you want, get your crush's 😍 choice, pay 💸, check that you give the correct bill or card, check that you are charged correctly, check that the order has the correct items, etc.
|
Then, when it's your turn, you do actual "productive" work, you process the menu, decide what you want, get your crush's choice, pay, check that you give the correct bill or card, check that you are charged correctly, check that the order has the correct items, etc.
|
||||||
|
|
||||||
But then, even though you still don't have your burgers 🍔, your work with the cashier 💁 is "on pause" ⏸, because you have to wait 🕙 for your burgers to be ready.
|
But then, even though you still don't have your burgers, your work with the cashier is "on pause", because you have to wait for your burgers to be ready.
|
||||||
|
|
||||||
But as you go away from the counter and sit on the table with a number for your turn, you can switch 🔀 your attention to your crush 😍, and "work" ⏯ 🤓 on that. Then you are again doing something very "productive" 🤓, as is flirting with your crush 😍.
|
But as you go away from the counter and seat on the table with a number for your turn, you can switch your attention to your crush, and "work" on that. Then you are again doing something very "productive", as is flirting with your crush.
|
||||||
|
|
||||||
Then the cashier 💁 says "I'm finished with doing the burgers" 🍔 by putting your number on the counter's display, but you don't jump like crazy immediately when the displayed number changes to your turn number. You know no one will steal your burgers 🍔 because you have the number of your turn, and they have theirs.
|
Then the cashier says "I'm finished with doing the burgers" by putting your number on the counter display, but you don't jump like crazy immediately when the displayed number changes to your turn number. You know no one will steal your burgers because you have the number of your turn, and they have theirs.
|
||||||
|
|
||||||
So you wait for your crush 😍 to finish the story (finish the current work ⏯ / task being processed 🤓), smile gently and say that you are going for the burgers ⏸.
|
So you wait for your crush to finish the story (finish the current work / task being processed), smile gently and say that you are going for the burgers.
|
||||||
|
|
||||||
|
Then you go to the counter, to the initial task that is now finished, pick the burgers, say thanks and take them to the table. That finishes that step / task of interaction with the counter. That in turn, creates a new task, of "eating burgers", but the previous one of "getting burgers" is finished.
|
||||||
|
|
||||||
Then you go to the counter 🔀, to the initial task that is now finished ⏯, pick the burgers 🍔, say thanks and take them to the table. That finishes that step / task of interaction with the counter ⏹. That in turn, creates a new task, of "eating burgers" 🔀 ⏯, but the previous one of "getting burgers" is finished ⏹.
|
|
||||||
|
|
||||||
### Parallel Burgers
|
### Parallel Burgers
|
||||||
|
|
||||||
Now let's imagine these aren't "Concurrent Burgers", but "Parallel Burgers".
|
You go with your crush to get parallel fast food.
|
||||||
|
|
||||||
You go with your crush 😍 to get parallel fast food 🍔.
|
You stand in line while several (let's say 8) cashiers take the orders from the people in front of you.
|
||||||
|
|
||||||
You stand in line while several (let's say 8) cashiers that at the same time are cooks 👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳 take the orders from the people in front of you.
|
Everyone before you is waiting for their burgers to be ready before leaving the counter because each of the 8 cashiers goes himself and preparers the burger right away before getting the next order.
|
||||||
|
|
||||||
Everyone before you is waiting 🕙 for their burgers 🍔 to be ready before leaving the counter because each of the 8 cashiers goes himself and prepares the burger right away before getting the next order.
|
Then it's finally your turn, you place your order of 2 very fancy burgers for your crush and you.
|
||||||
|
|
||||||
Then it's finally your turn, you place your order of 2 very fancy burgers 🍔 for your crush 😍 and you.
|
You pay.
|
||||||
|
|
||||||
You pay 💸.
|
The cashier goes to the kitchen.
|
||||||
|
|
||||||
The cashier goes to the kitchen 👨🍳.
|
You wait, standing in front of the counter, so that no one else takes your burgers before you, as there are no numbers for turns.
|
||||||
|
|
||||||
You wait, standing in front of the counter 🕙, so that no one else takes your burgers 🍔 before you do, as there are no numbers for turns.
|
As you and your crush are busy not letting anyone get in front of you and take your burgers whenever they arrive, you cannot pay attention to your crush.
|
||||||
|
|
||||||
As you and your crush 😍 are busy not letting anyone get in front of you and take your burgers whenever they arrive 🕙, you cannot pay attention to your crush 😞.
|
This is "synchronous" work, you are "synchronized" with the cashier/cook. You have to wait and be there at the exact moment that the cashier/cook finishes the burgers and gives them to you, or otherwise, someone else might take them.
|
||||||
|
|
||||||
This is "synchronous" work, you are "synchronized" with the cashier/cook 👨🍳. You have to wait 🕙 and be there at the exact moment that the cashier/cook 👨🍳 finishes the burgers 🍔 and gives them to you, or otherwise, someone else might take them.
|
Then your cashier/cook finally comes back with your burgers, after a long time waiting there in front of the counter.
|
||||||
|
|
||||||
Then your cashier/cook 👨🍳 finally comes back with your burgers 🍔, after a long time waiting 🕙 there in front of the counter.
|
You take your burgers and go to the table with your crush.
|
||||||
|
|
||||||
You take your burgers 🍔 and go to the table with your crush 😍.
|
You just eat them, and you are done.
|
||||||
|
|
||||||
You just eat them, and you are done 🍔 ⏹.
|
There was not much talk or flirting as most of the time was spent waiting in front of the counter.
|
||||||
|
|
||||||
There was not much talk or flirting as most of the time was spent waiting 🕙 in front of the counter 😞.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
In this scenario of the parallel burgers, you are a computer / program 🤖 with two processors (you and your crush 😍), both waiting 🕙 and dedicating their attention ⏯ to be "waiting on the counter" 🕙 for a long time.
|
In this scenario of the parallel burgers, you are a computer / program with two processors (you and your crush), both waiting and dedicating their attention to be "waiting on the counter" for a long time.
|
||||||
|
|
||||||
The fast food store has 8 processors (cashiers/cooks) 👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳. While the concurrent burgers store might have had only 2 (one cashier and one cook) 💁 👨🍳.
|
The fast food store has 8 processors (cashiers/cooks). While the concurrent burgers store might have had only 2 (one cashier and one cook).
|
||||||
|
|
||||||
But still, the final experience is not the best 😞.
|
But still, the final experience is not the best.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
This would be the parallel equivalent story for burgers 🍔.
|
This would be the parallel equivalent story for burgers.
|
||||||
|
|
||||||
For a more "real life" example of this, imagine a bank.
|
For a more "real life" example of this, imagine a bank.
|
||||||
|
|
||||||
Up to recently, most of the banks had multiple cashiers 👨💼👨💼👨💼👨💼 and a big line 🕙🕙🕙🕙🕙🕙🕙🕙.
|
Up to recently, most of the banks had multiple cashiers and a big line.
|
||||||
|
|
||||||
All of the cashiers doing all the work with one client after the other 👨💼⏯.
|
All of the cashiers doing all the work with one client after the other.
|
||||||
|
|
||||||
And you have to wait 🕙 in the line for a long time or you lose your turn.
|
And you have to wait in the line for a long time or you lose your turn.
|
||||||
|
|
||||||
|
You probably wouldn't want to take your crush with you to do errands at the bank.
|
||||||
|
|
||||||
You probably wouldn't want to take your crush 😍 with you to do errands at the bank 🏦.
|
|
||||||
|
|
||||||
### Burger Conclusion
|
### Burger Conclusion
|
||||||
|
|
||||||
In this scenario of "fast food burgers with your crush", as there is a lot of waiting 🕙, it makes a lot more sense to have a concurrent system ⏸🔀⏯.
|
In this scenario of "fast food burgers with your crush", as there is a lot of waiting, it makes a lot more sense to have a concurrent system.
|
||||||
|
|
||||||
This is the case for most of the web applications.
|
This is the case for most of the web applications.
|
||||||
|
|
||||||
Many, many users, but your server is waiting 🕙 for their not-so-good connection to send their requests.
|
Many, many users, but your server is waiting for their not-so-good connection to send their requests.
|
||||||
|
|
||||||
And then waiting 🕙 again for the responses to come back.
|
And then waiting again for the responses to come back.
|
||||||
|
|
||||||
This "waiting" 🕙 is measured in microseconds, but still, summing it all, it's a lot of waiting in the end.
|
This "waiting" is measured in microseconds, but still, summing it all, it's a lot of waiting in the end.
|
||||||
|
|
||||||
That's why it makes a lot of sense to use asynchronous ⏸🔀⏯ code for web APIs.
|
That's why it makes a lot of sense to use asynchronous code for web APIs.
|
||||||
|
|
||||||
Most of the existing popular Python frameworks (including Flask and Django) were created before the new asynchronous features in Python existed. So, the ways they can be deployed support parallel execution and an older form of asynchronous execution that is not as powerful as the new capabilities.
|
Most of the existing popular Python frameworks (including Flask and Django) were created before the new asynchronous features in Python existed. So, the ways they can be deployed support parallel execution and an older form of asynchronous execution that is not as powerful as the new capabilities.
|
||||||
|
|
||||||
Even though the main specification for asynchronous web Python (ASGI) was developed at Django, to add support for WebSockets.
|
Even though the main specification for asynchronous web Python (ASGI) was developed at Django, to add support for WebSockets.
|
||||||
|
|
||||||
That kind of asynchronicity is what made NodeJS popular (even though NodeJS is not parallel) and that's the strength of Go as a programming language.
|
That kind of asynchronicity is what made NodeJS popular (even though NodeJS is not parallel) and that's the strength of Go as a programing language.
|
||||||
|
|
||||||
And that's the same level of performance you get with **FastAPI**.
|
And that's the same level of performance</a> you get with **FastAPI**.
|
||||||
|
|
||||||
|
And as you can have parallelism and asynchronicity at the same time, you get higher performance than most of the tested NodeJS frameworks and on par with Go, which is a compiled language closer to C <a href="https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=query&l=zijmkf-1" target="_blank">(all thanks to Starlette)</a>.
|
||||||
|
|
||||||
And as you can have parallelism and asynchronicity at the same time, you get higher performance than most of the tested NodeJS frameworks and on par with Go, which is a compiled language closer to C <a href="https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=query&l=zijmkf-1" class="external-link" target="_blank">(all thanks to Starlette)</a>.
|
|
||||||
|
|
||||||
### Is concurrency better than parallelism?
|
### Is concurrency better than parallelism?
|
||||||
|
|
||||||
|
|
@ -230,13 +235,13 @@ So, to balance that out, imagine the following short story:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
There's no waiting 🕙 anywhere, just a lot of work to be done, on multiple places of the house.
|
There's no waiting anywhere, just a lot of work to be done, on multiple places of the house.
|
||||||
|
|
||||||
You could have turns as in the burgers example, first the living room, then the kitchen, but as you are not waiting 🕙 for anything, just cleaning and cleaning, the turns wouldn't affect anything.
|
You could have turns as in the burgers example, first the living room, then the kitchen, but as you are not waiting for anything, just cleaning and cleaning, the turns wouldn't affect anything.
|
||||||
|
|
||||||
It would take the same amount of time to finish with or without turns (concurrency) and you would have done the same amount of work.
|
It would take the same amount of time to finish with or without turns (concurrency) and you would have done the same amount of work.
|
||||||
|
|
||||||
But in this case, if you could bring the 8 ex-cashier/cooks/now-cleaners 👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳, and each one of them (plus you) could take a zone of the house to clean it, you could do all the work in **parallel**, with the extra help, and finish much sooner.
|
But in this case, if you could bring the 8 ex-cashier/cooks/now-cleaners, and each one of them (plus you) could take a zone of the house to clean it, you could do all the work in **parallel**, with the extra help, and finish much sooner.
|
||||||
|
|
||||||
In this scenario, each one of the cleaners (including you) would be a processor, doing their part of the job.
|
In this scenario, each one of the cleaners (including you) would be a processor, doing their part of the job.
|
||||||
|
|
||||||
|
|
@ -248,11 +253,12 @@ Common examples of CPU bound operations are things that require complex math pro
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
* **Audio** or **image processing**.
|
* **Audio** or **image processing**
|
||||||
* **Computer vision**: an image is composed of millions of pixels, each pixel has 3 values / colors, processing that normally requires computing something on those pixels, all at the same time.
|
* **Computer vision**: an image is composed of millions of pixels, each pixel has 3 values / colors, processing that normally requires computing something on those pixels, all at the same time)
|
||||||
* **Machine Learning**: it normally requires lots of "matrix" and "vector" multiplications. Think of a huge spreadsheet with numbers and multiplying all of them together at the same time.
|
* **Machine Learning**: it normally requires lots of "matrix" and "vector" multiplications. Think of a huge spreadsheet with numbers and multiplying all of them together at the same time.
|
||||||
* **Deep Learning**: this is a sub-field of Machine Learning, so, the same applies. It's just that there is not a single spreadsheet of numbers to multiply, but a huge set of them, and in many cases, you use a special processor to build and / or use those models.
|
* **Deep Learning**: this is a sub-field of Machine Learning, so, the same applies. It's just that there is not a single spreadsheet of numbers to multiply, but a huge set of them, and in many cases, you use a special processor to build and / or use those models.
|
||||||
|
|
||||||
|
|
||||||
### Concurrency + Parallelism: Web + Machine Learning
|
### Concurrency + Parallelism: Web + Machine Learning
|
||||||
|
|
||||||
With **FastAPI** you can take the advantage of concurrency that is very common for web development (the same main attractive of NodeJS).
|
With **FastAPI** you can take the advantage of concurrency that is very common for web development (the same main attractive of NodeJS).
|
||||||
|
|
@ -261,11 +267,12 @@ But you can also exploit the benefits of parallelism and multiprocessing (having
|
||||||
|
|
||||||
That, plus the simple fact that Python is the main language for **Data Science**, Machine Learning and especially Deep Learning, make FastAPI a very good match for Data Science / Machine Learning web APIs and applications (among many others).
|
That, plus the simple fact that Python is the main language for **Data Science**, Machine Learning and especially Deep Learning, make FastAPI a very good match for Data Science / Machine Learning web APIs and applications (among many others).
|
||||||
|
|
||||||
To see how to achieve this parallelism in production see the section about [Deployment](deployment/index.md){.internal-link target=_blank}.
|
To see how to achieve this parallelism in production see the section about [Deployment](deployment.md).
|
||||||
|
|
||||||
|
|
||||||
## `async` and `await`
|
## `async` and `await`
|
||||||
|
|
||||||
Modern versions of Python have a very intuitive way to define asynchronous code. This makes it look just like normal "sequential" code and do the "awaiting" for you at the right moments.
|
Modern versions of python have a very intuitive way to define asynchronous code. This makes it look just like normal "sequential" code and do the "awaiting" for you at the right moments.
|
||||||
|
|
||||||
When there is an operation that will require waiting before giving the results and has support for these new Python features, you can code it like:
|
When there is an operation that will require waiting before giving the results and has support for these new Python features, you can code it like:
|
||||||
|
|
||||||
|
|
@ -273,7 +280,7 @@ When there is an operation that will require waiting before giving the results a
|
||||||
burgers = await get_burgers(2)
|
burgers = await get_burgers(2)
|
||||||
```
|
```
|
||||||
|
|
||||||
The key here is the `await`. It tells Python that it has to wait ⏸ for `get_burgers(2)` to finish doing its thing 🕙 before storing the results in `burgers`. With that, Python will know that it can go and do something else 🔀 ⏯ in the meanwhile (like receiving another request).
|
The key here is the `await`. It tells Python that it has to wait for `get_burgers(2)` to finish doing its thing before storing the results in `burgers`. With that, Python will know that it can go and do something else in the meanwhile (like receiving another request).
|
||||||
|
|
||||||
For `await` to work, it has to be inside a function that supports this asynchronicity. To do that, you just declare it with `async def`:
|
For `await` to work, it has to be inside a function that supports this asynchronicity. To do that, you just declare it with `async def`:
|
||||||
|
|
||||||
|
|
@ -292,7 +299,7 @@ def get_sequential_burgers(number: int):
|
||||||
return burgers
|
return burgers
|
||||||
```
|
```
|
||||||
|
|
||||||
With `async def`, Python knows that, inside that function, it has to be aware of `await` expressions, and that it can "pause" ⏸ the execution of that function and go do something else 🔀 before coming back.
|
With `async def`, Python knows that, inside that function, it has to be aware of `await` expressions, and that it can "pause" the execution of that function and go do something else before coming back.
|
||||||
|
|
||||||
When you want to call an `async def` function, you have to "await" it. So, this won't work:
|
When you want to call an `async def` function, you have to "await" it. So, this won't work:
|
||||||
|
|
||||||
|
|
@ -303,9 +310,9 @@ burgers = get_burgers(2)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
So, if you are using a library that tells you that you can call it with `await`, you need to create the *path operation functions* that uses it with `async def`, like in:
|
So, if you are using a library that tells you that you can call it with `await`, you need to create the path operation functions that uses it with `async def`, like in:
|
||||||
|
|
||||||
```Python hl_lines="2-3"
|
```Python hl_lines="2 3"
|
||||||
@app.get('/burgers')
|
@app.get('/burgers')
|
||||||
async def read_burgers():
|
async def read_burgers():
|
||||||
burgers = await get_burgers(2)
|
burgers = await get_burgers(2)
|
||||||
|
|
@ -320,9 +327,10 @@ But at the same time, functions defined with `async def` have to be "awaited". S
|
||||||
|
|
||||||
So, about the egg and the chicken, how do you call the first `async` function?
|
So, about the egg and the chicken, how do you call the first `async` function?
|
||||||
|
|
||||||
If you are working with **FastAPI** you don't have to worry about that, because that "first" function will be your *path operation function*, and FastAPI will know how to do the right thing.
|
If you are working with **FastAPI** you don't have to worry about that, because that "first" function will be your path operation function, and FastAPI will know how to do the right thing.
|
||||||
|
|
||||||
|
But if you want to use `async` / `await` without FastAPI, <a href="https://docs.python.org/3/library/asyncio-task.html#coroutine" target="_blank">check the official Python docs</a>.
|
||||||
|
|
||||||
But if you want to use `async` / `await` without FastAPI, <a href="https://docs.python.org/3/library/asyncio-task.html#coroutine" class="external-link" target="_blank">check the official Python docs</a>.
|
|
||||||
|
|
||||||
### Other forms of asynchronous code
|
### Other forms of asynchronous code
|
||||||
|
|
||||||
|
|
@ -334,13 +342,14 @@ This same syntax (or almost identical) was also included recently in modern vers
|
||||||
|
|
||||||
But before that, handling asynchronous code was quite more complex and difficult.
|
But before that, handling asynchronous code was quite more complex and difficult.
|
||||||
|
|
||||||
In previous versions of Python, you could have used threads or <a href="https://www.gevent.org/" class="external-link" target="_blank">Gevent</a>. But the code is way more complex to understand, debug, and think about.
|
In previous versions of Python, you could have used threads or <a href="http://www.gevent.org/" target="_blank">Gevent</a>. But the code is way more complex to understand, debug, and think about.
|
||||||
|
|
||||||
|
In previous versions of NodeJS / Browser JavaScript, you would have used "callbacks". Which lead to <a href="http://callbackhell.com/" target="_blank">callback hell</a>.
|
||||||
|
|
||||||
In previous versions of NodeJS / Browser JavaScript, you would have used "callbacks". Which leads to <a href="http://callbackhell.com/" class="external-link" target="_blank">callback hell</a>.
|
|
||||||
|
|
||||||
## Coroutines
|
## Coroutines
|
||||||
|
|
||||||
**Coroutine** is just the very fancy term for the thing returned by an `async def` function. Python knows that it is something like a function that it can start and that it will end at some point, but that it might be paused ⏸ internally too, whenever there is an `await` inside of it.
|
**Coroutine** is just the very fancy term for the thing returned by an `async def` function. Python knows that it is something like a function that it can start and that it will end at some point, but that it might be paused internally too, whenever there is an `await` inside of it.
|
||||||
|
|
||||||
But all this functionality of using asynchronous code with `async` and `await` is many times summarized as using "coroutines". It is comparable to the main key feature of Go, the "Goroutines".
|
But all this functionality of using asynchronous code with `async` and `await` is many times summarized as using "coroutines". It is comparable to the main key feature of Go, the "Goroutines".
|
||||||
|
|
||||||
|
|
@ -350,10 +359,11 @@ Let's see the same phrase from above:
|
||||||
|
|
||||||
> Modern versions of Python have support for **"asynchronous code"** using something called **"coroutines"**, with **`async` and `await`** syntax.
|
> Modern versions of Python have support for **"asynchronous code"** using something called **"coroutines"**, with **`async` and `await`** syntax.
|
||||||
|
|
||||||
That should make more sense now. ✨
|
That should make more sense now.
|
||||||
|
|
||||||
All that is what powers FastAPI (through Starlette) and what makes it have such an impressive performance.
|
All that is what powers FastAPI (through Starlette) and what makes it have such an impressive performance.
|
||||||
|
|
||||||
|
|
||||||
## Very Technical Details
|
## Very Technical Details
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
@ -367,9 +377,9 @@ All that is what powers FastAPI (through Starlette) and what makes it have such
|
||||||
|
|
||||||
When you declare a *path operation function* with normal `def` instead of `async def`, it is run in an external threadpool that is then awaited, instead of being called directly (as it would block the server).
|
When you declare a *path operation function* with normal `def` instead of `async def`, it is run in an external threadpool that is then awaited, instead of being called directly (as it would block the server).
|
||||||
|
|
||||||
If you are coming from another async framework that does not work in the way described above and you are used to define trivial compute-only *path operation functions* with plain `def` for a tiny performance gain (about 100 nanoseconds), please note that in **FastAPI** the effect would be quite opposite. In these cases, it's better to use `async def` unless your *path operation functions* use code that performs blocking <abbr title="Input/Output: disk reading or writing, network communications.">I/O</abbr>.
|
If you are coming from another async framework that does not work in the way described above and you are used to define trivial compute-only *path operation functions* with plain `def` for a tiny performance gain (about 100 nanoseconds), please note that in **FastAPI** the effect would be quite opposite. In these cases, it's better to use `async def` unless your *path operation functions* use code that performs blocking <abbr title="Input/Output: disk reading or writing, network communications.">IO</abbr>.
|
||||||
|
|
||||||
Still, in both situations, chances are that **FastAPI** will [still be faster](/#performance){.internal-link target=_blank} than (or at least comparable to) your previous framework.
|
Still, in both situations, chances are that **FastAPI** will <a href="https://fastapi.tiangolo.com/#performance" target="_blank">still be faster</a> than (or at least comparable to) your previous framework.
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
|
|
@ -377,7 +387,7 @@ The same applies for dependencies. If a dependency is a standard `def` function
|
||||||
|
|
||||||
### Sub-dependencies
|
### Sub-dependencies
|
||||||
|
|
||||||
You can have multiple dependencies and sub-dependencies requiring each other (as parameters of the function definitions), some of them might be created with `async def` and some with normal `def`. It would still work, and the ones created with normal `def` would be called on an external thread (from the threadpool) instead of being "awaited".
|
You can have multiple dependencies and sub-dependencies requiring each other (as parameters of the function definitions), some of them might be created with `async def` and some with normal `def`. It would still work, and the ones created with normal `def` would be called on an external thread instead of being "awaited".
|
||||||
|
|
||||||
### Other utility functions
|
### Other utility functions
|
||||||
|
|
||||||
|
|
@ -385,7 +395,7 @@ Any other utility function that you call directly can be created with normal `de
|
||||||
|
|
||||||
This is in contrast to the functions that FastAPI calls for you: *path operation functions* and dependencies.
|
This is in contrast to the functions that FastAPI calls for you: *path operation functions* and dependencies.
|
||||||
|
|
||||||
If your utility function is a normal function with `def`, it will be called directly (as you write it in your code), not in a threadpool, if the function is created with `async def` then you should `await` for that function when you call it in your code.
|
If your utility function is a normal function with `def`, it will be called directly (as you write it in your code), not in a threadpool, if the function is created with `async def` then you should await for that function when you call it in your code.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
# Benchmarks
|
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
||||||
|
|
||||||
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
|
||||||
|
|
||||||
But when checking benchmarks and comparisons you should have the following in mind.
|
But when checking benchmarks and comparisons you should have the following in mind.
|
||||||
|
|
||||||
|
|
@ -10,7 +8,7 @@ When you check the benchmarks, it is common to see several tools of different ty
|
||||||
|
|
||||||
Specifically, to see Uvicorn, Starlette and FastAPI compared together (among many other tools).
|
Specifically, to see Uvicorn, Starlette and FastAPI compared together (among many other tools).
|
||||||
|
|
||||||
The simpler the problem solved by the tool, the better performance it will get. And most of the benchmarks don't test the additional features provided by the tool.
|
The simplest the problem solved by the tool, the better performance it will get. And most of the benchmarks don't test the additional features provided by the tool.
|
||||||
|
|
||||||
The hierarchy is like:
|
The hierarchy is like:
|
||||||
|
|
||||||
123
docs/contributing.md
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
First, you might want to see the basic ways to <a href="https://fastapi.tiangolo.com/help-fastapi/" target="_blank">help FastAPI and get help</a>.
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment.
|
||||||
|
|
||||||
|
|
||||||
|
### Pipenv
|
||||||
|
|
||||||
|
If you are using <a href="https://pipenv.readthedocs.io/en/latest/" target="_blank">Pipenv</a>, you can create a virtual environment and install the packages with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipenv install --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can activate that virtual environment with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipenv shell
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### No Pipenv
|
||||||
|
|
||||||
|
If you are not using Pipenv, you can create a virtual environment with your preferred tool, and install the packages listed in the file `Pipfile`.
|
||||||
|
|
||||||
|
|
||||||
|
### Flit
|
||||||
|
|
||||||
|
**FastAPI** uses <a href="https://flit.readthedocs.io/en/latest/index.html" target="_blank">Flit</a> to build, package and publish the project.
|
||||||
|
|
||||||
|
If you installed the development dependencies with one of the methods above, you already have the `flit` command.
|
||||||
|
|
||||||
|
To install your local version of FastAPI as a package in your local environment, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flit install --symlink
|
||||||
|
```
|
||||||
|
|
||||||
|
It will install your local FastAPI in your local environment.
|
||||||
|
|
||||||
|
|
||||||
|
#### Using your local FastAPI
|
||||||
|
|
||||||
|
If you create a Python file that imports and uses FastAPI, and run it with the Python from your local environment, it will use your local FastAPI source code.
|
||||||
|
|
||||||
|
And if you update that local FastAPI source code, as it is installed with `--symlink`, when you run that Python file again, it will use the fresh version of FastAPI you just edited.
|
||||||
|
|
||||||
|
That way, you don't have to "install" your local version to be able to test every change.
|
||||||
|
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
There is a script that you can run that will format and clean all your code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/lint.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
It will also auto-sort all your imports.
|
||||||
|
|
||||||
|
For it to sort them correctly, you need to have FastAPI installed locally in your environment, with the command in the section above:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flit install --symlink
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
|
||||||
|
The documentation uses <a href="https://www.mkdocs.org/" target="_blank">MkDocs</a>.
|
||||||
|
|
||||||
|
All the documentation is in Markdown format in the directory `./docs`.
|
||||||
|
|
||||||
|
Many of the tutorials have blocks of code.
|
||||||
|
|
||||||
|
In most of the cases, these blocks of code are actual complete applications that can be run as is.
|
||||||
|
|
||||||
|
In fact, those blocks of code are not written inside the Markdown, they are Python files in the `./docs/src/` directory.
|
||||||
|
|
||||||
|
And those Python files are included/injected in the documentation when generating the site.
|
||||||
|
|
||||||
|
|
||||||
|
#### Docs for tests
|
||||||
|
|
||||||
|
Most of the tests actually run against the example source files in the documentation.
|
||||||
|
|
||||||
|
This helps making sure that:
|
||||||
|
|
||||||
|
* The documentation is up to date.
|
||||||
|
* The documentation examples can be run as is.
|
||||||
|
* Most of the features are covered by the documentation, ensured by the coverage tests.
|
||||||
|
|
||||||
|
During local development, there is a script that builds the site and checks for any changes, live-reloading:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/docs-live.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
It will serve the documentation on `http://0.0.0.0:8008`.
|
||||||
|
|
||||||
|
That way, you can edit the documentation/source files and see the changes live.
|
||||||
|
|
||||||
|
#### Apps and docs at the same time
|
||||||
|
|
||||||
|
And if you run the examples with, e.g.:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn tutorial001:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
as Uvicorn by default will use the port `8000`, the documentation on port `8008` won't clash.
|
||||||
|
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
There is a script that you can run locally to test all the code and generate coverage reports in HTML:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/test-cov-html.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This command generates a directory `./htmlcov/`, if you open the file `./htmlcov/index.html` in your browser, you can explore interactively the regions of code that are covered by the tests, and notice if there is any region missing.
|
||||||
274
docs/deployment.md
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
You can use <a href="https://www.docker.com/" target="_blank">**Docker**</a> for deployment. It has several advantages like security, replicability, development simplicity, etc.
|
||||||
|
|
||||||
|
In this section you'll see instructions and links to guides to know how to:
|
||||||
|
|
||||||
|
* Make your **FastAPI** application a Docker image/container with maximum performance. In about **5 min**.
|
||||||
|
* (Optionally) understand what you, as a developer, need to know about HTTPS.
|
||||||
|
* Set up a Docker Swarm mode cluster with automatic HTTPS, even on a simple $5 USD/month server. In about **20 min**.
|
||||||
|
* Generate and deploy a full **FastAPI** application, using your Docker Swarm cluster, with HTTPS, etc. In about **10 min**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
You can also easily use **FastAPI** in a standard server directly too (without Docker).
|
||||||
|
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
If you are using Docker, you can use the official Docker image:
|
||||||
|
|
||||||
|
### <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>
|
||||||
|
|
||||||
|
This image has an "auto-tuning" mechanism included, so that you can just add your code and get very high performance automatically. And without making sacrifices.
|
||||||
|
|
||||||
|
But you can still change and update all the configurations with environment variables or configuration files.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
To see all the configurations and options, go to the Docker image page: <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>.
|
||||||
|
|
||||||
|
|
||||||
|
### Create a `Dockerfile`
|
||||||
|
|
||||||
|
* Go to your project directory.
|
||||||
|
* Create a `Dockerfile` with:
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
|
||||||
|
|
||||||
|
COPY ./app /app
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Bigger Applications
|
||||||
|
|
||||||
|
If you followed the section about creating <a href="https://fastapi.tiangolo.com/tutorial/bigger-applications/" target="_blank">Bigger Applications with Multiple Files
|
||||||
|
</a>, your `Dockerfile` might instead look like:
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
|
||||||
|
|
||||||
|
COPY ./app /app/app
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Raspberry Pi and other architectures
|
||||||
|
|
||||||
|
If you are running Docker in a Raspberry Pi (that has an ARM processor) or any other architecture, you can create a `Dockerfile` from scratch, based on a Python base image (that is multi-architecture) and use Uvicorn alone.
|
||||||
|
|
||||||
|
In this case, your `Dockerfile` could look like:
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
FROM python:3.7
|
||||||
|
|
||||||
|
RUN pip install fastapi uvicorn
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
COPY ./app /app
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create the **FastAPI** Code
|
||||||
|
|
||||||
|
* Create an `app` directory and enter in it.
|
||||||
|
* Create a `main.py` file with:
|
||||||
|
|
||||||
|
```Python
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def read_root():
|
||||||
|
return {"Hello": "World"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/items/{item_id}")
|
||||||
|
def read_item(item_id: int, q: str = None):
|
||||||
|
return {"item_id": item_id, "q": q}
|
||||||
|
```
|
||||||
|
|
||||||
|
* You should now have a directory structure like:
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── app
|
||||||
|
│ └── main.py
|
||||||
|
└── Dockerfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build the Docker image
|
||||||
|
|
||||||
|
* Go to the project directory (in where your `Dockerfile` is, containing your `app` directory).
|
||||||
|
* Build your FastAPI image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t myimage .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start the Docker container
|
||||||
|
|
||||||
|
* Run a container based on your image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name mycontainer -p 80:80 myimage
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you have an optimized FastAPI server in a Docker container. Auto-tuned for your current server (and number of CPU cores).
|
||||||
|
|
||||||
|
|
||||||
|
### Check it
|
||||||
|
|
||||||
|
You should be able to check it in your Docker container's URL, for example: <a href="http://192.168.99.100/items/5?q=somequery" target="_blank">http://192.168.99.100/items/5?q=somequery</a> or <a href="http://127.0.0.1/items/5?q=somequery" target="_blank">http://127.0.0.1/items/5?q=somequery</a> (or equivalent, using your Docker host).
|
||||||
|
|
||||||
|
You will see something like:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{"item_id": 5, "q": "somequery"}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Interactive API docs
|
||||||
|
|
||||||
|
Now you can go to <a href="http://192.168.99.100/docs" target="_blank">http://192.168.99.100/docs</a> or <a href="http://127.0.0.1/docs" target="_blank">http://127.0.0.1/docs</a> (or equivalent, using your Docker host).
|
||||||
|
|
||||||
|
You will see the automatic interactive API documentation (provided by <a href="https://github.com/swagger-api/swagger-ui" target="_blank">Swagger UI</a>):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### Alternative API docs
|
||||||
|
|
||||||
|
And you can also go to <a href="http://192.168.99.100/redoc" target="_blank">http://192.168.99.100/redoc</a> or <a href="http://127.0.0.1/redoc" target="_blank">http://127.0.0.1/redoc</a> (or equivalent, using your Docker host).
|
||||||
|
|
||||||
|
You will see the alternative automatic documentation (provided by <a href="https://github.com/Rebilly/ReDoc" target="_blank">ReDoc</a>):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## HTTPS
|
||||||
|
|
||||||
|
### About HTTPS
|
||||||
|
|
||||||
|
It is easy to assume that HTTPS is something that is just "enabled" or not.
|
||||||
|
|
||||||
|
But it is way more complex than that.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
If you are in a hurry or don't care, continue with the next section for step by step instructions to set everything up.
|
||||||
|
|
||||||
|
To learn the basics of HTTPS, from a consumer perspective, check <a href="https://howhttps.works/" target="_blank">https://howhttps.works/</a>.
|
||||||
|
|
||||||
|
Now, from a developer's perspective, here are several things to have in mind while thinking about HTTPS:
|
||||||
|
|
||||||
|
* For HTTPS, the server needs to have "certificates" generated by a third party.
|
||||||
|
* Those certificates are actually acquired from the third-party, not "generated".
|
||||||
|
* Certificates have a lifetime.
|
||||||
|
* They expire.
|
||||||
|
* And then they need to be renewed, acquired again from the third party.
|
||||||
|
* The encryption of the connection happens at the TCP level.
|
||||||
|
* That's one layer below HTTP.
|
||||||
|
* So, the certificate and encryption handling is done before HTTP.
|
||||||
|
* TCP doesn't know about "domains". Only about IP addresses.
|
||||||
|
* The information about the specific domain requested goes in the HTTP data.
|
||||||
|
* The HTTPS certificates "certificate" a certain domain, but the protocol and encryption happen at the TCP level, before knowing which domain is being dealt with.
|
||||||
|
* By default, that would mean that you can only have one HTTPS certificate per IP address.
|
||||||
|
* No matter how big is your server and how small each application you have there might be. But...
|
||||||
|
* There's an extension to the TLS protocol (the one handling the encryption at the TCP level, before HTTP) called <a href="https://en.wikipedia.org/wiki/Server_Name_Indication" target="_blank"><abbr title="Server Name Indication">SNI</abbr></a>.
|
||||||
|
* This SNI extension allows one single server (with a single IP address) to have several HTTPS certificates and server multiple HTTPS domains/applications.
|
||||||
|
* For this to work, a single component (program) running in the server, listening in the public IP address, must have all the HTTPS certificates in the server.
|
||||||
|
* After having a secure connection, the communication protocol is the same HTTP.
|
||||||
|
* It goes encrypted, but the encrypted contents are the same HTTP protocol.
|
||||||
|
|
||||||
|
|
||||||
|
It is a common practice to have one program/HTTP server running in the server (the machine, host, etc) and managing all the HTTPS parts, sending the decrypted HTTP requests to the actual HTTP application running in the same server (the **FastAPI** application, in this case), take the HTTP response from the application, encrypt it using the appropriate certificate and sending it back to the client using HTTPS. This server is ofter called a <a href="https://en.wikipedia.org/wiki/TLS_termination_proxy" target="_blank">TLS Termination Proxy</a>.
|
||||||
|
|
||||||
|
|
||||||
|
### Let's Encrypt
|
||||||
|
|
||||||
|
Up to some years ago, these HTTPS certificates were sold by trusted third-parties.
|
||||||
|
|
||||||
|
The process to acquire one of these certificates used to be cumbersome, require quite some paperwork and the certificates were quite expensive.
|
||||||
|
|
||||||
|
But then <a href="https://letsencrypt.org/" target="_blank">Let's Encrypt</a> was created.
|
||||||
|
|
||||||
|
It is a project from the Linux Foundation. It provides HTTPS certificates for free. In an automated way. These certificates use all the standard cryptographic security, and are short lived (about 3 months), so, the security is actually increased, by reducing their lifespan.
|
||||||
|
|
||||||
|
The domains are securely verified and the certificates are generated automatically. This also allows automatizing the renewal of these certificates.
|
||||||
|
|
||||||
|
The idea is to automatize the acquisition and renewal of these certificates, so that you can have secure HTTPS, free, forever.
|
||||||
|
|
||||||
|
|
||||||
|
### Traefik
|
||||||
|
|
||||||
|
<a href="https://traefik.io/" target="_blank">Traefik</a> is a high performance reverse proxy / load balancer. It can do the "TLS Termination Proxy" job (apart from other features).
|
||||||
|
|
||||||
|
It has integration with Let's Encrypt. So, it can handle all the HTTPS parts, including certificate acquisition and renewal.
|
||||||
|
|
||||||
|
It also has integrations with Docker. So, you can declare your domains in each application configurations and have it read those configurations, generate the HTTPS certificates and serve HTTPS to your application, all automatically. Without requiring any change in its configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
With this information and tools, continue with the next section to combine everything.
|
||||||
|
|
||||||
|
|
||||||
|
## Docker Swarm mode cluster with Traefik and HTTPS
|
||||||
|
|
||||||
|
You can have a Docker Swarm mode cluster set up in minutes (about 20 min) with a main Traefik handling HTTPS (including certificate acquisition and renewal).
|
||||||
|
|
||||||
|
By using Docker Swarm mode, you can start with a "cluster" of a single machine (it can even be a $5 USD / month server) and then you can grow as much as you need adding more servers.
|
||||||
|
|
||||||
|
To set up a Docker Swarm Mode cluster with Traefik and HTTPS handling, follow this guide:
|
||||||
|
|
||||||
|
### <a href="https://medium.com/@tiangolo/docker-swarm-mode-and-traefik-for-a-https-cluster-20328dba6232" target="_blank">Docker Swarm Mode and Traefik for an HTTPS cluster</a>.
|
||||||
|
|
||||||
|
|
||||||
|
### Deploy a FastAPI application
|
||||||
|
|
||||||
|
The easiest way to set everything up, would be using the <a href="/project-generation/" target="_blank">FastAPI project generator</a>.
|
||||||
|
|
||||||
|
It is designed to be integrated with this Docker Swarm cluster with Traefik and HTTPS described above.
|
||||||
|
|
||||||
|
You can generate a project in about 2 min.
|
||||||
|
|
||||||
|
The generated project has instructions to deploy it, doing it takes other 2 min.
|
||||||
|
|
||||||
|
|
||||||
|
## Alternatively, deploy **FastAPI** without Docker
|
||||||
|
|
||||||
|
You can deploy **FastAPI** directly without Docker too.
|
||||||
|
|
||||||
|
You just need to install an ASGI compatible server like:
|
||||||
|
|
||||||
|
* <a href="https://www.uvicorn.org/" target="_blank">Uvicorn</a>, a lightning-fast ASGI server, built on uvloop and httptools.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install uvicorn
|
||||||
|
```
|
||||||
|
|
||||||
|
* <a href="https://gitlab.com/pgjones/hypercorn" target="_blank">Hypercorn</a>, an ASGI server also compatible with HTTP/2.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install hypercorn
|
||||||
|
```
|
||||||
|
|
||||||
|
...or any other ASGI server.
|
||||||
|
|
||||||
|
And run your application the same way you have done in the tutorials, but without the `--reload` option, e.g.:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn main:app --host 0.0.0.0 --port 80
|
||||||
|
```
|
||||||
|
|
||||||
|
or with Hypercorn:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hypercorn main:app --bind 0.0.0.0:80
|
||||||
|
```
|
||||||
|
|
||||||
|
You might want to set up some tooling to make sure it is restarted automatically if it stops.
|
||||||
|
|
||||||
|
You might also want to install <a href="https://gunicorn.org/" target="_blank">Gunicorn</a> and <a href="https://www.uvicorn.org/#running-with-gunicorn" target="_blank">use it as a manager for Uvicorn</a>, or use Hypercorn with multiple workers.
|
||||||
|
|
||||||
|
Making sure to fine-tune the number of workers, etc.
|
||||||
|
|
||||||
|
But if you are doing all that, you might just use the Docker image that does it automatically.
|
||||||
|
|
@ -1,258 +0,0 @@
|
||||||
articles:
|
|
||||||
english:
|
|
||||||
- link: https://medium.com/@williamhayes/fastapi-starlette-debug-vs-prod-5f7561db3a59
|
|
||||||
title: FastAPI/Starlette debug vs prod
|
|
||||||
author_link: https://medium.com/@williamhayes
|
|
||||||
author: William Hayes
|
|
||||||
- link: https://medium.com/data-rebels/fastapi-google-as-an-external-authentication-provider-3a527672cf33
|
|
||||||
title: FastAPI — Google as an external authentication provider
|
|
||||||
author_link: https://medium.com/@nilsdebruin
|
|
||||||
author: Nils de Bruin
|
|
||||||
- link: https://medium.com/data-rebels/fastapi-how-to-add-basic-and-cookie-authentication-a45c85ef47d3
|
|
||||||
title: FastAPI — How to add basic and cookie authentication
|
|
||||||
author_link: https://medium.com/@nilsdebruin
|
|
||||||
author: Nils de Bruin
|
|
||||||
- link: https://dev.to/errietta/introduction-to-the-fastapi-python-framework-2n10
|
|
||||||
title: Introduction to the fastapi python framework
|
|
||||||
author_link: https://dev.to/errietta
|
|
||||||
author: Errieta Kostala
|
|
||||||
- link: https://nickc1.github.io/api,/scikit-learn/2019/01/10/scikit-fastapi.html
|
|
||||||
title: "FastAPI and Scikit-Learn: Easily Deploy Models"
|
|
||||||
author_link: https://nickc1.github.io/
|
|
||||||
author: Nick Cortale
|
|
||||||
- link: https://medium.com/data-rebels/fastapi-authentication-revisited-enabling-api-key-authentication-122dc5975680
|
|
||||||
title: "FastAPI authentication revisited: Enabling API key authentication"
|
|
||||||
author_link: https://medium.com/@nilsdebruin
|
|
||||||
author: Nils de Bruin
|
|
||||||
- link: https://medium.com/@nico.axtmann95/deploying-a-scikit-learn-model-with-onnx-und-fastapi-1af398268915
|
|
||||||
title: Deploying a scikit-learn model with ONNX and FastAPI
|
|
||||||
author_link: https://www.linkedin.com/in/nico-axtmann
|
|
||||||
author: Nico Axtmann
|
|
||||||
- link: https://geekflare.com/python-asynchronous-web-frameworks/
|
|
||||||
title: Top 5 Asynchronous Web Frameworks for Python
|
|
||||||
author_link: https://geekflare.com/author/ankush/
|
|
||||||
author: Ankush Thakur
|
|
||||||
- link: https://medium.com/@gntrm/jwt-authentication-with-fastapi-and-aws-cognito-1333f7f2729e
|
|
||||||
title: JWT Authentication with FastAPI and AWS Cognito
|
|
||||||
author_link: https://twitter.com/gntrm
|
|
||||||
author: Johannes Gontrum
|
|
||||||
- link: https://towardsdatascience.com/how-to-deploy-a-machine-learning-model-dc51200fe8cf
|
|
||||||
title: How to Deploy a Machine Learning Model
|
|
||||||
author_link: https://www.linkedin.com/in/mgrootendorst/
|
|
||||||
author: Maarten Grootendorst
|
|
||||||
- link: https://eng.uber.com/ludwig-v0-2/
|
|
||||||
title: "Uber: Ludwig v0.2 Adds New Features and Other Improvements to its Deep Learning Toolbox [including a FastAPI server]"
|
|
||||||
author_link: https://eng.uber.com
|
|
||||||
author: Uber Engineering
|
|
||||||
- link: https://gitlab.com/euri10/fastapi_cheatsheet
|
|
||||||
title: A FastAPI and Swagger UI visual cheatsheet
|
|
||||||
author_link: https://gitlab.com/euri10
|
|
||||||
author: "@euri10"
|
|
||||||
- link: https://medium.com/@mike.p.moritz/using-docker-compose-to-deploy-a-lightweight-python-rest-api-with-a-job-queue-37e6072a209b
|
|
||||||
title: Using Docker Compose to deploy a lightweight Python REST API with a job queue
|
|
||||||
author_link: https://medium.com/@mike.p.moritz
|
|
||||||
author: Mike Moritz
|
|
||||||
- link: https://robwagner.dev/tortoise-fastapi-setup/
|
|
||||||
title: Setting up Tortoise ORM with FastAPI
|
|
||||||
author_link: https://robwagner.dev/
|
|
||||||
author: Rob Wagner
|
|
||||||
- link: https://dev.to/dbanty/why-i-m-leaving-flask-3ki6
|
|
||||||
title: Why I'm Leaving Flask
|
|
||||||
author_link: https://dev.to/dbanty
|
|
||||||
author: Dylan Anthony
|
|
||||||
- link: https://medium.com/python-data/how-to-deploy-tensorflow-2-0-models-as-an-api-service-with-fastapi-docker-128b177e81f3
|
|
||||||
title: How To Deploy Tensorflow 2.0 Models As An API Service With FastAPI & Docker
|
|
||||||
author_link: https://medium.com/@bbrenyah
|
|
||||||
author: Bernard Brenyah
|
|
||||||
- link: https://testdriven.io/blog/fastapi-crud/
|
|
||||||
title: "TestDriven.io: Developing and Testing an Asynchronous API with FastAPI and Pytest"
|
|
||||||
author_link: https://testdriven.io/authors/herman
|
|
||||||
author: Michael Herman
|
|
||||||
- link: https://towardsdatascience.com/deploying-iris-classifications-with-fastapi-and-docker-7c9b83fdec3a
|
|
||||||
title: "Towards Data Science: Deploying Iris Classifications with FastAPI and Docker"
|
|
||||||
author_link: https://towardsdatascience.com/@mandygu
|
|
||||||
author: Mandy Gu
|
|
||||||
- link: https://medium.com/analytics-vidhya/deploy-machine-learning-models-with-keras-fastapi-redis-and-docker-4940df614ece
|
|
||||||
title: Deploy Machine Learning Models with Keras, FastAPI, Redis and Docker
|
|
||||||
author_link: https://medium.com/@shane.soh
|
|
||||||
author: Shane Soh
|
|
||||||
- link: https://medium.com/@arthur393/another-boilerplate-to-fastapi-azure-pipeline-ci-pytest-3c8d9a4be0bb
|
|
||||||
title: "Another Boilerplate to FastAPI: Azure Pipeline CI + Pytest"
|
|
||||||
author_link: https://twitter.com/arthurheinrique
|
|
||||||
author: Arthur Henrique
|
|
||||||
- link: https://iwpnd.pw/articles/2020-01/deploy-fastapi-to-aws-lambda
|
|
||||||
title: How to continuously deploy a FastAPI to AWS Lambda with AWS SAM
|
|
||||||
author_link: https://iwpnd.pw
|
|
||||||
author: Benjamin Ramser
|
|
||||||
- link: https://www.tutlinks.com/create-and-deploy-fastapi-app-to-heroku/
|
|
||||||
title: Create and Deploy FastAPI app to Heroku without using Docker
|
|
||||||
author_link: https://www.linkedin.com/in/navule/
|
|
||||||
author: Navule Pavan Kumar Rao
|
|
||||||
- link: https://iwpnd.pw/articles/2020-03/apache-kafka-fastapi-geostream
|
|
||||||
title: Apache Kafka producer and consumer with FastAPI and aiokafka
|
|
||||||
author_link: https://iwpnd.pw
|
|
||||||
author: Benjamin Ramser
|
|
||||||
- link: https://wuilly.com/2019/10/real-time-notifications-with-python-and-postgres/
|
|
||||||
title: Real-time Notifications with Python and Postgres
|
|
||||||
author_link: https://wuilly.com/
|
|
||||||
author: Guillermo Cruz
|
|
||||||
- link: https://dev.to/paurakhsharma/microservice-in-python-using-fastapi-24cc
|
|
||||||
title: Microservice in Python using FastAPI
|
|
||||||
author_link: https://twitter.com/PaurakhSharma
|
|
||||||
author: Paurakh Sharma Humagain
|
|
||||||
- link: https://dev.to/cuongld2/build-simple-api-service-with-python-fastapi-part-1-581o
|
|
||||||
title: Build simple API service with Python FastAPI — Part 1
|
|
||||||
author_link: https://dev.to/cuongld2
|
|
||||||
author: cuongld2
|
|
||||||
- link: https://paulsec.github.io/posts/fastapi_plus_zeit_serverless_fu/
|
|
||||||
title: FastAPI + Zeit.co = 🚀
|
|
||||||
author_link: https://twitter.com/PaulWebSec
|
|
||||||
author: Paul Sec
|
|
||||||
- link: https://dev.to/tiangolo/build-a-web-api-from-scratch-with-fastapi-the-workshop-2ehe
|
|
||||||
title: Build a web API from scratch with FastAPI - the workshop
|
|
||||||
author_link: https://twitter.com/tiangolo
|
|
||||||
author: Sebastián Ramírez (tiangolo)
|
|
||||||
- link: https://www.twilio.com/blog/build-secure-twilio-webhook-python-fastapi
|
|
||||||
title: Build a Secure Twilio Webhook with Python and FastAPI
|
|
||||||
author_link: https://www.twilio.com
|
|
||||||
author: Twilio
|
|
||||||
- link: https://www.stavros.io/posts/fastapi-with-django/
|
|
||||||
title: Using FastAPI with Django
|
|
||||||
author_link: https://twitter.com/Stavros
|
|
||||||
author: Stavros Korokithakis
|
|
||||||
- link: https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072
|
|
||||||
title: Introducing Dispatch
|
|
||||||
author_link: https://netflixtechblog.com/
|
|
||||||
author: Netflix
|
|
||||||
- link: https://davidefiocco.github.io/streamlit-fastapi-ml-serving/
|
|
||||||
title: Machine learning model serving in Python using FastAPI and streamlit
|
|
||||||
author_link: https://github.com/davidefiocco
|
|
||||||
author: Davide Fiocco
|
|
||||||
- link: https://www.tutlinks.com/deploy-fastapi-on-azure/
|
|
||||||
title: Deploy FastAPI on Azure App Service
|
|
||||||
author_link: https://www.linkedin.com/in/navule/
|
|
||||||
author: Navule Pavan Kumar Rao
|
|
||||||
- link: https://towardsdatascience.com/build-and-host-fast-data-science-applications-using-fastapi-823be8a1d6a0
|
|
||||||
title: Build And Host Fast Data Science Applications Using FastAPI
|
|
||||||
author_link: https://medium.com/@farhadmalik
|
|
||||||
author: Farhad Malik
|
|
||||||
- link: https://medium.com/@gabbyprecious2000/creating-a-crud-app-with-fastapi-part-one-7c049292ad37
|
|
||||||
title: Creating a CRUD App with FastAPI (Part one)
|
|
||||||
author_link: https://medium.com/@gabbyprecious2000
|
|
||||||
author: Precious Ndubueze
|
|
||||||
- link: https://julienharbulot.com/notification-server.html
|
|
||||||
title: HTTP server to display desktop notifications
|
|
||||||
author_link: https://julienharbulot.com/
|
|
||||||
author: Julien Harbulot
|
|
||||||
- link: https://guitton.co/posts/fastapi-monitoring/
|
|
||||||
title: How to monitor your FastAPI service
|
|
||||||
author_link: https://twitter.com/louis_guitton
|
|
||||||
author: Louis Guitton
|
|
||||||
- link: https://amitness.com/2020/06/fastapi-vs-flask/
|
|
||||||
title: FastAPI for Flask Users
|
|
||||||
author_link: https://twitter.com/amitness
|
|
||||||
author: Amit Chaudhary
|
|
||||||
- link: https://valonjanuzaj.medium.com/deploy-a-dockerized-fastapi-application-to-aws-cc757830ba1b
|
|
||||||
title: Deploy a dockerized FastAPI application to AWS
|
|
||||||
author_link: https://www.linkedin.com/in/valon-januzaj-b02692187/
|
|
||||||
author: Valon Januzaj
|
|
||||||
- link: https://dompatmore.com/blog/authenticate-your-fastapi-app-with-auth0
|
|
||||||
title: Authenticate Your FastAPI App with auth0
|
|
||||||
author_link: https://twitter.com/dompatmore
|
|
||||||
author: Dom Patmore
|
|
||||||
japanese:
|
|
||||||
- link: https://qiita.com/mtitg/items/47770e9a562dd150631d
|
|
||||||
title: FastAPI|DB接続してCRUDするPython製APIサーバーを構築
|
|
||||||
author_link: https://qiita.com/mtitg
|
|
||||||
author: "@mtitg"
|
|
||||||
- link: https://qiita.com/ryoryomaru/items/59958ed385b3571d50de
|
|
||||||
title: python製の最新APIフレームワーク FastAPI を触ってみた
|
|
||||||
author_link: https://qiita.com/ryoryomaru
|
|
||||||
author: "@ryoryomaru"
|
|
||||||
- link: https://qiita.com/angel_katayoku/items/0e1f5dbbe62efc612a78
|
|
||||||
title: FastAPIでCORSを回避
|
|
||||||
author_link: https://qiita.com/angel_katayoku
|
|
||||||
author: "@angel_katayoku"
|
|
||||||
- link: https://qiita.com/angel_katayoku/items/4fbc1a4e2b33fa2237d2
|
|
||||||
title: FastAPIをMySQLと接続してDockerで管理してみる
|
|
||||||
author_link: https://qiita.com/angel_katayoku
|
|
||||||
author: "@angel_katayoku"
|
|
||||||
- link: https://qiita.com/angel_katayoku/items/8a458a8952f50b73f420
|
|
||||||
title: FastAPIでPOSTされたJSONのレスポンスbodyを受け取る
|
|
||||||
author_link: https://qiita.com/angel_katayoku
|
|
||||||
author: "@angel_katayoku"
|
|
||||||
- link: https://qiita.com/hikarut/items/b178af2e2440c67c6ac4
|
|
||||||
title: フロントエンド開発者向けのDockerによるPython開発環境構築
|
|
||||||
author_link: https://qiita.com/hikarut
|
|
||||||
author: Hikaru Takahashi
|
|
||||||
- link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-environment
|
|
||||||
title: "【第1回】FastAPIチュートリアル: ToDoアプリを作ってみよう【環境構築編】"
|
|
||||||
author_link: https://rightcode.co.jp/author/jun
|
|
||||||
author: ライトコードメディア編集部
|
|
||||||
- link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-model-building
|
|
||||||
title: "【第2回】FastAPIチュートリアル: ToDoアプリを作ってみよう【モデル構築編】"
|
|
||||||
author_link: https://rightcode.co.jp/author/jun
|
|
||||||
author: ライトコードメディア編集部
|
|
||||||
- link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-authentication-user-registration
|
|
||||||
title: "【第3回】FastAPIチュートリアル: toDoアプリを作ってみよう【認証・ユーザ登録編】"
|
|
||||||
author_link: https://rightcode.co.jp/author/jun
|
|
||||||
author: ライトコードメディア編集部
|
|
||||||
- link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-admin-page-improvement
|
|
||||||
title: "【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】"
|
|
||||||
author_link: https://rightcode.co.jp/author/jun
|
|
||||||
author: ライトコードメディア編集部
|
|
||||||
- link: https://qiita.com/bee2/items/0ad260ab9835a2087dae
|
|
||||||
title: PythonのWeb frameworkのパフォーマンス比較 (Django, Flask, responder, FastAPI, japronto)
|
|
||||||
author_link: https://qiita.com/bee2
|
|
||||||
author: "@bee2"
|
|
||||||
- link: https://qiita.com/bee2/items/75d9c0d7ba20e7a4a0e9
|
|
||||||
title: "[FastAPI] Python製のASGI Web フレームワーク FastAPIに入門する"
|
|
||||||
author_link: https://qiita.com/bee2
|
|
||||||
author: "@bee2"
|
|
||||||
vietnamese:
|
|
||||||
- link: https://fullstackstation.com/fastapi-trien-khai-bang-docker/
|
|
||||||
title: "FASTAPI: TRIỂN KHAI BẰNG DOCKER"
|
|
||||||
author_link: https://fullstackstation.com/author/figonking/
|
|
||||||
author: Nguyễn Nhân
|
|
||||||
russian:
|
|
||||||
- link: https://habr.com/ru/post/454440/
|
|
||||||
title: "Мелкая питонячая радость #2: Starlette - Солидная примочка – FastAPI"
|
|
||||||
author_link: https://habr.com/ru/users/57uff3r/
|
|
||||||
author: Andrey Korchak
|
|
||||||
- link: https://habr.com/ru/post/478620/
|
|
||||||
title: Почему Вы должны попробовать FastAPI?
|
|
||||||
author_link: https://github.com/prostomarkeloff
|
|
||||||
author: prostomarkeloff
|
|
||||||
- link: https://trkohler.com/fast-api-introduction-to-framework
|
|
||||||
title: "FastAPI: знакомимся с фреймворком"
|
|
||||||
author_link: https://www.linkedin.com/in/trkohler/
|
|
||||||
author: Troy Köhler
|
|
||||||
german:
|
|
||||||
- link: https://blog.codecentric.de/2019/08/inbetriebnahme-eines-scikit-learn-modells-mit-onnx-und-fastapi/
|
|
||||||
title: Inbetriebnahme eines scikit-learn-Modells mit ONNX und FastAPI
|
|
||||||
author_link: https://twitter.com/_nicoax
|
|
||||||
author: Nico Axtmann
|
|
||||||
podcasts:
|
|
||||||
english:
|
|
||||||
- link: https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855
|
|
||||||
title: FastAPI on PythonBytes
|
|
||||||
author_link: https://pythonbytes.fm/
|
|
||||||
author: Python Bytes FM
|
|
||||||
- link: https://www.pythonpodcast.com/fastapi-web-application-framework-episode-259/
|
|
||||||
title: "Build The Next Generation Of Python Web Applications With FastAPI - Episode 259 - interview to Sebastían Ramírez (tiangolo)"
|
|
||||||
author_link: https://www.pythonpodcast.com/
|
|
||||||
author: Podcast.`__init__`
|
|
||||||
talks:
|
|
||||||
english:
|
|
||||||
- link: https://www.youtube.com/watch?v=3DLwPcrE5mA
|
|
||||||
title: "PyCon UK 2019: FastAPI from the ground up"
|
|
||||||
author_link: https://twitter.com/chriswithers13
|
|
||||||
author: Chris Withers
|
|
||||||
- link: https://www.youtube.com/watch?v=z9K5pwb0rt8
|
|
||||||
title: "PyConBY 2020: Serve ML models easily with FastAPI"
|
|
||||||
author_link: https://twitter.com/tiangolo
|
|
||||||
author: "Sebastián Ramírez (tiangolo)"
|
|
||||||
- link: https://www.youtube.com/watch?v=PnpTY1f4k2U
|
|
||||||
title: "[VIRTUAL] Py.Amsterdam's flying Software Circus: Intro to FastAPI"
|
|
||||||
author_link: https://twitter.com/tiangolo
|
|
||||||
author: "Sebastián Ramírez (tiangolo)"
|
|
||||||
|
|
@ -1,752 +0,0 @@
|
||||||
maintainers:
|
|
||||||
- login: tiangolo
|
|
||||||
answers: 1225
|
|
||||||
prs: 232
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=05f95ca7fdead36edd9c86be46b4ef6c3c71f876&v=4
|
|
||||||
url: https://github.com/tiangolo
|
|
||||||
experts:
|
|
||||||
- login: Kludex
|
|
||||||
count: 267
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4
|
|
||||||
url: https://github.com/Kludex
|
|
||||||
- login: dmontagu
|
|
||||||
count: 262
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4
|
|
||||||
url: https://github.com/dmontagu
|
|
||||||
- login: ycd
|
|
||||||
count: 216
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4
|
|
||||||
url: https://github.com/ycd
|
|
||||||
- login: Mause
|
|
||||||
count: 182
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4
|
|
||||||
url: https://github.com/Mause
|
|
||||||
- login: euri10
|
|
||||||
count: 166
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4
|
|
||||||
url: https://github.com/euri10
|
|
||||||
- login: phy25
|
|
||||||
count: 130
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4
|
|
||||||
url: https://github.com/phy25
|
|
||||||
- login: ArcLightSlavik
|
|
||||||
count: 64
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4
|
|
||||||
url: https://github.com/ArcLightSlavik
|
|
||||||
- login: falkben
|
|
||||||
count: 56
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4
|
|
||||||
url: https://github.com/falkben
|
|
||||||
- login: raphaelauv
|
|
||||||
count: 47
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4
|
|
||||||
url: https://github.com/raphaelauv
|
|
||||||
- login: sm-Fifteen
|
|
||||||
count: 46
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4
|
|
||||||
url: https://github.com/sm-Fifteen
|
|
||||||
- login: includeamin
|
|
||||||
count: 38
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4
|
|
||||||
url: https://github.com/includeamin
|
|
||||||
- login: prostomarkeloff
|
|
||||||
count: 33
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4
|
|
||||||
url: https://github.com/prostomarkeloff
|
|
||||||
- login: Dustyposa
|
|
||||||
count: 32
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4
|
|
||||||
url: https://github.com/Dustyposa
|
|
||||||
- login: krishnardt
|
|
||||||
count: 30
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4
|
|
||||||
url: https://github.com/krishnardt
|
|
||||||
- login: insomnes
|
|
||||||
count: 30
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4
|
|
||||||
url: https://github.com/insomnes
|
|
||||||
- login: wshayes
|
|
||||||
count: 29
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4
|
|
||||||
url: https://github.com/wshayes
|
|
||||||
- login: dbanty
|
|
||||||
count: 25
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=0cf33e4f40efc2ea206a1189fd63a11344eb88ed&v=4
|
|
||||||
url: https://github.com/dbanty
|
|
||||||
- login: SirTelemak
|
|
||||||
count: 24
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4
|
|
||||||
url: https://github.com/SirTelemak
|
|
||||||
- login: chbndrhnns
|
|
||||||
count: 22
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4
|
|
||||||
url: https://github.com/chbndrhnns
|
|
||||||
- login: acnebs
|
|
||||||
count: 22
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=bfd127b3e6200f4d00afd714f0fc95c2512df19b&v=4
|
|
||||||
url: https://github.com/acnebs
|
|
||||||
- login: nsidnev
|
|
||||||
count: 22
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4
|
|
||||||
url: https://github.com/nsidnev
|
|
||||||
- login: frankie567
|
|
||||||
count: 21
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=72adf1cb1d29787305c99700d669561952cea0af&v=4
|
|
||||||
url: https://github.com/frankie567
|
|
||||||
- login: chris-allnutt
|
|
||||||
count: 21
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4
|
|
||||||
url: https://github.com/chris-allnutt
|
|
||||||
- login: retnikt
|
|
||||||
count: 19
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4
|
|
||||||
url: https://github.com/retnikt
|
|
||||||
- login: Hultner
|
|
||||||
count: 18
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4
|
|
||||||
url: https://github.com/Hultner
|
|
||||||
- login: jorgerpo
|
|
||||||
count: 17
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4
|
|
||||||
url: https://github.com/jorgerpo
|
|
||||||
- login: nkhitrov
|
|
||||||
count: 17
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4
|
|
||||||
url: https://github.com/nkhitrov
|
|
||||||
- login: waynerv
|
|
||||||
count: 15
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4
|
|
||||||
url: https://github.com/waynerv
|
|
||||||
- login: haizaar
|
|
||||||
count: 13
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4
|
|
||||||
url: https://github.com/haizaar
|
|
||||||
- login: acidjunk
|
|
||||||
count: 11
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4
|
|
||||||
url: https://github.com/acidjunk
|
|
||||||
- login: zamiramir
|
|
||||||
count: 11
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/40475662?u=e58ef61034e8d0d6a312cc956fb09b9c3332b449&v=4
|
|
||||||
url: https://github.com/zamiramir
|
|
||||||
- login: juntatalor
|
|
||||||
count: 11
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/8134632?v=4
|
|
||||||
url: https://github.com/juntatalor
|
|
||||||
- login: valentin994
|
|
||||||
count: 11
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/42819267?u=fdeeaa9242a59b243f8603496b00994f6951d5a2&v=4
|
|
||||||
url: https://github.com/valentin994
|
|
||||||
- login: aalifadv
|
|
||||||
count: 11
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/78442260?v=4
|
|
||||||
url: https://github.com/aalifadv
|
|
||||||
- login: stefanondisponibile
|
|
||||||
count: 10
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/20441825?u=ee1e59446b98f8ec2363caeda4c17164d0d9cc7d&v=4
|
|
||||||
url: https://github.com/stefanondisponibile
|
|
||||||
last_month_active:
|
|
||||||
- login: Kludex
|
|
||||||
count: 12
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4
|
|
||||||
url: https://github.com/Kludex
|
|
||||||
- login: frankie567
|
|
||||||
count: 9
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=72adf1cb1d29787305c99700d669561952cea0af&v=4
|
|
||||||
url: https://github.com/frankie567
|
|
||||||
- login: Mause
|
|
||||||
count: 6
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4
|
|
||||||
url: https://github.com/Mause
|
|
||||||
- login: ArcLightSlavik
|
|
||||||
count: 6
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4
|
|
||||||
url: https://github.com/ArcLightSlavik
|
|
||||||
- login: gyKa
|
|
||||||
count: 4
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1000842?v=4
|
|
||||||
url: https://github.com/gyKa
|
|
||||||
- login: ricky-sb
|
|
||||||
count: 3
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/10700079?v=4
|
|
||||||
url: https://github.com/ricky-sb
|
|
||||||
- login: captainCapitalism
|
|
||||||
count: 3
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/32553875?v=4
|
|
||||||
url: https://github.com/captainCapitalism
|
|
||||||
- login: acidjunk
|
|
||||||
count: 3
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4
|
|
||||||
url: https://github.com/acidjunk
|
|
||||||
top_contributors:
|
|
||||||
- login: waynerv
|
|
||||||
count: 25
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4
|
|
||||||
url: https://github.com/waynerv
|
|
||||||
- login: tokusumi
|
|
||||||
count: 22
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4
|
|
||||||
url: https://github.com/tokusumi
|
|
||||||
- login: dmontagu
|
|
||||||
count: 16
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4
|
|
||||||
url: https://github.com/dmontagu
|
|
||||||
- login: euri10
|
|
||||||
count: 13
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4
|
|
||||||
url: https://github.com/euri10
|
|
||||||
- login: mariacamilagl
|
|
||||||
count: 9
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4
|
|
||||||
url: https://github.com/mariacamilagl
|
|
||||||
- login: RunningIkkyu
|
|
||||||
count: 7
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=706e1ee3f248245f2d68b976d149d06fd5a2010d&v=4
|
|
||||||
url: https://github.com/RunningIkkyu
|
|
||||||
- login: Serrones
|
|
||||||
count: 7
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4
|
|
||||||
url: https://github.com/Serrones
|
|
||||||
- login: hard-coders
|
|
||||||
count: 6
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=f2d3d2038c55d86d7f9348f4e6c5e30191e4ee8b&v=4
|
|
||||||
url: https://github.com/hard-coders
|
|
||||||
- login: wshayes
|
|
||||||
count: 5
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4
|
|
||||||
url: https://github.com/wshayes
|
|
||||||
- login: SwftAlpc
|
|
||||||
count: 5
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4
|
|
||||||
url: https://github.com/SwftAlpc
|
|
||||||
- login: Attsun1031
|
|
||||||
count: 5
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4
|
|
||||||
url: https://github.com/Attsun1031
|
|
||||||
- login: jekirl
|
|
||||||
count: 4
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/2546697?v=4
|
|
||||||
url: https://github.com/jekirl
|
|
||||||
- login: komtaki
|
|
||||||
count: 4
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=5a44657c0544111ee3c132d9bb9951c2804f7969&v=4
|
|
||||||
url: https://github.com/komtaki
|
|
||||||
- login: Smlep
|
|
||||||
count: 4
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4
|
|
||||||
url: https://github.com/Smlep
|
|
||||||
top_reviewers:
|
|
||||||
- login: Kludex
|
|
||||||
count: 80
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4
|
|
||||||
url: https://github.com/Kludex
|
|
||||||
- login: tokusumi
|
|
||||||
count: 44
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4
|
|
||||||
url: https://github.com/tokusumi
|
|
||||||
- login: Laineyzhang55
|
|
||||||
count: 42
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4
|
|
||||||
url: https://github.com/Laineyzhang55
|
|
||||||
- login: ycd
|
|
||||||
count: 41
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4
|
|
||||||
url: https://github.com/ycd
|
|
||||||
- login: waynerv
|
|
||||||
count: 38
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4
|
|
||||||
url: https://github.com/waynerv
|
|
||||||
- login: AdrianDeAnda
|
|
||||||
count: 28
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=bb7f8a0d6c9de4e9d0320a9f271210206e202250&v=4
|
|
||||||
url: https://github.com/AdrianDeAnda
|
|
||||||
- login: dmontagu
|
|
||||||
count: 23
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4
|
|
||||||
url: https://github.com/dmontagu
|
|
||||||
- login: komtaki
|
|
||||||
count: 21
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=5a44657c0544111ee3c132d9bb9951c2804f7969&v=4
|
|
||||||
url: https://github.com/komtaki
|
|
||||||
- login: ArcLightSlavik
|
|
||||||
count: 20
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4
|
|
||||||
url: https://github.com/ArcLightSlavik
|
|
||||||
- login: cassiobotaro
|
|
||||||
count: 16
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4
|
|
||||||
url: https://github.com/cassiobotaro
|
|
||||||
- login: yanever
|
|
||||||
count: 16
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/21978760?v=4
|
|
||||||
url: https://github.com/yanever
|
|
||||||
- login: SwftAlpc
|
|
||||||
count: 16
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4
|
|
||||||
url: https://github.com/SwftAlpc
|
|
||||||
- login: pedabraham
|
|
||||||
count: 15
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/16860088?u=abf922a7b920bf8fdb7867d8b43e091f1e796178&v=4
|
|
||||||
url: https://github.com/pedabraham
|
|
||||||
- login: delhi09
|
|
||||||
count: 15
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4
|
|
||||||
url: https://github.com/delhi09
|
|
||||||
- login: RunningIkkyu
|
|
||||||
count: 12
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=706e1ee3f248245f2d68b976d149d06fd5a2010d&v=4
|
|
||||||
url: https://github.com/RunningIkkyu
|
|
||||||
- login: hard-coders
|
|
||||||
count: 12
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=f2d3d2038c55d86d7f9348f4e6c5e30191e4ee8b&v=4
|
|
||||||
url: https://github.com/hard-coders
|
|
||||||
- login: sh0nk
|
|
||||||
count: 11
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4
|
|
||||||
url: https://github.com/sh0nk
|
|
||||||
- login: mariacamilagl
|
|
||||||
count: 10
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4
|
|
||||||
url: https://github.com/mariacamilagl
|
|
||||||
- login: Attsun1031
|
|
||||||
count: 10
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4
|
|
||||||
url: https://github.com/Attsun1031
|
|
||||||
- login: maoyibo
|
|
||||||
count: 10
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/7887703?v=4
|
|
||||||
url: https://github.com/maoyibo
|
|
||||||
- login: PandaHun
|
|
||||||
count: 9
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/13096845?u=646eba44db720e37d0dbe8e98e77ab534ea78a20&v=4
|
|
||||||
url: https://github.com/PandaHun
|
|
||||||
- login: bezaca
|
|
||||||
count: 9
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4
|
|
||||||
url: https://github.com/bezaca
|
|
||||||
- login: lsglucas
|
|
||||||
count: 8
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4
|
|
||||||
url: https://github.com/lsglucas
|
|
||||||
- login: rjNemo
|
|
||||||
count: 8
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4
|
|
||||||
url: https://github.com/rjNemo
|
|
||||||
- login: blt232018
|
|
||||||
count: 8
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/43393471?u=172b0e0391db1aa6c1706498d6dfcb003c8a4857&v=4
|
|
||||||
url: https://github.com/blt232018
|
|
||||||
- login: Serrones
|
|
||||||
count: 7
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4
|
|
||||||
url: https://github.com/Serrones
|
|
||||||
- login: ryuckel
|
|
||||||
count: 7
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/36391432?u=094eec0cfddd5013f76f31e55e56147d78b19553&v=4
|
|
||||||
url: https://github.com/ryuckel
|
|
||||||
- login: raphaelauv
|
|
||||||
count: 7
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4
|
|
||||||
url: https://github.com/raphaelauv
|
|
||||||
- login: NastasiaSaby
|
|
||||||
count: 7
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/8245071?u=b3afd005f9e4bf080c219ef61a592b3a8004b764&v=4
|
|
||||||
url: https://github.com/NastasiaSaby
|
|
||||||
- login: Smlep
|
|
||||||
count: 7
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4
|
|
||||||
url: https://github.com/Smlep
|
|
||||||
- login: jovicon
|
|
||||||
count: 6
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4
|
|
||||||
url: https://github.com/jovicon
|
|
||||||
- login: Mause
|
|
||||||
count: 6
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4
|
|
||||||
url: https://github.com/Mause
|
|
||||||
- login: nimctl
|
|
||||||
count: 5
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/49960770?u=e39b11d47188744ee07b2a1c7ce1a1bdf3c80760&v=4
|
|
||||||
url: https://github.com/nimctl
|
|
||||||
- login: juntatalor
|
|
||||||
count: 5
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/8134632?v=4
|
|
||||||
url: https://github.com/juntatalor
|
|
||||||
- login: SnkSynthesis
|
|
||||||
count: 5
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/63564282?u=0078826509dbecb2fdb543f4e881c9cd06157893&v=4
|
|
||||||
url: https://github.com/SnkSynthesis
|
|
||||||
- login: anthonycepeda
|
|
||||||
count: 5
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=892f700c79f9732211bd5221bf16eec32356a732&v=4
|
|
||||||
url: https://github.com/anthonycepeda
|
|
||||||
- login: euri10
|
|
||||||
count: 4
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4
|
|
||||||
url: https://github.com/euri10
|
|
||||||
- login: rkbeatss
|
|
||||||
count: 4
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/23391143?u=56ab6bff50be950fa8cae5cf736f2ae66e319ff3&v=4
|
|
||||||
url: https://github.com/rkbeatss
|
|
||||||
- login: aviramha
|
|
||||||
count: 4
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/41201924?u=6883cc4fc13a7b2e60d4deddd4be06f9c5287880&v=4
|
|
||||||
url: https://github.com/aviramha
|
|
||||||
- login: Zxilly
|
|
||||||
count: 4
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/31370133?v=4
|
|
||||||
url: https://github.com/Zxilly
|
|
||||||
- login: Bluenix2
|
|
||||||
count: 4
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/38372706?u=c9d28aff15958d6ebf1971148bfb3154ff943c4f&v=4
|
|
||||||
url: https://github.com/Bluenix2
|
|
||||||
sponsors_50:
|
|
||||||
- login: johnadjei
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/767860?v=4
|
|
||||||
url: https://github.com/johnadjei
|
|
||||||
- login: wdwinslow
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4
|
|
||||||
url: https://github.com/wdwinslow
|
|
||||||
- login: bingwu-chime
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/67026650?u=603a6b345f25c20c6706a8a6c7f71ae688d649a5&v=4
|
|
||||||
url: https://github.com/bingwu-chime
|
|
||||||
sponsors:
|
|
||||||
- login: kamalgill
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/133923?u=0df9181d97436ce330e9acf90ab8a54b7022efe7&v=4
|
|
||||||
url: https://github.com/kamalgill
|
|
||||||
- login: grillazz
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/3415861?u=16d7d0ffa5dfb99f8834f8f76d90e138ba09b94a&v=4
|
|
||||||
url: https://github.com/grillazz
|
|
||||||
- login: tizz98
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/5739698?u=f095a3659e3a8e7c69ccd822696990b521ea25f9&v=4
|
|
||||||
url: https://github.com/tizz98
|
|
||||||
- login: jmaralc
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/21101214?u=b15a9f07b7cbf6c9dcdbcb6550bbd2c52f55aa50&v=4
|
|
||||||
url: https://github.com/jmaralc
|
|
||||||
- login: psgandalf
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/8134158?v=4
|
|
||||||
url: https://github.com/psgandalf
|
|
||||||
- login: samuelcolvin
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=807390ba9cfe23906c3bf8a0d56aaca3cf2bfa0d&v=4
|
|
||||||
url: https://github.com/samuelcolvin
|
|
||||||
- login: jokull
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/701?u=0532b62166893d5160ef795c4c8b7512d971af05&v=4
|
|
||||||
url: https://github.com/jokull
|
|
||||||
- login: wshayes
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4
|
|
||||||
url: https://github.com/wshayes
|
|
||||||
- login: koxudaxi
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4
|
|
||||||
url: https://github.com/koxudaxi
|
|
||||||
- login: falkben
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4
|
|
||||||
url: https://github.com/falkben
|
|
||||||
- login: jqueguiner
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/690878?u=e4835b2a985a0f2d52018e4926cb5a58c26a62e8&v=4
|
|
||||||
url: https://github.com/jqueguiner
|
|
||||||
- login: Mazyod
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/860511?v=4
|
|
||||||
url: https://github.com/Mazyod
|
|
||||||
- login: ltieman
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1084689?u=e69b17de17cb3ca141a17daa7ccbe173ceb1eb17&v=4
|
|
||||||
url: https://github.com/ltieman
|
|
||||||
- login: mrmattwright
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1277725?v=4
|
|
||||||
url: https://github.com/mrmattwright
|
|
||||||
- login: westonsteimel
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1593939?u=0f2c0e3647f916fe295d62fa70da7a4c177115e3&v=4
|
|
||||||
url: https://github.com/westonsteimel
|
|
||||||
- login: timdrijvers
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1694939?v=4
|
|
||||||
url: https://github.com/timdrijvers
|
|
||||||
- login: mrgnw
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/2504532?u=7ec43837a6d0afa80f96f0788744ea6341b89f97&v=4
|
|
||||||
url: https://github.com/mrgnw
|
|
||||||
- login: madisonmay
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/2645393?u=f22b93c6ea345a4d26a90a3834dfc7f0789fcb63&v=4
|
|
||||||
url: https://github.com/madisonmay
|
|
||||||
- login: saivarunk
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/2976867?u=71f4385e781e9a9e871a52f2d4686f9a8d69ba2f&v=4
|
|
||||||
url: https://github.com/saivarunk
|
|
||||||
- login: andre1sk
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/3148093?v=4
|
|
||||||
url: https://github.com/andre1sk
|
|
||||||
- login: Shark009
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/3163309?v=4
|
|
||||||
url: https://github.com/Shark009
|
|
||||||
- login: peterHoburg
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/3860655?u=f55f47eb2d6a9b495e806ac5a044e3ae01ccc1fa&v=4
|
|
||||||
url: https://github.com/peterHoburg
|
|
||||||
- login: dudil
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/4785835?u=58b7ea39123e0507f3b2996448a27256b16fd697&v=4
|
|
||||||
url: https://github.com/dudil
|
|
||||||
- login: ennui93
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/5300907?u=5b5452725ddb391b2caaebf34e05aba873591c3a&v=4
|
|
||||||
url: https://github.com/ennui93
|
|
||||||
- login: sco1
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/5323929?u=2b8434060d0c9d93de80a2a945baed94a412c31e&v=4
|
|
||||||
url: https://github.com/sco1
|
|
||||||
- login: MacroPower
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/5648814?u=b2730000c9f9a471282b9849d2cc85711d7973d4&v=4
|
|
||||||
url: https://github.com/MacroPower
|
|
||||||
- login: ginomempin
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/6091865?v=4
|
|
||||||
url: https://github.com/ginomempin
|
|
||||||
- login: iwpnd
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/6152183?u=b2286006daafff5f991557344fee20b5da59639a&v=4
|
|
||||||
url: https://github.com/iwpnd
|
|
||||||
- login: s3ich4n
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/6926298?u=ba3025d698e1c986655e776ae383a3d60d9d578e&v=4
|
|
||||||
url: https://github.com/s3ich4n
|
|
||||||
- login: Rehket
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4
|
|
||||||
url: https://github.com/Rehket
|
|
||||||
- login: christippett
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/7218120?u=434b9d29287d7de25772d94ddc74a9bd6d969284&v=4
|
|
||||||
url: https://github.com/christippett
|
|
||||||
- login: Kludex
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4
|
|
||||||
url: https://github.com/Kludex
|
|
||||||
- login: Shackelford-Arden
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/7362263?v=4
|
|
||||||
url: https://github.com/Shackelford-Arden
|
|
||||||
- login: macleodmac
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/8996312?u=e39c68c3e0b1d264dcba4850134a291680f46355&v=4
|
|
||||||
url: https://github.com/macleodmac
|
|
||||||
- login: cristeaadrian
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/9112724?v=4
|
|
||||||
url: https://github.com/cristeaadrian
|
|
||||||
- login: otivvormes
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/11317418?u=6de1edefb6afd0108c0ad2816bd6efc4464a9c44&v=4
|
|
||||||
url: https://github.com/otivvormes
|
|
||||||
- login: iambobmae
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/12390270?u=c9a35c2ee5092a9b4135ebb1f91b7f521c467031&v=4
|
|
||||||
url: https://github.com/iambobmae
|
|
||||||
- login: ronaldnwilliams
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/13632749?u=ac41a086d0728bf66a9d2bee9e5e377041ff44a4&v=4
|
|
||||||
url: https://github.com/ronaldnwilliams
|
|
||||||
- login: uselessscat
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/15332878?u=8485a1b7383c274b28f383370ee2d5f9a6cd423b&v=4
|
|
||||||
url: https://github.com/uselessscat
|
|
||||||
- login: natenka
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/15850513?u=00d1083c980d0b4ce32835dc07eee7f43f34fd2f&v=4
|
|
||||||
url: https://github.com/natenka
|
|
||||||
- login: la-mar
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/16618300?u=7755c0521d2bb0d704f35a51464b15c1e2e6c4da&v=4
|
|
||||||
url: https://github.com/la-mar
|
|
||||||
- login: robintully
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/17059673?u=862b9bb01513f5acd30df97433cb97a24dbfb772&v=4
|
|
||||||
url: https://github.com/robintully
|
|
||||||
- login: ShaulAb
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/18129076?u=2c8d48e47f2dbee15c3f89c3d17d4c356504386c&v=4
|
|
||||||
url: https://github.com/ShaulAb
|
|
||||||
- login: wedwardbeck
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/19333237?u=1de4ae2bf8d59eb4c013f21d863cbe0f2010575f&v=4
|
|
||||||
url: https://github.com/wedwardbeck
|
|
||||||
- login: linusg
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/19366641?u=125e390abef8fff3b3b0d370c369cba5d7fd4c67&v=4
|
|
||||||
url: https://github.com/linusg
|
|
||||||
- login: RedCarpetUp
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/20360440?v=4
|
|
||||||
url: https://github.com/RedCarpetUp
|
|
||||||
- login: daddycocoaman
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/21189155?u=756f6a17c71c538b11470f70839baacab43807ef&v=4
|
|
||||||
url: https://github.com/daddycocoaman
|
|
||||||
- login: Filimoa
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=75e02d102d2ee3e3d793e555fa5c63045913ccb0&v=4
|
|
||||||
url: https://github.com/Filimoa
|
|
||||||
- login: raminsj13
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/24259406?u=d51f2a526312ebba150a06936ed187ca0727d329&v=4
|
|
||||||
url: https://github.com/raminsj13
|
|
||||||
- login: comoelcometa
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/25950317?u=c6751efa038561b9bc5fa56d1033d5174e10cd65&v=4
|
|
||||||
url: https://github.com/comoelcometa
|
|
||||||
- login: veprimk
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/29689749?u=f8cb5a15a286e522e5b189bc572d5a1a90217fb2&v=4
|
|
||||||
url: https://github.com/veprimk
|
|
||||||
- login: orihomie
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/29889683?u=6bc2135a52fcb3a49e69e7d50190796618185fda&v=4
|
|
||||||
url: https://github.com/orihomie
|
|
||||||
- login: SaltyCoco
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/31451104?u=6ee4e17c07d21b7054f54a12fa9cc377a1b24ff9&v=4
|
|
||||||
url: https://github.com/SaltyCoco
|
|
||||||
- login: mauroalejandrojm
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/31569442?u=cdada990a1527926a36e95f62c30a8b48bbc49a1&v=4
|
|
||||||
url: https://github.com/mauroalejandrojm
|
|
||||||
- login: public-daniel
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/32238294?u=0377e38dd023395c9643d5388b4e9489a24b4d34&v=4
|
|
||||||
url: https://github.com/public-daniel
|
|
||||||
- login: ybressler
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=6621dc9ab53b697912ab2a32211bb29ae90a9112&v=4
|
|
||||||
url: https://github.com/ybressler
|
|
||||||
- login: dbanty
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=0cf33e4f40efc2ea206a1189fd63a11344eb88ed&v=4
|
|
||||||
url: https://github.com/dbanty
|
|
||||||
- login: dudikbender
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=494f85229115076121b3639a3806bbac1c6ae7f6&v=4
|
|
||||||
url: https://github.com/dudikbender
|
|
||||||
- login: primer-io
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4
|
|
||||||
url: https://github.com/primer-io
|
|
||||||
- login: tkrestiankova
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/67013045?v=4
|
|
||||||
url: https://github.com/tkrestiankova
|
|
||||||
- login: daverin
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/70378377?u=6d1814195c0de7162820eaad95a25b423a3869c0&v=4
|
|
||||||
url: https://github.com/daverin
|
|
||||||
- login: anthonycepeda
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=892f700c79f9732211bd5221bf16eec32356a732&v=4
|
|
||||||
url: https://github.com/anthonycepeda
|
|
||||||
- login: linux-china
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/46711?v=4
|
|
||||||
url: https://github.com/linux-china
|
|
||||||
- login: jhb
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/142217?v=4
|
|
||||||
url: https://github.com/jhb
|
|
||||||
- login: yourkin
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/178984?v=4
|
|
||||||
url: https://github.com/yourkin
|
|
||||||
- login: jmagnusson
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/190835?v=4
|
|
||||||
url: https://github.com/jmagnusson
|
|
||||||
- login: slafs
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4
|
|
||||||
url: https://github.com/slafs
|
|
||||||
- login: adamghill
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/317045?u=f1349d5ffe84a19f324e204777859fbf69ddf633&v=4
|
|
||||||
url: https://github.com/adamghill
|
|
||||||
- login: eteq
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/346587?v=4
|
|
||||||
url: https://github.com/eteq
|
|
||||||
- login: dmig
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/388564?v=4
|
|
||||||
url: https://github.com/dmig
|
|
||||||
- login: hongqn
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/405587?u=470b4c04832e45141fd5264d3354845cc9fc6466&v=4
|
|
||||||
url: https://github.com/hongqn
|
|
||||||
- login: rinckd
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/546002?u=1fcc7e664dc86524a0af6837a0c222829c3fd4e5&v=4
|
|
||||||
url: https://github.com/rinckd
|
|
||||||
- login: Pytlicek
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1430522?u=169dba3bfbc04ed214a914640ff435969f19ddb3&v=4
|
|
||||||
url: https://github.com/Pytlicek
|
|
||||||
- login: okken
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1568356?u=0a991a21bdc62e2bea9ad311652f2c45f453dc84&v=4
|
|
||||||
url: https://github.com/okken
|
|
||||||
- login: leogregianin
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/1684053?u=94ddd387601bd1805034dbe83e6eba0491c15323&v=4
|
|
||||||
url: https://github.com/leogregianin
|
|
||||||
- login: cbonoz
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4
|
|
||||||
url: https://github.com/cbonoz
|
|
||||||
- login: rglsk
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/2768101?u=e349c88673f2155fe021331377c656a9d74bcc25&v=4
|
|
||||||
url: https://github.com/rglsk
|
|
||||||
- login: Atem18
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/2875254?v=4
|
|
||||||
url: https://github.com/Atem18
|
|
||||||
- login: paul121
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/3116995?u=6e2d8691cc345e63ee02e4eb4d7cef82b1fcbedc&v=4
|
|
||||||
url: https://github.com/paul121
|
|
||||||
- login: igorcorrea
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/3438238?u=c57605077c31a8f7b2341fc4912507f91b4a5621&v=4
|
|
||||||
url: https://github.com/igorcorrea
|
|
||||||
- login: zsinx6
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4
|
|
||||||
url: https://github.com/zsinx6
|
|
||||||
- login: pawamoy
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4
|
|
||||||
url: https://github.com/pawamoy
|
|
||||||
- login: spyker77
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/4953435?u=568baae6469628e020fe0bab16e395b7ae10c7d3&v=4
|
|
||||||
url: https://github.com/spyker77
|
|
||||||
- login: serfer2
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/5196592?u=e8798d87120952ed41876778f0cc8a1ddb47f901&v=4
|
|
||||||
url: https://github.com/serfer2
|
|
||||||
- login: JonasKs
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/5310116?u=98a049f3e1491bffb91e1feb7e93def6881a9389&v=4
|
|
||||||
url: https://github.com/JonasKs
|
|
||||||
- login: holec
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/6438041?u=f5af71ec85b3a9d7b8139cb5af0512b02fa9ab1e&v=4
|
|
||||||
url: https://github.com/holec
|
|
||||||
- login: BartlomiejRasztabiga
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/8852711?u=ed213d60f7a423df31ceb1004aa3ec60e612cb98&v=4
|
|
||||||
url: https://github.com/BartlomiejRasztabiga
|
|
||||||
- login: davanstrien
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/8995957?u=fb2aad2b52bb4e7b56db6d7c8ecc9ae1eac1b984&v=4
|
|
||||||
url: https://github.com/davanstrien
|
|
||||||
- login: and-semakin
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/9129071?u=ea77ddf7de4bc375d546bf2825ed420eaddb7666&v=4
|
|
||||||
url: https://github.com/and-semakin
|
|
||||||
- login: VivianSolide
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/9358572?u=ffb2e2ec522a15dcd3f0af1f9fd1df4afe418afa&v=4
|
|
||||||
url: https://github.com/VivianSolide
|
|
||||||
- login: hard-coders
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=f2d3d2038c55d86d7f9348f4e6c5e30191e4ee8b&v=4
|
|
||||||
url: https://github.com/hard-coders
|
|
||||||
- login: satwikkansal
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/10217535?u=b12d6ef74ea297de9e46da6933b1a5b7ba9e6a61&v=4
|
|
||||||
url: https://github.com/satwikkansal
|
|
||||||
- login: pheanex
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/10408624?u=5b6bab6ee174aa6e991333e06eb29f628741013d&v=4
|
|
||||||
url: https://github.com/pheanex
|
|
||||||
- login: wotori
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/10486621?u=0044c295b91694b8c9bccc0a805681f794250f7b&v=4
|
|
||||||
url: https://github.com/wotori
|
|
||||||
- login: JimFawkes
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/12075115?u=dc58ecfd064d72887c34bf500ddfd52592509acd&v=4
|
|
||||||
url: https://github.com/JimFawkes
|
|
||||||
- login: logan-connolly
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/16244943?u=8ae66dfbba936463cc8aa0dd7a6d2b4c0cc757eb&v=4
|
|
||||||
url: https://github.com/logan-connolly
|
|
||||||
- login: iPr0ger
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/19322290?v=4
|
|
||||||
url: https://github.com/iPr0ger
|
|
||||||
- login: sadikkuzu
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=765ed469c44c004560079210ccdad5b29938eaa9&v=4
|
|
||||||
url: https://github.com/sadikkuzu
|
|
||||||
- login: ghandic
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4
|
|
||||||
url: https://github.com/ghandic
|
|
||||||
- login: MoronVV
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/24293616?v=4
|
|
||||||
url: https://github.com/MoronVV
|
|
||||||
- login: AngusWG
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/26385612?u=f4d4c8bd2097cdd58eb9e385932b83c78777f3c0&v=4
|
|
||||||
url: https://github.com/AngusWG
|
|
||||||
- login: mertguvencli
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/29762151?u=16a906d90df96c8cff9ea131a575c4bc171b1523&v=4
|
|
||||||
url: https://github.com/mertguvencli
|
|
||||||
- login: rgreen32
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/35779241?u=c9d64ad1ab364b6a1ec8e3d859da9ca802d681d8&v=4
|
|
||||||
url: https://github.com/rgreen32
|
|
||||||
- login: askurihin
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/37978981?v=4
|
|
||||||
url: https://github.com/askurihin
|
|
||||||
- login: berrysauce
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/38889179?u=758ed15a5be8bbd03855f5a74f42c19f7946ee32&v=4
|
|
||||||
url: https://github.com/berrysauce
|
|
||||||
- login: JitPackJoyride
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/40203625?u=9638bfeacfa5940358188f8205ce662bba022b53&v=4
|
|
||||||
url: https://github.com/JitPackJoyride
|
|
||||||
- login: es3n1n
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/40367813?u=e881a3880f1e342d19a1ea7c8e1b6d76c52dc294&v=4
|
|
||||||
url: https://github.com/es3n1n
|
|
||||||
- login: ilias-ant
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/42189572?u=a2d6121bac4d125d92ec207460fa3f1842d37e66&v=4
|
|
||||||
url: https://github.com/ilias-ant
|
|
||||||
- login: kbhatiya999
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/47816034?v=4
|
|
||||||
url: https://github.com/kbhatiya999
|
|
||||||
- login: akanz1
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/51492342?u=2280f57134118714645e16b535c1a37adf6b369b&v=4
|
|
||||||
url: https://github.com/akanz1
|
|
||||||
- login: rychardvale
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/54805553?u=3d20ab05301d05f9ac3500fb79a2bfee3842b753&v=4
|
|
||||||
url: https://github.com/rychardvale
|
|
||||||
- login: athemeart
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/61623624?v=4
|
|
||||||
url: https://github.com/athemeart
|
|
||||||
- login: Rhythmicc
|
|
||||||
avatarUrl: https://avatars.githubusercontent.com/u/29839231?u=2100781089a259707c475c4547bd7995b0fc18ee&v=4
|
|
||||||
url: https://github.com/Rhythmicc
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
gold:
|
|
||||||
- url: https://www.deta.sh/?ref=fastapi
|
|
||||||
title: The launchpad for all your (team's) ideas
|
|
||||||
img: https://fastapi.tiangolo.com/img/sponsors/deta.svg
|
|
||||||
- url: https://bit.ly/2QSouzH
|
|
||||||
title: "Jina: build neural search-as-a-service for any kind of data in just minutes."
|
|
||||||
img: https://fastapi.tiangolo.com/img/sponsors/jina.svg
|
|
||||||
silver:
|
|
||||||
- url: https://www.investsuite.com/jobs
|
|
||||||
title: Wealthtech jobs with FastAPI
|
|
||||||
img: https://fastapi.tiangolo.com/img/sponsors/investsuite.svg
|
|
||||||
- url: https://www.vim.so/?utm_source=FastAPI
|
|
||||||
title: We help you master vim with interactive exercises
|
|
||||||
img: https://fastapi.tiangolo.com/img/sponsors/vimso.png
|
|
||||||
- url: https://talkpython.fm/fastapi-sponsor
|
|
||||||
title: FastAPI video courses on demand from people you trust
|
|
||||||
img: https://fastapi.tiangolo.com/img/sponsors/talkpython.png
|
|
||||||
bronze:
|
|
||||||
- url: https://testdriven.io/courses/tdd-fastapi/
|
|
||||||
title: Learn to build high-quality web apps with best practices
|
|
||||||
img: https://fastapi.tiangolo.com/img/sponsors/testdriven.svg
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
# Async Tests
|
|
||||||
|
|
||||||
You have already seen how to test your **FastAPI** applications using the provided `TestClient`, but with it, you can't test or run any other `async` function in your (synchronous) pytest functions.
|
|
||||||
|
|
||||||
Being able to use asynchronous functions in your tests could be useful, for example, when you're querying your database asynchronously. Imagine you want to test sending requests to your FastAPI application and then verify that your backend successfully wrote the correct data in the database, while using an async database library.
|
|
||||||
|
|
||||||
Let's look at how we can make that work.
|
|
||||||
|
|
||||||
## pytest-asyncio
|
|
||||||
|
|
||||||
If we want to call asynchronous functions in our tests, our test functions have to be asynchronous. Pytest provides a neat library for this, called `pytest-asyncio`, that allows us to specify that some test functions are to be called asynchronously.
|
|
||||||
|
|
||||||
You can install it via:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ pip install pytest-asyncio
|
|
||||||
|
|
||||||
---> 100%
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## HTTPX
|
|
||||||
|
|
||||||
Even if your **FastAPI** application uses normal `def` functions instead of `async def`, it is still an `async` application underneath.
|
|
||||||
|
|
||||||
The `TestClient` does some magic inside to call the asynchronous FastAPI application in your normal `def` test functions, using standard pytest. But that magic doesn't work anymore when we're using it inside asynchronous functions. By running our tests asynchronously, we can no longer use the `TestClient` inside our test functions.
|
|
||||||
|
|
||||||
Luckily there's a nice alternative, called <a href="https://www.python-httpx.org/" class="external-link" target="_blank">HTTPX</a>.
|
|
||||||
|
|
||||||
HTTPX is an HTTP client for Python 3 that allows us to query our FastAPI application similarly to how we did it with the `TestClient`.
|
|
||||||
|
|
||||||
If you're familiar with the <a href="https://requests.readthedocs.io/en/master/" class="external-link" target="_blank">Requests</a> library, you'll find that the API of HTTPX is almost identical.
|
|
||||||
|
|
||||||
The important difference for us is that with HTTPX we are not limited to synchronous, but can also make asynchronous requests.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
For a simple example, let's consider the following `main.py` module:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
{!../../../docs_src/async_tests/main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `test_main.py` module that contains the tests for `main.py` could look like this now:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
{!../../../docs_src/async_tests/test_main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run it
|
|
||||||
|
|
||||||
You can run your tests as usual via:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ pytest
|
|
||||||
|
|
||||||
---> 100%
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## In Detail
|
|
||||||
|
|
||||||
The marker `@pytest.mark.asyncio` tells pytest that this test function should be called asynchronously:
|
|
||||||
|
|
||||||
```Python hl_lines="7"
|
|
||||||
{!../../../docs_src/async_tests/test_main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Note that the test function is now `async def` instead of just `def` as before when using the `TestClient`.
|
|
||||||
|
|
||||||
Then we can create an `AsyncClient` with the app, and send async requests to it, using `await`.
|
|
||||||
|
|
||||||
```Python hl_lines="9-10"
|
|
||||||
{!../../../docs_src/async_tests/test_main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
This is the equivalent to:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
response = client.get('/')
|
|
||||||
```
|
|
||||||
|
|
||||||
that we used to make our requests with the `TestClient`.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Note that we're using async/await with the new `AsyncClient` - the request is asynchronous.
|
|
||||||
|
|
||||||
## Other Asynchronous Function Calls
|
|
||||||
|
|
||||||
As the testing function is now asynchronous, you can now also call (and `await`) other `async` functions apart from sending requests to your FastAPI application in your tests, exactly as you would call them anywhere else in your code.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
If you encounter a `RuntimeError: Task attached to a different loop` when integrating asynchronous function calls in your tests (e.g. when using <a href="https://stackoverflow.com/questions/41584243/runtimeerror-task-attached-to-a-different-loop" class="external-link" target="_blank">MongoDB's MotorClient</a>) check out <a href="https://github.com/pytest-dev/pytest-asyncio/issues/38#issuecomment-264418154" class="external-link" target="_blank">this issue</a> in the pytest-asyncio repository.
|
|
||||||
|
|
@ -1,346 +0,0 @@
|
||||||
# Behind a Proxy
|
|
||||||
|
|
||||||
In some situations, you might need to use a **proxy** server like Traefik or Nginx with a configuration that adds an extra path prefix that is not seen by your application.
|
|
||||||
|
|
||||||
In these cases you can use `root_path` to configure your application.
|
|
||||||
|
|
||||||
The `root_path` is a mechanism provided by the ASGI specification (that FastAPI is built on, through Starlette).
|
|
||||||
|
|
||||||
The `root_path` is used to handle these specific cases.
|
|
||||||
|
|
||||||
And it's also used internally when mounting sub-applications.
|
|
||||||
|
|
||||||
## Proxy with a stripped path prefix
|
|
||||||
|
|
||||||
Having a proxy with a stripped path prefix, in this case, means that you could declare a path at `/app` in your code, but then, you add a layer on top (the proxy) that would put your **FastAPI** application under a path like `/api/v1`.
|
|
||||||
|
|
||||||
In this case, the original path `/app` would actually be served at `/api/v1/app`.
|
|
||||||
|
|
||||||
Even though all your code is written assuming there's just `/app`.
|
|
||||||
|
|
||||||
And the proxy would be **"stripping"** the **path prefix** on the fly before transmitting the request to Uvicorn, keep your application convinced that it is serving at `/app`, so that you don't have to update all your code to include the prefix `/api/v1`.
|
|
||||||
|
|
||||||
Up to here, everything would work as normally.
|
|
||||||
|
|
||||||
But then, when you open the integrated docs UI (the frontend), it would expect to get the OpenAPI schema at `/openapi.json`, instead of `/api/v1/openapi.json`.
|
|
||||||
|
|
||||||
So, the frontend (that runs in the browser) would try to reach `/openapi.json` and wouldn't be able to get the OpenAPI schema.
|
|
||||||
|
|
||||||
Because we have a proxy with a path prefix of `/api/v1` for our app, the frontend needs to fetch the OpenAPI schema at `/api/v1/openapi.json`.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
|
|
||||||
browser("Browser")
|
|
||||||
proxy["Proxy on http://0.0.0.0:9999/api/v1/app"]
|
|
||||||
server["Server on http://127.0.0.1:8000/app"]
|
|
||||||
|
|
||||||
browser --> proxy
|
|
||||||
proxy --> server
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The IP `0.0.0.0` is commonly used to mean that the program listens on all the IPs available in that machine/server.
|
|
||||||
|
|
||||||
The docs UI would also need the OpenAPI schema to declare that this API `server` is located at `/api/v1` (behind the proxy). For example:
|
|
||||||
|
|
||||||
```JSON hl_lines="4-8"
|
|
||||||
{
|
|
||||||
"openapi": "3.0.2",
|
|
||||||
// More stuff here
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"url": "/api/v1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
// More stuff here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In this example, the "Proxy" could be something like **Traefik**. And the server would be something like **Uvicorn**, running your FastAPI application.
|
|
||||||
|
|
||||||
### Providing the `root_path`
|
|
||||||
|
|
||||||
To achieve this, you can use the command line option `--root-path` like:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ uvicorn main:app --root-path /api/v1
|
|
||||||
|
|
||||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
If you use Hypercorn, it also has the option `--root-path`.
|
|
||||||
|
|
||||||
!!! note "Technical Details"
|
|
||||||
The ASGI specification defines a `root_path` for this use case.
|
|
||||||
|
|
||||||
And the `--root-path` command line option provides that `root_path`.
|
|
||||||
|
|
||||||
### Checking the current `root_path`
|
|
||||||
|
|
||||||
You can get the current `root_path` used by your application for each request, it is part of the `scope` dictionary (that's part of the ASGI spec).
|
|
||||||
|
|
||||||
Here we are including it in the message just for demonstration purposes.
|
|
||||||
|
|
||||||
```Python hl_lines="8"
|
|
||||||
{!../../../docs_src/behind_a_proxy/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, if you start Uvicorn with:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ uvicorn main:app --root-path /api/v1
|
|
||||||
|
|
||||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
The response would be something like:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"message": "Hello World",
|
|
||||||
"root_path": "/api/v1"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Setting the `root_path` in the FastAPI app
|
|
||||||
|
|
||||||
Alternatively, if you don't have a way to provide a command line option like `--root-path` or equivalent, you can set the `root_path` parameter when creating your FastAPI app:
|
|
||||||
|
|
||||||
```Python hl_lines="3"
|
|
||||||
{!../../../docs_src/behind_a_proxy/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
Passing the `root_path` to `FastAPI` would be the equivalent of passing the `--root-path` command line option to Uvicorn or Hypercorn.
|
|
||||||
|
|
||||||
### About `root_path`
|
|
||||||
|
|
||||||
Have in mind that the server (Uvicorn) won't use that `root_path` for anything else than passing it to the app.
|
|
||||||
|
|
||||||
But if you go with your browser to <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000/app</a> you will see the normal response:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"message": "Hello World",
|
|
||||||
"root_path": "/api/v1"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
So, it won't expect to be accessed at `http://127.0.0.1:8000/api/v1/app`.
|
|
||||||
|
|
||||||
Uvicorn will expect the proxy to access Uvicorn at `http://127.0.0.1:8000/app`, and then it would be the proxy's responsibility to add the extra `/api/v1` prefix on top.
|
|
||||||
|
|
||||||
## About proxies with a stripped path prefix
|
|
||||||
|
|
||||||
Have in mind that a proxy with stripped path prefix is only one of the ways to configure it.
|
|
||||||
|
|
||||||
Probably in many cases the default will be that the proxy doesn't have a stripped path prefix.
|
|
||||||
|
|
||||||
In a case like that (without a stripped path prefix), the proxy would listen on something like `https://myawesomeapp.com`, and then if the browser goes to `https://myawesomeapp.com/api/v1/app` and your server (e.g. Uvicorn) listens on `http://127.0.0.1:8000` the proxy (without a stripped path prefix) would access Uvicorn at the same path: `http://127.0.0.1:8000/api/v1/app`.
|
|
||||||
|
|
||||||
## Testing locally with Traefik
|
|
||||||
|
|
||||||
You can easily run the experiment locally with a stripped path prefix using <a href="https://docs.traefik.io/" class="external-link" target="_blank">Traefik</a>.
|
|
||||||
|
|
||||||
<a href="https://github.com/containous/traefik/releases" class="external-link" target="_blank">Download Traefik</a>, it's a single binary, you can extract the compressed file and run it directly from the terminal.
|
|
||||||
|
|
||||||
Then create a file `traefik.toml` with:
|
|
||||||
|
|
||||||
```TOML hl_lines="3"
|
|
||||||
[entryPoints]
|
|
||||||
[entryPoints.http]
|
|
||||||
address = ":9999"
|
|
||||||
|
|
||||||
[providers]
|
|
||||||
[providers.file]
|
|
||||||
filename = "routes.toml"
|
|
||||||
```
|
|
||||||
|
|
||||||
This tells Traefik to listen on port 9999 and to use another file `routes.toml`.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
We are using port 9999 instead of the standard HTTP port 80 so that you don't have to run it with admin (`sudo`) privileges.
|
|
||||||
|
|
||||||
Now create that other file `routes.toml`:
|
|
||||||
|
|
||||||
```TOML hl_lines="5 12 20"
|
|
||||||
[http]
|
|
||||||
[http.middlewares]
|
|
||||||
|
|
||||||
[http.middlewares.api-stripprefix.stripPrefix]
|
|
||||||
prefixes = ["/api/v1"]
|
|
||||||
|
|
||||||
[http.routers]
|
|
||||||
|
|
||||||
[http.routers.app-http]
|
|
||||||
entryPoints = ["http"]
|
|
||||||
service = "app"
|
|
||||||
rule = "PathPrefix(`/api/v1`)"
|
|
||||||
middlewares = ["api-stripprefix"]
|
|
||||||
|
|
||||||
[http.services]
|
|
||||||
|
|
||||||
[http.services.app]
|
|
||||||
[http.services.app.loadBalancer]
|
|
||||||
[[http.services.app.loadBalancer.servers]]
|
|
||||||
url = "http://127.0.0.1:8000"
|
|
||||||
```
|
|
||||||
|
|
||||||
This file configures Traefik to use the path prefix `/api/v1`.
|
|
||||||
|
|
||||||
And then it will redirect its requests to your Uvicorn running on `http://127.0.0.1:8000`.
|
|
||||||
|
|
||||||
Now start Traefik:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ ./traefik --configFile=traefik.toml
|
|
||||||
|
|
||||||
INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
And now start your app with Uvicorn, using the `--root-path` option:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ uvicorn main:app --root-path /api/v1
|
|
||||||
|
|
||||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Check the responses
|
|
||||||
|
|
||||||
Now, if you go to the URL with the port for Uvicorn: <a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app</a>, you will see the normal response:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"message": "Hello World",
|
|
||||||
"root_path": "/api/v1"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Notice that even though you are accessing it at `http://127.0.0.1:8000/app` it shows the `root_path` of `/api/v1`, taken from the option `--root-path`.
|
|
||||||
|
|
||||||
And now open the URL with the port for Traefik, including the path prefix: <a href="http://127.0.0.1:9999/api/v1/app" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/app</a>.
|
|
||||||
|
|
||||||
We get the same response:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"message": "Hello World",
|
|
||||||
"root_path": "/api/v1"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
but this time at the URL with the prefix path provided by the proxy: `/api/v1`.
|
|
||||||
|
|
||||||
Of course, the idea here is that everyone would access the app through the proxy, so the version with the path prefix `/app/v1` is the "correct" one.
|
|
||||||
|
|
||||||
And the version without the path prefix (`http://127.0.0.1:8000/app`), provided by Uvicorn directly, would be exclusively for the _proxy_ (Traefik) to access it.
|
|
||||||
|
|
||||||
That demonstrates how the Proxy (Traefik) uses the path prefix and how the server (Uvicorn) uses the `root_path` from the option `--root-path`.
|
|
||||||
|
|
||||||
### Check the docs UI
|
|
||||||
|
|
||||||
But here's the fun part. ✨
|
|
||||||
|
|
||||||
The "official" way to access the app would be through the proxy with the path prefix that we defined. So, as we would expect, if you try the docs UI served by Uvicorn directly, without the path prefix in the URL, it won't work, because it expects to be accessed through the proxy.
|
|
||||||
|
|
||||||
You can check it at <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/behind-a-proxy/image01.png">
|
|
||||||
|
|
||||||
But if we access the docs UI at the "official" URL using the proxy with port `9999`, at `/api/v1/docs`, it works correctly! 🎉
|
|
||||||
|
|
||||||
You can check it at <a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a>:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/behind-a-proxy/image02.png">
|
|
||||||
|
|
||||||
Right as we wanted it. ✔️
|
|
||||||
|
|
||||||
This is because FastAPI uses this `root_path` to create the default `server` in OpenAPI with the URL provided by `root_path`.
|
|
||||||
|
|
||||||
## Additional servers
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
This is a more advanced use case. Feel free to skip it.
|
|
||||||
|
|
||||||
By default, **FastAPI** will create a `server` in the OpenAPI schema with the URL for the `root_path`.
|
|
||||||
|
|
||||||
But you can also provide other alternative `servers`, for example if you want *the same* docs UI to interact with a staging and production environments.
|
|
||||||
|
|
||||||
If you pass a custom list of `servers` and there's a `root_path` (because your API lives behind a proxy), **FastAPI** will insert a "server" with this `root_path` at the beginning of the list.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```Python hl_lines="4-7"
|
|
||||||
{!../../../docs_src/behind_a_proxy/tutorial003.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
Will generate an OpenAPI schema like:
|
|
||||||
|
|
||||||
```JSON hl_lines="5-7"
|
|
||||||
{
|
|
||||||
"openapi": "3.0.2",
|
|
||||||
// More stuff here
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"url": "/api/v1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://stag.example.com",
|
|
||||||
"description": "Staging environment"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://prod.example.com",
|
|
||||||
"description": "Production environment"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
// More stuff here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Notice the auto-generated server with a `url` value of `/api/v1`, taken from the `root_path`.
|
|
||||||
|
|
||||||
In the docs UI at <a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a> it would look like:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/behind-a-proxy/image03.png">
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The docs UI will interact with the server that you select.
|
|
||||||
|
|
||||||
### Disable automatic server from `root_path`
|
|
||||||
|
|
||||||
If you don't want **FastAPI** to include an automatic server using the `root_path`, you can use the parameter `root_path_in_servers=False`:
|
|
||||||
|
|
||||||
```Python hl_lines="9"
|
|
||||||
{!../../../docs_src/behind_a_proxy/tutorial004.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
and then it won't include it in the OpenAPI schema.
|
|
||||||
|
|
||||||
## Mounting a sub-application
|
|
||||||
|
|
||||||
If you need to mount a sub-application (as described in [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}) while also using a proxy with `root_path`, you can do it normally, as you would expect.
|
|
||||||
|
|
||||||
FastAPI will internally use the `root_path` smartly, so it will just work. ✨
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
# Conditional OpenAPI
|
|
||||||
|
|
||||||
If you needed to, you could use settings and environment variables to configure OpenAPI conditionally depending on the environment, and even disable it entirely.
|
|
||||||
|
|
||||||
## About security, APIs, and docs
|
|
||||||
|
|
||||||
Hiding your documentation user interfaces in production *shouldn't* be the way to protect your API.
|
|
||||||
|
|
||||||
That doesn't add any extra security to your API, the *path operations* will still be available where they are.
|
|
||||||
|
|
||||||
If there's a security flaw in your code, it will still exist.
|
|
||||||
|
|
||||||
Hiding the documentation just makes it more difficult to understand how to interact with your API, and could make it more difficult for you to debug it in production. It could be considered simply a form of <a href="https://en.wikipedia.org/wiki/Security_through_obscurity" class="external-link" target="_blank">Security through obscurity</a>.
|
|
||||||
|
|
||||||
If you want to secure your API, there are several better things you can do, for example:
|
|
||||||
|
|
||||||
* Make sure you have well defined Pydantic models for your request bodies and responses.
|
|
||||||
* Configure any required permissions and roles using dependencies.
|
|
||||||
* Never store plaintext passwords, only password hashes.
|
|
||||||
* Implement and use well-known cryptographic tools, like Passlib and JWT tokens, etc.
|
|
||||||
* Add more granular permission controls with OAuth2 scopes where needed.
|
|
||||||
* ...etc.
|
|
||||||
|
|
||||||
Nevertheless, you might have a very specific use case where you really need to disable the API docs for some environment (e.g. for production) or depending on configurations from environment variables.
|
|
||||||
|
|
||||||
## Conditional OpenAPI from settings and env vars
|
|
||||||
|
|
||||||
You can easily use the same Pydantic settings to configure your generated OpenAPI and the docs UIs.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```Python hl_lines="6 11"
|
|
||||||
{!../../../docs_src/conditional_openapi/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we declare the setting `openapi_url` with the same default of `"/openapi.json"`.
|
|
||||||
|
|
||||||
And then we use it when creating the `FastAPI` app.
|
|
||||||
|
|
||||||
Then you could disable OpenAPI (including the UI docs) by setting the environment variable `OPENAPI_URL` to the empty string, like:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ OPENAPI_URL= uvicorn main:app
|
|
||||||
|
|
||||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Then if you go to the URLs at `/openapi.json`, `/docs`, or `/redoc` you will just get a `404 Not Found` error like:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"detail": "Not Found"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
# Custom Request and APIRoute class
|
|
||||||
|
|
||||||
In some cases, you may want to override the logic used by the `Request` and `APIRoute` classes.
|
|
||||||
|
|
||||||
In particular, this may be a good alternative to logic in a middleware.
|
|
||||||
|
|
||||||
For example, if you want to read or manipulate the request body before it is processed by your application.
|
|
||||||
|
|
||||||
!!! danger
|
|
||||||
This is an "advanced" feature.
|
|
||||||
|
|
||||||
If you are just starting with **FastAPI** you might want to skip this section.
|
|
||||||
|
|
||||||
## Use cases
|
|
||||||
|
|
||||||
Some use cases include:
|
|
||||||
|
|
||||||
* Converting non-JSON request bodies to JSON (e.g. <a href="https://msgpack.org/index.html" class="external-link" target="_blank">`msgpack`</a>).
|
|
||||||
* Decompressing gzip-compressed request bodies.
|
|
||||||
* Automatically logging all request bodies.
|
|
||||||
|
|
||||||
## Handling custom request body encodings
|
|
||||||
|
|
||||||
Let's see how to make use of a custom `Request` subclass to decompress gzip requests.
|
|
||||||
|
|
||||||
And an `APIRoute` subclass to use that custom request class.
|
|
||||||
|
|
||||||
### Create a custom `GzipRequest` class
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
This is a toy example to demonstrate how it works, if you need Gzip support, you can use the provided [`GzipMiddleware`](./middleware.md#gzipmiddleware){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
First, we create a `GzipRequest` class, which will overwrite the `Request.body()` method to decompress the body in the presence of an appropriate header.
|
|
||||||
|
|
||||||
If there's no `gzip` in the header, it will not try to decompress the body.
|
|
||||||
|
|
||||||
That way, the same route class can handle gzip compressed or uncompressed requests.
|
|
||||||
|
|
||||||
```Python hl_lines="8-15"
|
|
||||||
{!../../../docs_src/custom_request_and_route/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create a custom `GzipRoute` class
|
|
||||||
|
|
||||||
Next, we create a custom subclass of `fastapi.routing.APIRoute` that will make use of the `GzipRequest`.
|
|
||||||
|
|
||||||
This time, it will overwrite the method `APIRoute.get_route_handler()`.
|
|
||||||
|
|
||||||
This method returns a function. And that function is what will receive a request and return a response.
|
|
||||||
|
|
||||||
Here we use it to create a `GzipRequest` from the original request.
|
|
||||||
|
|
||||||
```Python hl_lines="18-26"
|
|
||||||
{!../../../docs_src/custom_request_and_route/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! note "Technical Details"
|
|
||||||
A `Request` has a `request.scope` attribute, that's just a Python `dict` containing the metadata related to the request.
|
|
||||||
|
|
||||||
A `Request` also has a `request.receive`, that's a function to "receive" the body of the request.
|
|
||||||
|
|
||||||
The `scope` `dict` and `receive` function are both part of the ASGI specification.
|
|
||||||
|
|
||||||
And those two things, `scope` and `receive`, are what is needed to create a new `Request` instance.
|
|
||||||
|
|
||||||
To learn more about the `Request` check <a href="https://www.starlette.io/requests/" class="external-link" target="_blank">Starlette's docs about Requests</a>.
|
|
||||||
|
|
||||||
The only thing the function returned by `GzipRequest.get_route_handler` does differently is convert the `Request` to a `GzipRequest`.
|
|
||||||
|
|
||||||
Doing this, our `GzipRequest` will take care of decompressing the data (if necessary) before passing it to our *path operations*.
|
|
||||||
|
|
||||||
After that, all of the processing logic is the same.
|
|
||||||
|
|
||||||
But because of our changes in `GzipRequest.body`, the request body will be automatically decompressed when it is loaded by **FastAPI** when needed.
|
|
||||||
|
|
||||||
## Accessing the request body in an exception handler
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
To solve this same problem, it's probably a lot easier to use the `body` in a custom handler for `RequestValidationError` ([Handling Errors](../tutorial/handling-errors.md#use-the-requestvalidationerror-body){.internal-link target=_blank}).
|
|
||||||
|
|
||||||
But this example is still valid and it shows how to interact with the internal components.
|
|
||||||
|
|
||||||
We can also use this same approach to access the request body in an exception handler.
|
|
||||||
|
|
||||||
All we need to do is handle the request inside a `try`/`except` block:
|
|
||||||
|
|
||||||
```Python hl_lines="13 15"
|
|
||||||
{!../../../docs_src/custom_request_and_route/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
If an exception occurs, the`Request` instance will still be in scope, so we can read and make use of the request body when handling the error:
|
|
||||||
|
|
||||||
```Python hl_lines="16-18"
|
|
||||||
{!../../../docs_src/custom_request_and_route/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Custom `APIRoute` class in a router
|
|
||||||
|
|
||||||
You can also set the `route_class` parameter of an `APIRouter`:
|
|
||||||
|
|
||||||
```Python hl_lines="26"
|
|
||||||
{!../../../docs_src/custom_request_and_route/tutorial003.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
In this example, the *path operations* under the `router` will use the custom `TimedRoute` class, and will have an extra `X-Response-Time` header in the response with the time it took to generate the response:
|
|
||||||
|
|
||||||
```Python hl_lines="13-20"
|
|
||||||
{!../../../docs_src/custom_request_and_route/tutorial003.py!}
|
|
||||||
```
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
# Custom Response - HTML, Stream, File, others
|
|
||||||
|
|
||||||
By default, **FastAPI** will return the responses using `JSONResponse`.
|
|
||||||
|
|
||||||
You can override it by returning a `Response` directly as seen in [Return a Response directly](response-directly.md){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
But if you return a `Response` directly, the data won't be automatically converted, and the documentation won't be automatically generated (for example, including the specific "media type", in the HTTP header `Content-Type` as part of the generated OpenAPI).
|
|
||||||
|
|
||||||
But you can also declare the `Response` that you want to be used, in the *path operation decorator*.
|
|
||||||
|
|
||||||
The contents that you return from your *path operation function* will be put inside of that `Response`.
|
|
||||||
|
|
||||||
And if that `Response` has a JSON media type (`application/json`), like is the case with the `JSONResponse` and `UJSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*.
|
|
||||||
|
|
||||||
!!! note
|
|
||||||
If you use a response class with no media type, FastAPI will expect your response to have no content, so it will not document the response format in its generated OpenAPI docs.
|
|
||||||
|
|
||||||
## Use `ORJSONResponse`
|
|
||||||
|
|
||||||
For example, if you are squeezing performance, you can install and use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> and set the response to be `ORJSONResponse`.
|
|
||||||
|
|
||||||
Import the `Response` class (sub-class) you want to use and declare it in the *path operation decorator*.
|
|
||||||
|
|
||||||
```Python hl_lines="2 7"
|
|
||||||
{!../../../docs_src/custom_response/tutorial001b.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! info
|
|
||||||
The parameter `response_class` will also be used to define the "media type" of the response.
|
|
||||||
|
|
||||||
In this case, the HTTP header `Content-Type` will be set to `application/json`.
|
|
||||||
|
|
||||||
And it will be documented as such in OpenAPI.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The `ORJSONResponse` is currently only available in FastAPI, not in Starlette.
|
|
||||||
|
|
||||||
## HTML Response
|
|
||||||
|
|
||||||
To return a response with HTML directly from **FastAPI**, use `HTMLResponse`.
|
|
||||||
|
|
||||||
* Import `HTMLResponse`.
|
|
||||||
* Pass `HTMLResponse` as the parameter `response_class` of your *path operation decorator*.
|
|
||||||
|
|
||||||
```Python hl_lines="2 7"
|
|
||||||
{!../../../docs_src/custom_response/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! info
|
|
||||||
The parameter `response_class` will also be used to define the "media type" of the response.
|
|
||||||
|
|
||||||
In this case, the HTTP header `Content-Type` will be set to `text/html`.
|
|
||||||
|
|
||||||
And it will be documented as such in OpenAPI.
|
|
||||||
|
|
||||||
### Return a `Response`
|
|
||||||
|
|
||||||
As seen in [Return a Response directly](response-directly.md){.internal-link target=_blank}, you can also override the response directly in your *path operation*, by returning it.
|
|
||||||
|
|
||||||
The same example from above, returning an `HTMLResponse`, could look like:
|
|
||||||
|
|
||||||
```Python hl_lines="2 7 19"
|
|
||||||
{!../../../docs_src/custom_response/tutorial003.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
A `Response` returned directly by your *path operation function* won't be documented in OpenAPI (for example, the `Content-Type` won't be documented) and won't be visible in the automatic interactive docs.
|
|
||||||
|
|
||||||
!!! info
|
|
||||||
Of course, the actual `Content-Type` header, status code, etc, will come from the `Response` object your returned.
|
|
||||||
|
|
||||||
### Document in OpenAPI and override `Response`
|
|
||||||
|
|
||||||
If you want to override the response from inside of the function but at the same time document the "media type" in OpenAPI, you can use the `response_class` parameter AND return a `Response` object.
|
|
||||||
|
|
||||||
The `response_class` will then be used only to document the OpenAPI *path operation*, but your `Response` will be used as is.
|
|
||||||
|
|
||||||
#### Return an `HTMLResponse` directly
|
|
||||||
|
|
||||||
For example, it could be something like:
|
|
||||||
|
|
||||||
```Python hl_lines="7 21 23"
|
|
||||||
{!../../../docs_src/custom_response/tutorial004.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
In this example, the function `generate_html_response()` already generates and returns a `Response` instead of returning the HTML in a `str`.
|
|
||||||
|
|
||||||
By returning the result of calling `generate_html_response()`, you are already returning a `Response` that will override the default **FastAPI** behavior.
|
|
||||||
|
|
||||||
But as you passed the `HTMLResponse` in the `response_class` too, **FastAPI** will know how to document it in OpenAPI and the interactive docs as HTML with `text/html`:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/custom-response/image01.png">
|
|
||||||
|
|
||||||
## Available responses
|
|
||||||
|
|
||||||
Here are some of the available responses.
|
|
||||||
|
|
||||||
Have in mind that you can use `Response` to return anything else, or even create a custom sub-class.
|
|
||||||
|
|
||||||
!!! note "Technical Details"
|
|
||||||
You could also use `from starlette.responses import HTMLResponse`.
|
|
||||||
|
|
||||||
**FastAPI** provides the same `starlette.responses` as `fastapi.responses` just as a convenience for you, the developer. But most of the available responses come directly from Starlette.
|
|
||||||
|
|
||||||
### `Response`
|
|
||||||
|
|
||||||
The main `Response` class, all the other responses inherit from it.
|
|
||||||
|
|
||||||
You can return it directly.
|
|
||||||
|
|
||||||
It accepts the following parameters:
|
|
||||||
|
|
||||||
* `content` - A `str` or `bytes`.
|
|
||||||
* `status_code` - An `int` HTTP status code.
|
|
||||||
* `headers` - A `dict` of strings.
|
|
||||||
* `media_type` - A `str` giving the media type. E.g. `"text/html"`.
|
|
||||||
|
|
||||||
FastAPI (actually Starlette) will automatically include a Content-Length header. It will also include a Content-Type header, based on the media_type and appending a charset for text types.
|
|
||||||
|
|
||||||
```Python hl_lines="1 18"
|
|
||||||
{!../../../docs_src/response_directly/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `HTMLResponse`
|
|
||||||
|
|
||||||
Takes some text or bytes and returns an HTML response, as you read above.
|
|
||||||
|
|
||||||
### `PlainTextResponse`
|
|
||||||
|
|
||||||
Takes some text or bytes and returns an plain text response.
|
|
||||||
|
|
||||||
```Python hl_lines="2 7 9"
|
|
||||||
{!../../../docs_src/custom_response/tutorial005.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `JSONResponse`
|
|
||||||
|
|
||||||
Takes some data and returns an `application/json` encoded response.
|
|
||||||
|
|
||||||
This is the default response used in **FastAPI**, as you read above.
|
|
||||||
|
|
||||||
### `ORJSONResponse`
|
|
||||||
|
|
||||||
A fast alternative JSON response using <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>, as you read above.
|
|
||||||
|
|
||||||
### `UJSONResponse`
|
|
||||||
|
|
||||||
An alternative JSON response using <a href="https://github.com/ultrajson/ultrajson" class="external-link" target="_blank">`ujson`</a>.
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
`ujson` is less careful than Python's built-in implementation in how it handles some edge-cases.
|
|
||||||
|
|
||||||
```Python hl_lines="2 7"
|
|
||||||
{!../../../docs_src/custom_response/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
It's possible that `ORJSONResponse` might be a faster alternative.
|
|
||||||
|
|
||||||
### `RedirectResponse`
|
|
||||||
|
|
||||||
Returns an HTTP redirect. Uses a 307 status code (Temporary Redirect) by default.
|
|
||||||
|
|
||||||
```Python hl_lines="2 9"
|
|
||||||
{!../../../docs_src/custom_response/tutorial006.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `StreamingResponse`
|
|
||||||
|
|
||||||
Takes an async generator or a normal generator/iterator and streams the response body.
|
|
||||||
|
|
||||||
```Python hl_lines="2 14"
|
|
||||||
{!../../../docs_src/custom_response/tutorial007.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Using `StreamingResponse` with file-like objects
|
|
||||||
|
|
||||||
If you have a file-like object (e.g. the object returned by `open()`), you can return it in a `StreamingResponse`.
|
|
||||||
|
|
||||||
This includes many libraries to interact with cloud storage, video processing, and others.
|
|
||||||
|
|
||||||
```Python hl_lines="2 10-11"
|
|
||||||
{!../../../docs_src/custom_response/tutorial008.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Notice that here as we are using standard `open()` that doesn't support `async` and `await`, we declare the path operation with normal `def`.
|
|
||||||
|
|
||||||
### `FileResponse`
|
|
||||||
|
|
||||||
Asynchronously streams a file as the response.
|
|
||||||
|
|
||||||
Takes a different set of arguments to instantiate than the other response types:
|
|
||||||
|
|
||||||
* `path` - The filepath to the file to stream.
|
|
||||||
* `headers` - Any custom headers to include, as a dictionary.
|
|
||||||
* `media_type` - A string giving the media type. If unset, the filename or path will be used to infer a media type.
|
|
||||||
* `filename` - If set, this will be included in the response `Content-Disposition`.
|
|
||||||
|
|
||||||
File responses will include appropriate `Content-Length`, `Last-Modified` and `ETag` headers.
|
|
||||||
|
|
||||||
```Python hl_lines="2 10"
|
|
||||||
{!../../../docs_src/custom_response/tutorial009.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Default response class
|
|
||||||
|
|
||||||
When creating a **FastAPI** class instance or an `APIRouter` you can specify which response class to use by default.
|
|
||||||
|
|
||||||
The parameter that defines this is `default_response_class`.
|
|
||||||
|
|
||||||
In the example below, **FastAPI** will use `ORJSONResponse` by default, in all *path operations*, instead of `JSONResponse`.
|
|
||||||
|
|
||||||
```Python hl_lines="2 4"
|
|
||||||
{!../../../docs_src/custom_response/tutorial010.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
You can still override `response_class` in *path operations* as before.
|
|
||||||
|
|
||||||
## Additional documentation
|
|
||||||
|
|
||||||
You can also declare the media type and many other details in OpenAPI using `responses`: [Additional Responses in OpenAPI](additional-responses.md){.internal-link target=_blank}.
|
|
||||||
|
|
@ -1,250 +0,0 @@
|
||||||
# Extending OpenAPI
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
This is a rather advanced feature. You probably can skip it.
|
|
||||||
|
|
||||||
If you are just following the tutorial - user guide, you can probably skip this section.
|
|
||||||
|
|
||||||
If you already know that you need to modify the generated OpenAPI schema, continue reading.
|
|
||||||
|
|
||||||
There are some cases where you might need to modify the generated OpenAPI schema.
|
|
||||||
|
|
||||||
In this section you will see how.
|
|
||||||
|
|
||||||
## The normal process
|
|
||||||
|
|
||||||
The normal (default) process, is as follows.
|
|
||||||
|
|
||||||
A `FastAPI` application (instance) has an `.openapi()` method that is expected to return the OpenAPI schema.
|
|
||||||
|
|
||||||
As part of the application object creation, a *path operation* for `/openapi.json` (or for whatever you set your `openapi_url`) is registered.
|
|
||||||
|
|
||||||
It just returns a JSON response with the result of the application's `.openapi()` method.
|
|
||||||
|
|
||||||
By default, what the method `.openapi()` does is check the property `.openapi_schema` to see if it has contents and return them.
|
|
||||||
|
|
||||||
If it doesn't, it generates them using the utility function at `fastapi.openapi.utils.get_openapi`.
|
|
||||||
|
|
||||||
And that function `get_openapi()` receives as parameters:
|
|
||||||
|
|
||||||
* `title`: The OpenAPI title, shown in the docs.
|
|
||||||
* `version`: The version of your API, e.g. `2.5.0`.
|
|
||||||
* `openapi_version`: The version of the OpenAPI specification used. By default, the latest: `3.0.2`.
|
|
||||||
* `description`: The description of your API.
|
|
||||||
* `routes`: A list of routes, these are each of the registered *path operations*. They are taken from `app.routes`.
|
|
||||||
|
|
||||||
## Overriding the defaults
|
|
||||||
|
|
||||||
Using the information above, you can use the same utility function to generate the OpenAPI schema and override each part that you need.
|
|
||||||
|
|
||||||
For example, let's add <a href="https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md#x-logo" class="external-link" target="_blank">ReDoc's OpenAPI extension to include a custom logo</a>.
|
|
||||||
|
|
||||||
### Normal **FastAPI**
|
|
||||||
|
|
||||||
First, write all your **FastAPI** application as normally:
|
|
||||||
|
|
||||||
```Python hl_lines="1 4 7-9"
|
|
||||||
{!../../../docs_src/extending_openapi/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Generate the OpenAPI schema
|
|
||||||
|
|
||||||
Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function:
|
|
||||||
|
|
||||||
```Python hl_lines="2 15-20"
|
|
||||||
{!../../../docs_src/extending_openapi/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modify the OpenAPI schema
|
|
||||||
|
|
||||||
Now you can add the ReDoc extension, adding a custom `x-logo` to the `info` "object" in the OpenAPI schema:
|
|
||||||
|
|
||||||
```Python hl_lines="21-23"
|
|
||||||
{!../../../docs_src/extending_openapi/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cache the OpenAPI schema
|
|
||||||
|
|
||||||
You can use the property `.openapi_schema` as a "cache", to store your generated schema.
|
|
||||||
|
|
||||||
That way, your application won't have to generate the schema every time a user opens your API docs.
|
|
||||||
|
|
||||||
It will be generated only once, and then the same cached schema will be used for the next requests.
|
|
||||||
|
|
||||||
```Python hl_lines="13-14 24-25"
|
|
||||||
{!../../../docs_src/extending_openapi/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Override the method
|
|
||||||
|
|
||||||
Now you can replace the `.openapi()` method with your new function.
|
|
||||||
|
|
||||||
```Python hl_lines="28"
|
|
||||||
{!../../../docs_src/extending_openapi/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check it
|
|
||||||
|
|
||||||
Once you go to <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> you will see that you are using your custom logo (in this example, **FastAPI**'s logo):
|
|
||||||
|
|
||||||
<img src="/img/tutorial/extending-openapi/image01.png">
|
|
||||||
|
|
||||||
## Self-hosting JavaScript and CSS for docs
|
|
||||||
|
|
||||||
The API docs use **Swagger UI** and **ReDoc**, and each of those need some JavaScript and CSS files.
|
|
||||||
|
|
||||||
By default, those files are served from a <abbr title="Content Delivery Network: A service, normally composed of several servers, that provides static files, like JavaScript and CSS. It's commonly used to serve those files from the server closer to the client, improving performance.">CDN</abbr>.
|
|
||||||
|
|
||||||
But it's possible to customize it, you can set a specific CDN, or serve the files yourself.
|
|
||||||
|
|
||||||
That's useful, for example, if you need your app to keep working even while offline, without open Internet access, or in a local network.
|
|
||||||
|
|
||||||
Here you'll see how to serve those files yourself, in the same FastAPI app, and configure the docs to use them.
|
|
||||||
|
|
||||||
### Project file structure
|
|
||||||
|
|
||||||
Let's say your project file structure looks like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── app
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Now create a directory to store those static files.
|
|
||||||
|
|
||||||
Your new file structure could look like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── app
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── main.py
|
|
||||||
└── static/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Download the files
|
|
||||||
|
|
||||||
Download the static files needed for the docs and put them on that `static/` directory.
|
|
||||||
|
|
||||||
You can probably right-click each link and select an option similar to `Save link as...`.
|
|
||||||
|
|
||||||
**Swagger UI** uses the files:
|
|
||||||
|
|
||||||
* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js" class="external-link" target="_blank">`swagger-ui-bundle.js`</a>
|
|
||||||
* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css" class="external-link" target="_blank">`swagger-ui.css`</a>
|
|
||||||
|
|
||||||
And **ReDoc** uses the file:
|
|
||||||
|
|
||||||
* <a href="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js" class="external-link" target="_blank">`redoc.standalone.js`</a>
|
|
||||||
|
|
||||||
After that, your file structure could look like:
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── app
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── main.py
|
|
||||||
└── static
|
|
||||||
├── redoc.standalone.js
|
|
||||||
├── swagger-ui-bundle.js
|
|
||||||
└── swagger-ui.css
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install `aiofiles`
|
|
||||||
|
|
||||||
Now you need to install `aiofiles`:
|
|
||||||
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ pip install aiofiles
|
|
||||||
|
|
||||||
---> 100%
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Serve the static files
|
|
||||||
|
|
||||||
* Import `StaticFiles`.
|
|
||||||
* "Mount" a `StaticFiles()` instance in a specific path.
|
|
||||||
|
|
||||||
```Python hl_lines="7 11"
|
|
||||||
{!../../../docs_src/extending_openapi/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test the static files
|
|
||||||
|
|
||||||
Start your application and go to <a href="http://127.0.0.1:8000/static/redoc.standalone.js" class="external-link" target="_blank">http://127.0.0.1:8000/static/redoc.standalone.js</a>.
|
|
||||||
|
|
||||||
You should see a very long JavaScript file for **ReDoc**.
|
|
||||||
|
|
||||||
It could start with something like:
|
|
||||||
|
|
||||||
```JavaScript
|
|
||||||
/*!
|
|
||||||
* ReDoc - OpenAPI/Swagger-generated API Reference Documentation
|
|
||||||
* -------------------------------------------------------------
|
|
||||||
* Version: "2.0.0-rc.18"
|
|
||||||
* Repo: https://github.com/Redocly/redoc
|
|
||||||
*/
|
|
||||||
!function(e,t){"object"==typeof exports&&"object"==typeof m
|
|
||||||
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
That confirms that you are being able to serve static files from your app, and that you placed the static files for the docs in the correct place.
|
|
||||||
|
|
||||||
Now we can configure the app to use those static files for the docs.
|
|
||||||
|
|
||||||
### Disable the automatic docs
|
|
||||||
|
|
||||||
The first step is to disable the automatic docs, as those use the CDN by default.
|
|
||||||
|
|
||||||
To disable them, set their URLs to `None` when creating your `FastAPI` app:
|
|
||||||
|
|
||||||
```Python hl_lines="9"
|
|
||||||
{!../../../docs_src/extending_openapi/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Include the custom docs
|
|
||||||
|
|
||||||
Now you can create the *path operations* for the custom docs.
|
|
||||||
|
|
||||||
You can re-use FastAPI's internal functions to create the HTML pages for the docs, and pass them the needed arguments:
|
|
||||||
|
|
||||||
* `openapi_url`: the URL where the HTML page for the docs can get the OpenAPI schema for your API. You can use here the attribute `app.openapi_url`.
|
|
||||||
* `title`: the title of your API.
|
|
||||||
* `oauth2_redirect_url`: you can use `app.swagger_ui_oauth2_redirect_url` here to use the default.
|
|
||||||
* `swagger_js_url`: the URL where the HTML for your Swagger UI docs can get the **JavaScript** file. This is the one that your own app is now serving.
|
|
||||||
* `swagger_css_url`: the URL where the HTML for your Swagger UI docs can get the **CSS** file. This is the one that your own app is now serving.
|
|
||||||
|
|
||||||
And similarly for ReDoc...
|
|
||||||
|
|
||||||
```Python hl_lines="2-6 14-22 25-27 30-36"
|
|
||||||
{!../../../docs_src/extending_openapi/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The *path operation* for `swagger_ui_redirect` is a helper for when you use OAuth2.
|
|
||||||
|
|
||||||
If you integrate your API with an OAuth2 provider, you will be able to authenticate and come back to the API docs with the acquired credentials. And interact with it using the real OAuth2 authentication.
|
|
||||||
|
|
||||||
Swagger UI will handle it behind the scenes for you, but it needs this "redirect" helper.
|
|
||||||
|
|
||||||
### Create a *path operation* to test it
|
|
||||||
|
|
||||||
Now, to be able to test that everything works, create a *path operation*:
|
|
||||||
|
|
||||||
```Python hl_lines="39-41"
|
|
||||||
{!../../../docs_src/extending_openapi/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test it
|
|
||||||
|
|
||||||
Now, you should be able to disconnect your WiFi, go to your docs at <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>, and reload the page.
|
|
||||||
|
|
||||||
And even without Internet, you would be able to see the docs for your API and interact with it.
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
# Advanced User Guide - Intro
|
|
||||||
|
|
||||||
## Additional Features
|
|
||||||
|
|
||||||
The main [Tutorial - User Guide](../tutorial/){.internal-link target=_blank} should be enough to give you a tour through all the main features of **FastAPI**.
|
|
||||||
|
|
||||||
In the next sections you will see other options, configurations, and additional features.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The next sections are **not necessarily "advanced"**.
|
|
||||||
|
|
||||||
And it's possible that for your use case, the solution is in one of them.
|
|
||||||
|
|
||||||
## Read the Tutorial first
|
|
||||||
|
|
||||||
You could still use most of the features in **FastAPI** with the knowledge from the main [Tutorial - User Guide](../tutorial/){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
And the next sections assume you already read it, and assume that you know those main ideas.
|
|
||||||
|
|
||||||
## TestDriven.io course
|
|
||||||
|
|
||||||
If you would like to take an advanced-beginner course to complement this section of the docs, you might want to check: <a href="https://testdriven.io/courses/tdd-fastapi/" class="external-link" target="_blank">Test-Driven Development with FastAPI and Docker</a> by **TestDriven.io**.
|
|
||||||
|
|
||||||
They are currently donating 10% of all profits to the development of **FastAPI**. 🎉 😄
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
# Advanced Middleware
|
|
||||||
|
|
||||||
In the main tutorial you read how to add [Custom Middleware](../tutorial/middleware.md){.internal-link target=_blank} to your application.
|
|
||||||
|
|
||||||
And then you also read how to handle [CORS with the `CORSMiddleware`](../tutorial/cors.md){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
In this section we'll see how to use other middlewares.
|
|
||||||
|
|
||||||
## Adding ASGI middlewares
|
|
||||||
|
|
||||||
As **FastAPI** is based on Starlette and implements the <abbr title="Asynchronous Server Gateway Interface">ASGI</abbr> specification, you can use any ASGI middleware.
|
|
||||||
|
|
||||||
A middleware doesn't have to be made for FastAPI or Starlette to work, as long as it follows the ASGI spec.
|
|
||||||
|
|
||||||
In general, ASGI middlewares are classes that expect to receive an ASGI app as the first argument.
|
|
||||||
|
|
||||||
So, in the documentation for third-party ASGI middlewares they will probably tell you to do something like:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
from unicorn import UnicornMiddleware
|
|
||||||
|
|
||||||
app = SomeASGIApp()
|
|
||||||
|
|
||||||
new_app = UnicornMiddleware(app, some_config="rainbow")
|
|
||||||
```
|
|
||||||
|
|
||||||
But FastAPI (actually Starlette) provides a simpler way to do it that makes sure that the internal middlewares to handle server errors and custom exception handlers work properly.
|
|
||||||
|
|
||||||
For that, you use `app.add_middleware()` (as in the example for CORS).
|
|
||||||
|
|
||||||
```Python
|
|
||||||
from fastapi import FastAPI
|
|
||||||
from unicorn import UnicornMiddleware
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
app.add_middleware(UnicornMiddleware, some_config="rainbow")
|
|
||||||
```
|
|
||||||
|
|
||||||
`app.add_middleware()` receives a middleware class as the first argument and any additional arguments to be passed to the middleware.
|
|
||||||
|
|
||||||
## Integrated middlewares
|
|
||||||
|
|
||||||
**FastAPI** includes several middlewares for common use cases, we'll see next how to use them.
|
|
||||||
|
|
||||||
!!! note "Technical Details"
|
|
||||||
For the next examples, you could also use `from starlette.middleware.something import SomethingMiddleware`.
|
|
||||||
|
|
||||||
**FastAPI** provides several middlewares in `fastapi.middleware` just as a convenience for you, the developer. But most of the available middlewares come directly from Starlette.
|
|
||||||
|
|
||||||
## `HTTPSRedirectMiddleware`
|
|
||||||
|
|
||||||
Enforces that all incoming requests must either be `https` or `wss`.
|
|
||||||
|
|
||||||
Any incoming requests to `http` or `ws` will be redirected to the secure scheme instead.
|
|
||||||
|
|
||||||
```Python hl_lines="2 6"
|
|
||||||
{!../../../docs_src/advanced_middleware/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `TrustedHostMiddleware`
|
|
||||||
|
|
||||||
Enforces that all incoming requests have a correctly set `Host` header, in order to guard against HTTP Host Header attacks.
|
|
||||||
|
|
||||||
```Python hl_lines="2 6-8"
|
|
||||||
{!../../../docs_src/advanced_middleware/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
The following arguments are supported:
|
|
||||||
|
|
||||||
* `allowed_hosts` - A list of domain names that should be allowed as hostnames. Wildcard domains such as `*.example.com` are supported for matching subdomains to allow any hostname either use `allowed_hosts=["*"]` or omit the middleware.
|
|
||||||
|
|
||||||
If an incoming request does not validate correctly then a `400` response will be sent.
|
|
||||||
|
|
||||||
## `GZipMiddleware`
|
|
||||||
|
|
||||||
Handles GZip responses for any request that includes `"gzip"` in the `Accept-Encoding` header.
|
|
||||||
|
|
||||||
The middleware will handle both standard and streaming responses.
|
|
||||||
|
|
||||||
```Python hl_lines="2 6"
|
|
||||||
{!../../../docs_src/advanced_middleware/tutorial003.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
The following arguments are supported:
|
|
||||||
|
|
||||||
* `minimum_size` - Do not GZip responses that are smaller than this minimum size in bytes. Defaults to `500`.
|
|
||||||
|
|
||||||
## Other middlewares
|
|
||||||
|
|
||||||
There are many other ASGI middlewares.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
* <a href="https://docs.sentry.io/platforms/python/asgi/" class="external-link" target="_blank">Sentry</a>
|
|
||||||
* <a href="https://github.com/encode/uvicorn/blob/master/uvicorn/middleware/proxy_headers.py" class="external-link" target="_blank">Uvicorn's `ProxyHeadersMiddleware`</a>
|
|
||||||
* <a href="https://github.com/florimondmanca/msgpack-asgi" class="external-link" target="_blank">MessagePack</a>
|
|
||||||
|
|
||||||
To see other available middlewares check <a href="https://www.starlette.io/middleware/" class="external-link" target="_blank">Starlette's Middleware docs</a> and the <a href="https://github.com/florimondmanca/awesome-asgi" class="external-link" target="_blank">ASGI Awesome List</a>.
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
# OpenAPI Callbacks
|
|
||||||
|
|
||||||
You could create an API with a *path operation* that could trigger a request to an *external API* created by someone else (probably the same developer that would be *using* your API).
|
|
||||||
|
|
||||||
The process that happens when your API app calls the *external API* is named a "callback". Because the software that the external developer wrote sends a request to your API and then your API *calls back*, sending a request to an *external API* (that was probably created by the same developer).
|
|
||||||
|
|
||||||
In this case, you could want to document how that external API *should* look like. What *path operation* it should have, what body it should expect, what response it should return, etc.
|
|
||||||
|
|
||||||
## An app with callbacks
|
|
||||||
|
|
||||||
Let's see all this with an example.
|
|
||||||
|
|
||||||
Imagine you develop an app that allows creating invoices.
|
|
||||||
|
|
||||||
These invoices will have an `id`, `title` (optional), `customer`, and `total`.
|
|
||||||
|
|
||||||
The user of your API (an external developer) will create an invoice in your API with a POST request.
|
|
||||||
|
|
||||||
Then your API will (let's imagine):
|
|
||||||
|
|
||||||
* Send the invoice to some customer of the external developer.
|
|
||||||
* Collect the money.
|
|
||||||
* Send a notification back to the API user (the external developer).
|
|
||||||
* This will be done by sending a POST request (from *your API*) to some *external API* provided by that external developer (this is the "callback").
|
|
||||||
|
|
||||||
## The normal **FastAPI** app
|
|
||||||
|
|
||||||
Let's first see how the normal API app would look like before adding the callback.
|
|
||||||
|
|
||||||
It will have a *path operation* that will receive an `Invoice` body, and a query parameter `callback_url` that will contain the URL for the callback.
|
|
||||||
|
|
||||||
This part is pretty normal, most of the code is probably already familiar to you:
|
|
||||||
|
|
||||||
```Python hl_lines="10-14 37-54"
|
|
||||||
{!../../../docs_src/openapi_callbacks/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The `callback_url` query parameter uses a Pydantic <a href="https://pydantic-docs.helpmanual.io/usage/types/#urls" class="external-link" target="_blank">URL</a> type.
|
|
||||||
|
|
||||||
The only new thing is the `callbacks=messages_callback_router.routes` as an argument to the *path operation decorator*. We'll see what that is next.
|
|
||||||
|
|
||||||
## Documenting the callback
|
|
||||||
|
|
||||||
The actual callback code will depend heavily on your own API app.
|
|
||||||
|
|
||||||
And it will probably vary a lot from one app to the next.
|
|
||||||
|
|
||||||
It could be just one or two lines of code, like:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
callback_url = "https://example.com/api/v1/invoices/events/"
|
|
||||||
requests.post(callback_url, json={"description": "Invoice paid", "paid": True})
|
|
||||||
```
|
|
||||||
|
|
||||||
But possibly the most important part of the callback is making sure that your API user (the external developer) implements the *external API* correctly, according to the data that *your API* is going to send in the request body of the callback, etc.
|
|
||||||
|
|
||||||
So, what we will do next is add the code to document how that *external API* should look like to receive the callback from *your API*.
|
|
||||||
|
|
||||||
That documentation will show up in the Swagger UI at `/docs` in your API, and it will let external developers know how to build the *external API*.
|
|
||||||
|
|
||||||
This example doesn't implement the callback itself (that could be just a line of code), only the documentation part.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The actual callback is just an HTTP request.
|
|
||||||
|
|
||||||
When implementing the callback yourself, you could use something like <a href="https://www.encode.io/httpx/" class="external-link" target="_blank">HTTPX</a> or <a href="https://requests.readthedocs.io/" class="external-link" target="_blank">Requests</a>.
|
|
||||||
|
|
||||||
## Write the callback documentation code
|
|
||||||
|
|
||||||
This code won't be executed in your app, we only need it to *document* how that *external API* should look like.
|
|
||||||
|
|
||||||
But, you already know how to easily create automatic documentation for an API with **FastAPI**.
|
|
||||||
|
|
||||||
So we are going to use that same knowledge to document how the *external API* should look like... by creating the *path operation(s)* that the external API should implement (the ones your API will call).
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
When writing the code to document a callback, it might be useful to imagine that you are that *external developer*. And that you are currently implementing the *external API*, not *your API*.
|
|
||||||
|
|
||||||
Temporarily adopting this point of view (of the *external developer*) can help you feel like it's more obvious where to put the parameters, the Pydantic model for the body, for the response, etc. for that *external API*.
|
|
||||||
|
|
||||||
### Create a callback `APIRouter`
|
|
||||||
|
|
||||||
First create a new `APIRouter` that will contain one or more callbacks.
|
|
||||||
|
|
||||||
```Python hl_lines="5 26"
|
|
||||||
{!../../../docs_src/openapi_callbacks/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create the callback *path operation*
|
|
||||||
|
|
||||||
To create the callback *path operation* use the same `APIRouter` you created above.
|
|
||||||
|
|
||||||
It should look just like a normal FastAPI *path operation*:
|
|
||||||
|
|
||||||
* It should probably have a declaration of the body it should receive, e.g. `body: InvoiceEvent`.
|
|
||||||
* And it could also have a declaration of the response it should return, e.g. `response_model=InvoiceEventReceived`.
|
|
||||||
|
|
||||||
```Python hl_lines="17-19 22-23 29-33"
|
|
||||||
{!../../../docs_src/openapi_callbacks/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
There are 2 main differences from a normal *path operation*:
|
|
||||||
|
|
||||||
* It doesn't need to have any actual code, because your app will never call this code. It's only used to document the *external API*. So, the function could just have `pass`.
|
|
||||||
* The *path* can contain an <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#key-expression" class="external-link" target="_blank">OpenAPI 3 expression</a> (see more below) where it can use variables with parameters and parts of the original request sent to *your API*.
|
|
||||||
|
|
||||||
### The callback path expression
|
|
||||||
|
|
||||||
The callback *path* can have an <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#key-expression" class="external-link" target="_blank">OpenAPI 3 expression</a> that can contain parts of the original request sent to *your API*.
|
|
||||||
|
|
||||||
In this case, it's the `str`:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
"{$callback_url}/invoices/{$request.body.id}"
|
|
||||||
```
|
|
||||||
|
|
||||||
So, if your API user (the external developer) sends a request to *your API* to:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://yourapi.com/invoices/?callback_url=https://www.external.org/events
|
|
||||||
```
|
|
||||||
|
|
||||||
with a JSON body of:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"id": "2expen51ve",
|
|
||||||
"customer": "Mr. Richie Rich",
|
|
||||||
"total": "9999"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then *your API* will process the invoice, and at some point later, send a callback request to the `callback_url` (the *external API*):
|
|
||||||
|
|
||||||
```
|
|
||||||
https://www.external.org/events/invoices/2expen51ve
|
|
||||||
```
|
|
||||||
|
|
||||||
with a JSON body containing something like:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"description": "Payment celebration",
|
|
||||||
"paid": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
and it would expect a response from that *external API* with a JSON body like:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"ok": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Notice how the callback URL used contains the URL received as a query parameter in `callback_url` (`https://www.external.org/events`) and also the invoice `id` from inside of the JSON body (`2expen51ve`).
|
|
||||||
|
|
||||||
### Add the callback router
|
|
||||||
|
|
||||||
At this point you have the *callback path operation(s)* needed (the one(s) that the *external developer* should implement in the *external API*) in the callback router you created above.
|
|
||||||
|
|
||||||
Now use the parameter `callbacks` in *your API's path operation decorator* to pass the attribute `.routes` (that's actually just a `list` of routes/*path operations*) from that callback router:
|
|
||||||
|
|
||||||
```Python hl_lines="36"
|
|
||||||
{!../../../docs_src/openapi_callbacks/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Notice that you are not passing the router itself (`invoices_callback_router`) to `callback=`, but the attribute `.routes`, as in `invoices_callback_router.routes`.
|
|
||||||
|
|
||||||
### Check the docs
|
|
||||||
|
|
||||||
Now you can start your app with Uvicorn and go to <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
|
|
||||||
|
|
||||||
You will see your docs including a "Callback" section for your *path operation* that shows how the *external API* should look like:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/openapi-callbacks/image01.png">
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
# Path Operation Advanced Configuration
|
|
||||||
|
|
||||||
## OpenAPI operationId
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
If you are not an "expert" in OpenAPI, you probably don't need this.
|
|
||||||
|
|
||||||
You can set the OpenAPI `operationId` to be used in your *path operation* with the parameter `operation_id`.
|
|
||||||
|
|
||||||
You would have to make sure that it is unique for each operation.
|
|
||||||
|
|
||||||
```Python hl_lines="6"
|
|
||||||
{!../../../docs_src/path_operation_advanced_configuration/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using the *path operation function* name as the operationId
|
|
||||||
|
|
||||||
If you want to use your APIs' function names as `operationId`s, you can iterate over all of them and override each *path operation's* `operation_id` using their `APIRoute.name`.
|
|
||||||
|
|
||||||
You should do it after adding all your *path operations*.
|
|
||||||
|
|
||||||
```Python hl_lines="2 12-21 24"
|
|
||||||
{!../../../docs_src/path_operation_advanced_configuration/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
If you manually call `app.openapi()`, you should update the `operationId`s before that.
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
If you do this, you have to make sure each one of your *path operation functions* has a unique name.
|
|
||||||
|
|
||||||
Even if they are in different modules (Python files).
|
|
||||||
|
|
||||||
## Exclude from OpenAPI
|
|
||||||
|
|
||||||
To exclude a *path operation* from the generated OpenAPI schema (and thus, from the automatic documentation systems), use the parameter `include_in_schema` and set it to `False`;
|
|
||||||
|
|
||||||
```Python hl_lines="6"
|
|
||||||
{!../../../docs_src/path_operation_advanced_configuration/tutorial003.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advanced description from docstring
|
|
||||||
|
|
||||||
You can limit the lines used from the docstring of a *path operation function* for OpenAPI.
|
|
||||||
|
|
||||||
Adding an `\f` (an escaped "form feed" character) causes **FastAPI** to truncate the output used for OpenAPI at this point.
|
|
||||||
|
|
||||||
It won't show up in the documentation, but other tools (such as Sphinx) will be able to use the rest.
|
|
||||||
|
|
||||||
```Python hl_lines="19-29"
|
|
||||||
{!../../../docs_src/path_operation_advanced_configuration/tutorial004.py!}
|
|
||||||
```
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
# Response Headers
|
|
||||||
|
|
||||||
## Use a `Response` parameter
|
|
||||||
|
|
||||||
You can declare a parameter of type `Response` in your *path operation function* (as you can do for cookies).
|
|
||||||
|
|
||||||
And then you can set headers in that *temporal* response object.
|
|
||||||
|
|
||||||
```Python hl_lines="1 7-8"
|
|
||||||
{!../../../docs_src/response_headers/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
And then you can return any object you need, as you normally would (a `dict`, a database model, etc).
|
|
||||||
|
|
||||||
And if you declared a `response_model`, it will still be used to filter and convert the object you returned.
|
|
||||||
|
|
||||||
**FastAPI** will use that *temporal* response to extract the headers (also cookies and status code), and will put them in the final response that contains the value you returned, filtered by any `response_model`.
|
|
||||||
|
|
||||||
You can also declare the `Response` parameter in dependencies, and set headers (and cookies) in them.
|
|
||||||
|
|
||||||
## Return a `Response` directly
|
|
||||||
|
|
||||||
You can also add headers when you return a `Response` directly.
|
|
||||||
|
|
||||||
Create a response as described in [Return a Response Directly](response-directly.md){.internal-link target=_blank} and pass the headers as an additional parameter:
|
|
||||||
|
|
||||||
```Python hl_lines="10-12"
|
|
||||||
{!../../../docs_src/response_headers/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! note "Technical Details"
|
|
||||||
You could also use `from starlette.responses import Response` or `from starlette.responses import JSONResponse`.
|
|
||||||
|
|
||||||
**FastAPI** provides the same `starlette.responses` as `fastapi.responses` just as a convenience for you, the developer. But most of the available responses come directly from Starlette.
|
|
||||||
|
|
||||||
And as the `Response` can be used frequently to set headers and cookies, **FastAPI** also provides it at `fastapi.Response`.
|
|
||||||
|
|
||||||
## Custom Headers
|
|
||||||
|
|
||||||
Have in mind that custom proprietary headers can be added <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">using the 'X-' prefix</a>.
|
|
||||||
|
|
||||||
But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations (read more in [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), using the parameter `expose_headers` documented in <a href="https://www.starlette.io/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette's CORS docs</a>.
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
# HTTP Basic Auth
|
|
||||||
|
|
||||||
For the simplest cases, you can use HTTP Basic Auth.
|
|
||||||
|
|
||||||
In HTTP Basic Auth, the application expects a header that contains a username and a password.
|
|
||||||
|
|
||||||
If it doesn't receive it, it returns an HTTP 401 "Unauthorized" error.
|
|
||||||
|
|
||||||
And returns a header `WWW-Authenticate` with a value of `Basic`, and an optional `realm` parameter.
|
|
||||||
|
|
||||||
That tells the browser to show the integrated prompt for a username and password.
|
|
||||||
|
|
||||||
Then, when you type that username and password, the browser sends them in the header automatically.
|
|
||||||
|
|
||||||
## Simple HTTP Basic Auth
|
|
||||||
|
|
||||||
* Import `HTTPBasic` and `HTTPBasicCredentials`.
|
|
||||||
* Create a "`security` scheme" using `HTTPBasic`.
|
|
||||||
* Use that `security` with a dependency in your *path operation*.
|
|
||||||
* It returns an object of type `HTTPBasicCredentials`:
|
|
||||||
* It contains the `username` and `password` sent.
|
|
||||||
|
|
||||||
```Python hl_lines="2 6 10"
|
|
||||||
{!../../../docs_src/security/tutorial006.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
When you try to open the URL for the first time (or click the "Execute" button in the docs) the browser will ask you for your username and password:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/security/image12.png">
|
|
||||||
|
|
||||||
## Check the username
|
|
||||||
|
|
||||||
Here's a more complete example.
|
|
||||||
|
|
||||||
Use a dependency to check if the username and password are correct.
|
|
||||||
|
|
||||||
For this, use the Python standard module <a href="https://docs.python.org/3/library/secrets.html" class="external-link" target="_blank">`secrets`</a> to check the username and password:
|
|
||||||
|
|
||||||
```Python hl_lines="1 11-13"
|
|
||||||
{!../../../docs_src/security/tutorial007.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
This will ensure that `credentials.username` is `"stanleyjobson"`, and that `credentials.password` is `"swordfish"`. This would be similar to:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"):
|
|
||||||
# Return some error
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
But by using the `secrets.compare_digest()` it will be secure against a type of attacks called "timing attacks".
|
|
||||||
|
|
||||||
### Timing Attacks
|
|
||||||
|
|
||||||
But what's a "timing attack"?
|
|
||||||
|
|
||||||
Let's imagine some attackers are trying to guess the username and password.
|
|
||||||
|
|
||||||
And they send a request with a username `johndoe` and a password `love123`.
|
|
||||||
|
|
||||||
Then the Python code in your application would be equivalent to something like:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
if "johndoe" == "stanleyjobson" and "love123" == "swordfish":
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
But right at the moment Python compares the first `j` in `johndoe` to the first `s` in `stanleyjobson`, it will return `False`, because it already knows that those two strings are not the same, thinking that "there's no need to waste more computation comparing the rest of the letters". And your application will say "incorrect user or password".
|
|
||||||
|
|
||||||
But then the attackers try with username `stanleyjobsox` and password `love123`.
|
|
||||||
|
|
||||||
And your application code does something like:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Python will have to compare the whole `stanleyjobso` in both `stanleyjobsox` and `stanleyjobson` before realizing that both strings are not the same. So it will take some extra microseconds to reply back "incorrect user or password".
|
|
||||||
|
|
||||||
#### The time to answer helps the attackers
|
|
||||||
|
|
||||||
At that point, by noticing that the server took some microseconds longer to send the "incorrect user or password" response, the attackers will know that they got _something_ right, some of the initial letters were right.
|
|
||||||
|
|
||||||
And then they can try again knowing that it's probably something more similar to `stanleyjobsox` than to `johndoe`.
|
|
||||||
|
|
||||||
#### A "professional" attack
|
|
||||||
|
|
||||||
Of course, the attackers would not try all this by hand, they would write a program to do it, possibly with thousands or millions of tests per second. And would get just one extra correct letter at a time.
|
|
||||||
|
|
||||||
But doing that, in some minutes or hours the attackers would have guessed the correct username and password, with the "help" of our application, just using the time taken to answer.
|
|
||||||
|
|
||||||
#### Fix it with `secrets.compare_digest()`
|
|
||||||
|
|
||||||
But in our code we are actually using `secrets.compare_digest()`.
|
|
||||||
|
|
||||||
In short, it will take the same time to compare `stanleyjobsox` to `stanleyjobson` than it takes to compare `johndoe` to `stanleyjobson`. And the same for the password.
|
|
||||||
|
|
||||||
That way, using `secrets.compare_digest()` in your application code, it will be safe against this whole range of security attacks.
|
|
||||||
|
|
||||||
### Return the error
|
|
||||||
|
|
||||||
After detecting that the credentials are incorrect, return an `HTTPException` with a status code 401 (the same returned when no credentials are provided) and add the header `WWW-Authenticate` to make the browser show the login prompt again:
|
|
||||||
|
|
||||||
```Python hl_lines="15-19"
|
|
||||||
{!../../../docs_src/security/tutorial007.py!}
|
|
||||||
```
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
# Advanced Security - Intro
|
|
||||||
|
|
||||||
## Additional Features
|
|
||||||
|
|
||||||
There are some extra features to handle security apart from the ones covered in the [Tutorial - User Guide: Security](../../tutorial/security/){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The next sections are **not necessarily "advanced"**.
|
|
||||||
|
|
||||||
And it's possible that for your use case, the solution is in one of them.
|
|
||||||
|
|
||||||
## Read the Tutorial first
|
|
||||||
|
|
||||||
The next sections assume you already read the main [Tutorial - User Guide: Security](../../tutorial/security/){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
They are all based on the same concepts, but allow some extra functionalities.
|
|
||||||
|
|
@ -1,382 +0,0 @@
|
||||||
# Settings and Environment Variables
|
|
||||||
|
|
||||||
In many cases your application could need some external settings or configurations, for example secret keys, database credentials, credentials for email services, etc.
|
|
||||||
|
|
||||||
Most of these settings are variable (can change), like database URLs. And many could be sensitive, like secrets.
|
|
||||||
|
|
||||||
For this reason it's common to provide them in environment variables that are read by the application.
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
If you already know what "environment variables" are and how to use them, feel free to skip to the next section below.
|
|
||||||
|
|
||||||
An <a href="https://en.wikipedia.org/wiki/Environment_variable" class="external-link" target="_blank">environment variable</a> (also known as "env var") is a variable that lives outside of the Python code, in the operating system, and could be read by your Python code (or by other programs as well).
|
|
||||||
|
|
||||||
You can create and use environment variables in the shell, without needing Python:
|
|
||||||
|
|
||||||
=== "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>
|
|
||||||
|
|
||||||
=== "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>
|
|
||||||
|
|
||||||
### Read env vars in Python
|
|
||||||
|
|
||||||
You could also create environment variables outside of Python, in the terminal (or with any other method), and then read them in Python.
|
|
||||||
|
|
||||||
For example you could have a file `main.py` with:
|
|
||||||
|
|
||||||
```Python hl_lines="3"
|
|
||||||
import os
|
|
||||||
|
|
||||||
name = os.getenv("MY_NAME", "World")
|
|
||||||
print(f"Hello {name} from Python")
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The second argument to <a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> is the default value to return.
|
|
||||||
|
|
||||||
If not provided, it's `None` by default, here we provide `"World"` as the default value to use.
|
|
||||||
|
|
||||||
Then you could call that Python program:
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
As environment variables can be set outside of the code, but can be read by the code, and don't have to be stored (committed to `git`) with the rest of the files, it's common to use them for configurations or settings.
|
|
||||||
|
|
||||||
You can also create an environment variable only for a specific program invocation, that is only available to that program, and only for its duration.
|
|
||||||
|
|
||||||
To do that, create it right before the program itself, on the same line:
|
|
||||||
|
|
||||||
<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
|
|
||||||
You can read more about it at <a href="https://12factor.net/config" class="external-link" target="_blank">The Twelve-Factor App: Config</a>.
|
|
||||||
|
|
||||||
### Types and validation
|
|
||||||
|
|
||||||
These environment variables can only handle text strings, as they are external to Python and have to be compatible with other programs and the rest of the system (and even with different operating systems, as Linux, Windows, macOS).
|
|
||||||
|
|
||||||
That means that any value read in Python from an environment variable will be a `str`, and any conversion to a different type or validation has to be done in code.
|
|
||||||
|
|
||||||
## Pydantic `Settings`
|
|
||||||
|
|
||||||
Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with <a href="https://pydantic-docs.helpmanual.io/usage/settings/" class="external-link" target="_blank">Pydantic: Settings management</a>.
|
|
||||||
|
|
||||||
### Create the `Settings` object
|
|
||||||
|
|
||||||
Import `BaseSettings` from Pydantic and create a sub-class, very much like with a Pydantic model.
|
|
||||||
|
|
||||||
The same way as with Pydantic models, you declare class attributes with type annotations, and possibly default values.
|
|
||||||
|
|
||||||
You can use all the same validation features and tools you use for Pydantic models, like different data types and additional validations with `Field()`.
|
|
||||||
|
|
||||||
```Python hl_lines="2 5-8 11"
|
|
||||||
{!../../../docs_src/settings/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
If you want something quick to copy and paste, don't use this example, use the last one below.
|
|
||||||
|
|
||||||
Then, when you create an instance of that `Settings` class (in this case, in the `settings` object), Pydantic will read the environment variables in a case-insensitive way, so, an upper-case variable `APP_NAME` will still be read for the attribute `app_name`.
|
|
||||||
|
|
||||||
Next it will convert and validate the data. So, when you use that `settings` object, you will have data of the types you declared (e.g. `items_per_user` will be an `int`).
|
|
||||||
|
|
||||||
### Use the `settings`
|
|
||||||
|
|
||||||
Then you can use the new `settings` object in your application:
|
|
||||||
|
|
||||||
```Python hl_lines="18-20"
|
|
||||||
{!../../../docs_src/settings/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run the server
|
|
||||||
|
|
||||||
Next, you would run the server passing the configurations as environment variables, for example you could set an `ADMIN_EMAIL` and `APP_NAME` with:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" uvicorn main:app
|
|
||||||
|
|
||||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
To set multiple env vars for a single command just separate them with a space, and put them all before the command.
|
|
||||||
|
|
||||||
And then the `admin_email` setting would be set to `"deadpool@example.com"`.
|
|
||||||
|
|
||||||
The `app_name` would be `"ChimichangApp"`.
|
|
||||||
|
|
||||||
And the `items_per_user` would keep its default value of `50`.
|
|
||||||
|
|
||||||
## Settings in another module
|
|
||||||
|
|
||||||
You could put those settings in another module file as you saw in [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
For example, you could have a file `config.py` with:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
{!../../../docs_src/settings/app01/config.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
And then use it in a file `main.py`:
|
|
||||||
|
|
||||||
```Python hl_lines="3 11-13"
|
|
||||||
{!../../../docs_src/settings/app01/main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
You would also need a file `__init__.py` as you saw on [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
## Settings in a dependency
|
|
||||||
|
|
||||||
In some occasions it might be useful to provide the settings from a dependency, instead of having a global object with `settings` that is used everywhere.
|
|
||||||
|
|
||||||
This could be especially useful during testing, as it's very easy to override a dependency with your own custom settings.
|
|
||||||
|
|
||||||
### The config file
|
|
||||||
|
|
||||||
Coming from the previous example, your `config.py` file could look like:
|
|
||||||
|
|
||||||
```Python hl_lines="10"
|
|
||||||
{!../../../docs_src/settings/app02/config.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
Notice that now we don't create a default instance `settings = Settings()`.
|
|
||||||
|
|
||||||
### The main app file
|
|
||||||
|
|
||||||
Now we create a dependency that returns a new `config.Settings()`.
|
|
||||||
|
|
||||||
```Python hl_lines="5 11-12"
|
|
||||||
{!../../../docs_src/settings/app02/main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
We'll discuss the `@lru_cache()` in a bit.
|
|
||||||
|
|
||||||
For now you can assume `get_settings()` is a normal function.
|
|
||||||
|
|
||||||
And then we can require it from the *path operation function* as a dependency and use it anywhere we need it.
|
|
||||||
|
|
||||||
```Python hl_lines="16 18-20"
|
|
||||||
{!../../../docs_src/settings/app02/main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Settings and testing
|
|
||||||
|
|
||||||
Then it would be very easy to provide a different settings object during testing by creating a dependency override for `get_settings`:
|
|
||||||
|
|
||||||
```Python hl_lines="8-9 12 21"
|
|
||||||
{!../../../docs_src/settings/app02/test_main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
In the dependency override we set a new value for the `admin_email` when creating the new `Settings` object, and then we return that new object.
|
|
||||||
|
|
||||||
Then we can test that it is used.
|
|
||||||
|
|
||||||
## Reading a `.env` file
|
|
||||||
|
|
||||||
If you have many settings that possibly change a lot, maybe in different environments, it might be useful to put them on a file and then read them from it as if they were environment variables.
|
|
||||||
|
|
||||||
This practice is common enough that it has a name, these environment variables are commonly placed in a file `.env`, and the file is called a "dotenv".
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
A file starting with a dot (`.`) is a hidden file in Unix-like systems, like Linux and macOS.
|
|
||||||
|
|
||||||
But a dotenv file doesn't really have to have that exact filename.
|
|
||||||
|
|
||||||
Pydantic has support for reading from these types of files using an external library. You can read more at <a href="https://pydantic-docs.helpmanual.io/usage/settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic Settings: Dotenv (.env) support</a>.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
For this to work, you need to `pip install python-dotenv`.
|
|
||||||
|
|
||||||
### The `.env` file
|
|
||||||
|
|
||||||
You could have a `.env` file with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ADMIN_EMAIL="deadpool@example.com"
|
|
||||||
APP_NAME="ChimichangApp"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Read settings from `.env`
|
|
||||||
|
|
||||||
And then update your `config.py` with:
|
|
||||||
|
|
||||||
```Python hl_lines="9-10"
|
|
||||||
{!../../../docs_src/settings/app03/config.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we create a class `Config` inside of your Pydantic `Settings` class, and set the `env_file` to the filename with the dotenv file we want to use.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The `Config` class is used just for Pydantic configuration. You can read more at <a href="https://pydantic-docs.helpmanual.io/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>
|
|
||||||
|
|
||||||
### Creating the `Settings` only once with `lru_cache`
|
|
||||||
|
|
||||||
Reading a file from disk is normally a costly (slow) operation, so you probably want to do it only once and then re-use the same settings object, instead of reading it for each request.
|
|
||||||
|
|
||||||
But every time we do:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
config.Settings()
|
|
||||||
```
|
|
||||||
|
|
||||||
a new `Settings` object would be created, and at creation it would read the `.env` file again.
|
|
||||||
|
|
||||||
If the dependency function was just like:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
def get_settings():
|
|
||||||
return config.Settings()
|
|
||||||
```
|
|
||||||
|
|
||||||
we would create that object for each request, and we would be reading the `.env` file for each request. ⚠️
|
|
||||||
|
|
||||||
But as we are using the `@lru_cache()` decorator on top, the `Settings` object will be created only once, the first time it's called. ✔️
|
|
||||||
|
|
||||||
```Python hl_lines="1 10"
|
|
||||||
{!../../../docs_src/settings/app03/main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then for any subsequent calls of `get_settings()` in the dependencies for the next requests, instead of executing the internal code of `get_settings()` and creating a new `Settings` object, it will return the same object that was returned on the first call, again and again.
|
|
||||||
|
|
||||||
#### `lru_cache` Technical Details
|
|
||||||
|
|
||||||
`@lru_cache()` modifies the function it decorates to return the same value that was returned the first time, instead of computing it again, executing the code of the function every time.
|
|
||||||
|
|
||||||
So, the function below it will be executed once for each combination of arguments. And then the values returned by each of those combinations of arguments will be used again and again whenever the function is called with exactly the same combination of arguments.
|
|
||||||
|
|
||||||
For example, if you have a function:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
@lru_cache()
|
|
||||||
def say_hi(name: str, salutation: str = "Ms."):
|
|
||||||
return f"Hello {salutation} {name}"
|
|
||||||
```
|
|
||||||
|
|
||||||
your program could execute like this:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
|
|
||||||
participant code as Code
|
|
||||||
participant function as say_hi()
|
|
||||||
participant execute as Execute function
|
|
||||||
|
|
||||||
rect rgba(0, 255, 0, .1)
|
|
||||||
code ->> function: say_hi(name="Camila")
|
|
||||||
function ->> execute: execute function code
|
|
||||||
execute ->> code: return the result
|
|
||||||
end
|
|
||||||
|
|
||||||
rect rgba(0, 255, 255, .1)
|
|
||||||
code ->> function: say_hi(name="Camila")
|
|
||||||
function ->> code: return stored result
|
|
||||||
end
|
|
||||||
|
|
||||||
rect rgba(0, 255, 0, .1)
|
|
||||||
code ->> function: say_hi(name="Rick")
|
|
||||||
function ->> execute: execute function code
|
|
||||||
execute ->> code: return the result
|
|
||||||
end
|
|
||||||
|
|
||||||
rect rgba(0, 255, 0, .1)
|
|
||||||
code ->> function: say_hi(name="Rick", salutation="Mr.")
|
|
||||||
function ->> execute: execute function code
|
|
||||||
execute ->> code: return the result
|
|
||||||
end
|
|
||||||
|
|
||||||
rect rgba(0, 255, 255, .1)
|
|
||||||
code ->> function: say_hi(name="Rick")
|
|
||||||
function ->> code: return stored result
|
|
||||||
end
|
|
||||||
|
|
||||||
rect rgba(0, 255, 255, .1)
|
|
||||||
code ->> function: say_hi(name="Camila")
|
|
||||||
function ->> code: return stored result
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
In the case of our dependency `get_settings()`, the function doesn't even take any arguments, so it always returns the same value.
|
|
||||||
|
|
||||||
That way, it behaves almost as if it was just a global variable. But as it uses a dependency function, then we can override it easily for testing.
|
|
||||||
|
|
||||||
`@lru_cache()` is part of `functools` which is part of Python's standard library, you can read more about it in the <a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">Python docs for `@lru_cache()`</a>.
|
|
||||||
|
|
||||||
## Recap
|
|
||||||
|
|
||||||
You can use Pydantic Settings to handle the settings or configurations for your application, with all the power of Pydantic models.
|
|
||||||
|
|
||||||
* By using a dependency you can simplify testing.
|
|
||||||
* You can use `.env` files with it.
|
|
||||||
* Using `@lru_cache()` lets you avoid reading the dotenv file again and again for each request, while allowing you to override it during testing.
|
|
||||||
|
|
@ -1,529 +0,0 @@
|
||||||
# SQL (Relational) Databases with Peewee
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
If you are just starting, the tutorial [SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank} that uses SQLAlchemy should be enough.
|
|
||||||
|
|
||||||
Feel free to skip this.
|
|
||||||
|
|
||||||
If you are starting a project from scratch, you are probably better off with SQLAlchemy ORM ([SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank}), or any other async ORM.
|
|
||||||
|
|
||||||
If you already have a code base that uses <a href="https://docs.peewee-orm.com/en/latest/" class="external-link" target="_blank">Peewee ORM</a>, you can check here how to use it with **FastAPI**.
|
|
||||||
|
|
||||||
!!! warning "Python 3.7+ required"
|
|
||||||
You will need Python 3.7 or above to safely use Peewee with FastAPI.
|
|
||||||
|
|
||||||
## Peewee for async
|
|
||||||
|
|
||||||
Peewee was not designed for async frameworks, or with them in mind.
|
|
||||||
|
|
||||||
Peewee has some heavy assumptions about its defaults and about how it should be used.
|
|
||||||
|
|
||||||
If you are developing an application with an older non-async framework, and can work with all its defaults, **it can be a great tool**.
|
|
||||||
|
|
||||||
But if you need to change some of the defaults, support more than one predefined database, work with an async framework (like FastAPI), etc, you will need to add quite some complex extra code to override those defaults.
|
|
||||||
|
|
||||||
Nevertheless, it's possible to do it, and here you'll see exactly what code you have to add to be able to use Peewee with FastAPI.
|
|
||||||
|
|
||||||
!!! note "Technical Details"
|
|
||||||
You can read more about Peewee's stand about async in Python <a href="https://docs.peewee-orm.com/en/latest/peewee/database.html#async-with-gevent" class="external-link" target="_blank">in the docs</a>, <a href="https://github.com/coleifer/peewee/issues/263#issuecomment-517347032" class="external-link" target="_blank">an issue</a>, <a href="https://github.com/coleifer/peewee/pull/2072#issuecomment-563215132" class="external-link" target="_blank">a PR</a>.
|
|
||||||
|
|
||||||
## The same app
|
|
||||||
|
|
||||||
We are going to create the same application as in the SQLAlchemy tutorial ([SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank}).
|
|
||||||
|
|
||||||
Most of the code is actually the same.
|
|
||||||
|
|
||||||
So, we are going to focus only on the differences.
|
|
||||||
|
|
||||||
## File structure
|
|
||||||
|
|
||||||
Let's say you have a directory named `my_super_project` that contains a sub-directory called `sql_app` with a structure like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
└── sql_app
|
|
||||||
├── __init__.py
|
|
||||||
├── crud.py
|
|
||||||
├── database.py
|
|
||||||
├── main.py
|
|
||||||
└── schemas.py
|
|
||||||
```
|
|
||||||
|
|
||||||
This is almost the same structure as we had for the SQLAlchemy tutorial.
|
|
||||||
|
|
||||||
Now let's see what each file/module does.
|
|
||||||
|
|
||||||
## Create the Peewee parts
|
|
||||||
|
|
||||||
Let's refer to the file `sql_app/database.py`.
|
|
||||||
|
|
||||||
### The standard Peewee code
|
|
||||||
|
|
||||||
Let's first check all the normal Peewee code, create a Peewee database:
|
|
||||||
|
|
||||||
```Python hl_lines="3 5 22"
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/database.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Have in mind that if you wanted to use a different database, like PostgreSQL, you couldn't just change the string. You would need to use a different Peewee database class.
|
|
||||||
|
|
||||||
#### Note
|
|
||||||
|
|
||||||
The argument:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
check_same_thread=False
|
|
||||||
```
|
|
||||||
|
|
||||||
is equivalent to the one in the SQLAlchemy tutorial:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
connect_args={"check_same_thread": False}
|
|
||||||
```
|
|
||||||
|
|
||||||
...it is needed only for `SQLite`.
|
|
||||||
|
|
||||||
!!! info "Technical Details"
|
|
||||||
|
|
||||||
Exactly the same technical details as in [SQL (Relational) Databases](../tutorial/sql-databases.md#note){.internal-link target=_blank} apply.
|
|
||||||
|
|
||||||
### Make Peewee async-compatible `PeeweeConnectionState`
|
|
||||||
|
|
||||||
The main issue with Peewee and FastAPI is that Peewee relies heavily on <a href="https://docs.python.org/3/library/threading.html#thread-local-data" class="external-link" target="_blank">Python's `threading.local`</a>, and it doesn't have a direct way to override it or let you handle connections/sessions directly (as is done in the SQLAlchemy tutorial).
|
|
||||||
|
|
||||||
And `threading.local` is not compatible with the new async features of modern Python.
|
|
||||||
|
|
||||||
!!! note "Technical Details"
|
|
||||||
`threading.local` is used to have a "magic" variable that has a different value for each thread.
|
|
||||||
|
|
||||||
This was useful in older frameworks designed to have one single thread per request, no more, no less.
|
|
||||||
|
|
||||||
Using this, each request would have its own database connection/session, which is the actual final goal.
|
|
||||||
|
|
||||||
But FastAPI, using the new async features, could handle more than one request on the same thread. And at the same time, for a single request, it could run multiple things in different threads (in a threadpool), depending on if you use `async def` or normal `def`. This is what gives all the performance improvements to FastAPI.
|
|
||||||
|
|
||||||
But Python 3.7 and above provide a more advanced alternative to `threading.local`, that can also be used in the places where `threading.local` would be used, but is compatible with the new async features.
|
|
||||||
|
|
||||||
We are going to use that. It's called <a href="https://docs.python.org/3/library/contextvars.html" class="external-link" target="_blank">`contextvars`</a>.
|
|
||||||
|
|
||||||
We are going to override the internal parts of Peewee that use `threading.local` and replace them with `contextvars`, with the corresponding updates.
|
|
||||||
|
|
||||||
This might seem a bit complex (and it actually is), you don't really need to completely understand how it works to use it.
|
|
||||||
|
|
||||||
We will create a `PeeweeConnectionState`:
|
|
||||||
|
|
||||||
```Python hl_lines="10-19"
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/database.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
This class inherits from a special internal class used by Peewee.
|
|
||||||
|
|
||||||
It has all the logic to make Peewee use `contextvars` instead of `threading.local`.
|
|
||||||
|
|
||||||
`contextvars` works a bit differently than `threading.local`. But the rest of Peewee's internal code assumes that this class works with `threading.local`.
|
|
||||||
|
|
||||||
So, we need to do some extra tricks to make it work as if it was just using `threading.local`. The `__init__`, `__setattr__`, and `__getattr__` implement all the required tricks for this to be used by Peewee without knowing that it is now compatible with FastAPI.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
This will just make Peewee behave correctly when used with FastAPI. Not randomly opening or closing connections that are being used, creating errors, etc.
|
|
||||||
|
|
||||||
But it doesn't give Peewee async super-powers. You should still use normal `def` functions and not `async def`.
|
|
||||||
|
|
||||||
### Use the custom `PeeweeConnectionState` class
|
|
||||||
|
|
||||||
Now, overwrite the `._state` internal attribute in the Peewee database `db` object using the new `PeeweeConnectionState`:
|
|
||||||
|
|
||||||
```Python hl_lines="24"
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/database.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Make sure you overwrite `db._state` *after* creating `db`.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
You would do the same for any other Peewee database, including `PostgresqlDatabase`, `MySQLDatabase`, etc.
|
|
||||||
|
|
||||||
## Create the database models
|
|
||||||
|
|
||||||
Let's now see the file `sql_app/models.py`.
|
|
||||||
|
|
||||||
### Create Peewee models for our data
|
|
||||||
|
|
||||||
Now create the Peewee models (classes) for `User` and `Item`.
|
|
||||||
|
|
||||||
This is the same you would do if you followed the Peewee tutorial and updated the models to have the same data as in the SQLAlchemy tutorial.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Peewee also uses the term "**model**" to refer to these classes and instances that interact with the database.
|
|
||||||
|
|
||||||
But Pydantic also uses the term "**model**" to refer to something different, the data validation, conversion, and documentation classes and instances.
|
|
||||||
|
|
||||||
Import `db` from `database` (the file `database.py` from above) and use it here.
|
|
||||||
|
|
||||||
```Python hl_lines="3 6-12 15-21"
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/models.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Peewee creates several magic attributes.
|
|
||||||
|
|
||||||
It will automatically add an `id` attribute as an integer to be the primary key.
|
|
||||||
|
|
||||||
It will chose the name of the tables based on the class names.
|
|
||||||
|
|
||||||
For the `Item`, it will create an attribute `owner_id` with the integer ID of the `User`. But we don't declare it anywhere.
|
|
||||||
|
|
||||||
## Create the Pydantic models
|
|
||||||
|
|
||||||
Now let's check the file `sql_app/schemas.py`.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
To avoid confusion between the Peewee *models* and the Pydantic *models*, we will have the file `models.py` with the Peewee models, and the file `schemas.py` with the Pydantic models.
|
|
||||||
|
|
||||||
These Pydantic models define more or less a "schema" (a valid data shape).
|
|
||||||
|
|
||||||
So this will help us avoiding confusion while using both.
|
|
||||||
|
|
||||||
### Create the Pydantic *models* / schemas
|
|
||||||
|
|
||||||
Create all the same Pydantic models as in the SQLAlchemy tutorial:
|
|
||||||
|
|
||||||
```Python hl_lines="16-18 21-22 25-30 34-35 38-39 42-48"
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Here we are creating the models with an `id`.
|
|
||||||
|
|
||||||
We didn't explicitly specify an `id` attribute in the Peewee models, but Peewee adds one automatically.
|
|
||||||
|
|
||||||
We are also adding the magic `owner_id` attribute to `Item`.
|
|
||||||
|
|
||||||
### Create a `PeeweeGetterDict` for the Pydantic *models* / schemas
|
|
||||||
|
|
||||||
When you access a relationship in a Peewee object, like in `some_user.items`, Peewee doesn't provide a `list` of `Item`.
|
|
||||||
|
|
||||||
It provides a special custom object of class `ModelSelect`.
|
|
||||||
|
|
||||||
It's possible to create a `list` of its items with `list(some_user.items)`.
|
|
||||||
|
|
||||||
But the object itself is not a `list`. And it's also not an actual Python <a href="https://docs.python.org/3/glossary.html#term-generator" class="external-link" target="_blank">generator</a>. Because of this, Pydantic doesn't know by default how to convert it to a `list` of Pydantic *models* / schemas.
|
|
||||||
|
|
||||||
But recent versions of Pydantic allow providing a custom class that inherits from `pydantic.utils.GetterDict`, to provide the functionality used when using the `orm_mode = True` to retrieve the values for ORM model attributes.
|
|
||||||
|
|
||||||
We are going to create a custom `PeeweeGetterDict` class and use it in all the same Pydantic *models* / schemas that use `orm_mode`:
|
|
||||||
|
|
||||||
```Python hl_lines="3 8-13 31 49"
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we are checking if the attribute that is being accessed (e.g. `.items` in `some_user.items`) is an instance of `peewee.ModelSelect`.
|
|
||||||
|
|
||||||
And if that's the case, just return a `list` with it.
|
|
||||||
|
|
||||||
And then we use it in the Pydantic *models* / schemas that use `orm_mode = True`, with the configuration variable `getter_dict = PeeweeGetterDict`.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
We only need to create one `PeeweeGetterDict` class, and we can use it in all the Pydantic *models* / schemas.
|
|
||||||
|
|
||||||
## CRUD utils
|
|
||||||
|
|
||||||
Now let's see the file `sql_app/crud.py`.
|
|
||||||
|
|
||||||
### Create all the CRUD utils
|
|
||||||
|
|
||||||
Create all the same CRUD utils as in the SQLAlchemy tutorial, all the code is very similar:
|
|
||||||
|
|
||||||
```Python hl_lines="1 4-5 8-9 12-13 16-20 23-24 27-30"
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/crud.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
There are some differences with the code for the SQLAlchemy tutorial.
|
|
||||||
|
|
||||||
We don't pass a `db` attribute around. Instead we use the models directly. This is because the `db` object is a global object, that includes all the connection logic. That's why we had to do all the `contextvars` updates above.
|
|
||||||
|
|
||||||
Aso, when returning several objects, like in `get_users`, we directly call `list`, like in:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
list(models.User.select())
|
|
||||||
```
|
|
||||||
|
|
||||||
This is for the same reason that we had to create a custom `PeeweeGetterDict`. But by returning something that is already a `list` instead of the `peewee.ModelSelect` the `response_model` in the *path operation* with `List[models.User]` (that we'll see later) will work correctly.
|
|
||||||
|
|
||||||
## Main **FastAPI** app
|
|
||||||
|
|
||||||
And now in the file `sql_app/main.py` let's integrate and use all the other parts we created before.
|
|
||||||
|
|
||||||
### Create the database tables
|
|
||||||
|
|
||||||
In a very simplistic way create the database tables:
|
|
||||||
|
|
||||||
```Python hl_lines="9-11"
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create a dependency
|
|
||||||
|
|
||||||
Create a dependency that will connect the database right at the beginning of a request and disconnect it at the end:
|
|
||||||
|
|
||||||
```Python hl_lines="23-29"
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we have an empty `yield` because we are actually not using the database object directly.
|
|
||||||
|
|
||||||
It is connecting to the database and storing the connection data in an internal variable that is independent for each request (using the `contextvars` tricks from above).
|
|
||||||
|
|
||||||
Because the database connection is potentially I/O blocking, this dependency is created with a normal `def` function.
|
|
||||||
|
|
||||||
And then, in each *path operation function* that needs to access the database we add it as a dependency.
|
|
||||||
|
|
||||||
But we are not using the value given by this dependency (it actually doesn't give any value, as it has an empty `yield`). So, we don't add it to the *path operation function* but to the *path operation decorator* in the `dependencies` parameter:
|
|
||||||
|
|
||||||
```Python hl_lines="32 40 47 59 65 72"
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Context variable sub-dependency
|
|
||||||
|
|
||||||
For all the `contextvars` parts to work, we need to make sure we have an independent value in the `ContextVar` for each request that uses the database, and that value will be used as the database state (connection, transactions, etc) for the whole request.
|
|
||||||
|
|
||||||
For that, we need to create another `async` dependency `reset_db_state()` that is used as a sub-dependency in `get_db()`. It will set the value for the context variable (with just a default `dict`) that will be used as the database state for the whole request. And then the dependency `get_db()` will store in it the database state (connection, transactions, etc).
|
|
||||||
|
|
||||||
```Python hl_lines="18-20"
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
For the **next request**, as we will reset that context variable again in the `async` dependency `reset_db_state()` and then create a new connection in the `get_db()` dependency, that new request will have its own database state (connection, transactions, etc).
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
As FastAPI is an async framework, one request could start being processed, and before finishing, another request could be received and start processing as well, and it all could be processed in the same thread.
|
|
||||||
|
|
||||||
But context variables are aware of these async features, so, a Peewee database state set in the `async` dependency `reset_db_state()` will keep its own data throughout the entire request.
|
|
||||||
|
|
||||||
And at the same time, the other concurrent request will have its own database state that will be independent for the whole request.
|
|
||||||
|
|
||||||
#### Peewee Proxy
|
|
||||||
|
|
||||||
If you are using a <a href="https://docs.peewee-orm.com/en/latest/peewee/database.html#dynamically-defining-a-database" class="external-link" target="_blank">Peewee Proxy</a>, the actual database is at `db.obj`.
|
|
||||||
|
|
||||||
So, you would reset it with:
|
|
||||||
|
|
||||||
```Python hl_lines="3-4"
|
|
||||||
async def reset_db_state():
|
|
||||||
database.db.obj._state._state.set(db_state_default.copy())
|
|
||||||
database.db.obj._state.reset()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create your **FastAPI** *path operations*
|
|
||||||
|
|
||||||
Now, finally, here's the standard **FastAPI** *path operations* code.
|
|
||||||
|
|
||||||
```Python hl_lines="32-37 40-43 46-53 56-62 65-68 71-79"
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### About `def` vs `async def`
|
|
||||||
|
|
||||||
The same as with SQLAlchemy, we are not doing something like:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
user = await models.User.select().first()
|
|
||||||
```
|
|
||||||
|
|
||||||
...but instead we are using:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
user = models.User.select().first()
|
|
||||||
```
|
|
||||||
|
|
||||||
So, again, we should declare the *path operation functions* and the dependency without `async def`, just with a normal `def`, as:
|
|
||||||
|
|
||||||
```Python hl_lines="2"
|
|
||||||
# Something goes here
|
|
||||||
def read_users(skip: int = 0, limit: int = 100):
|
|
||||||
# Something goes here
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Peewee with async
|
|
||||||
|
|
||||||
This example includes an extra *path operation* that simulates a long processing request with `time.sleep(sleep_time)`.
|
|
||||||
|
|
||||||
It will have the database connection open at the beginning and will just wait some seconds before replying back. And each new request will wait one second less.
|
|
||||||
|
|
||||||
This will easily let you test that your app with Peewee and FastAPI is behaving correctly with all the stuff about threads.
|
|
||||||
|
|
||||||
If you want to check how Peewee would break your app if used without modification, go the the `sql_app/database.py` file and comment the line:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
# db._state = PeeweeConnectionState()
|
|
||||||
```
|
|
||||||
|
|
||||||
And in the file `sql_app/main.py` file, comment the body of the `async` dependency `reset_db_state()` and replace it with a `pass`:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
async def reset_db_state():
|
|
||||||
# database.db._state._state.set(db_state_default.copy())
|
|
||||||
# database.db._state.reset()
|
|
||||||
pass
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run your app with Uvicorn:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ uvicorn sql_app.main:app --reload
|
|
||||||
|
|
||||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Open your browser at <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> and create a couple of users.
|
|
||||||
|
|
||||||
Then open 10 tabs at <a href="http://127.0.0.1:8000/docs#/default/read_slow_users_slowusers__get" class="external-link" target="_blank">http://127.0.0.1:8000/docs#/default/read_slow_users_slowusers__get</a> at the same time.
|
|
||||||
|
|
||||||
Go to the *path operation* "Get `/slowusers/`" in all of the tabs. Use the "Try it out" button and execute the request in each tab, one right after the other.
|
|
||||||
|
|
||||||
The tabs will wait for a bit and then some of them will show `Internal Server Error`.
|
|
||||||
|
|
||||||
### What happens
|
|
||||||
|
|
||||||
The first tab will make your app create a connection to the database and wait for some seconds before replying back and closing the database connection.
|
|
||||||
|
|
||||||
Then, for the request in the next tab, your app will wait for one second less, and so on.
|
|
||||||
|
|
||||||
This means that it will end up finishing some of the last tabs' requests earlier than some of the previous ones.
|
|
||||||
|
|
||||||
Then one the last requests that wait less seconds will try to open a database connection, but as one of those previous requests for the other tabs will probably be handled in the same thread as the first one, it will have the same database connection that is already open, and Peewee will throw an error and you will see it in the terminal, and the response will have an `Internal Server Error`.
|
|
||||||
|
|
||||||
This will probably happen for more than one of those tabs.
|
|
||||||
|
|
||||||
If you had multiple clients talking to your app exactly at the same time, this is what could happen.
|
|
||||||
|
|
||||||
And as your app starts to handle more and more clients at the same time, the waiting time in a single request needs to be shorter and shorter to trigger the error.
|
|
||||||
|
|
||||||
### Fix Peewee with FastAPI
|
|
||||||
|
|
||||||
Now go back to the file `sql_app/database.py`, and uncomment the line:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
db._state = PeeweeConnectionState()
|
|
||||||
```
|
|
||||||
|
|
||||||
And in the file `sql_app/main.py` file, uncomment the body of the `async` dependency `reset_db_state()`:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
async def reset_db_state():
|
|
||||||
database.db._state._state.set(db_state_default.copy())
|
|
||||||
database.db._state.reset()
|
|
||||||
```
|
|
||||||
|
|
||||||
Terminate your running app and start it again.
|
|
||||||
|
|
||||||
Repeat the same process with the 10 tabs. This time all of them will wait and you will get all the results without errors.
|
|
||||||
|
|
||||||
...You fixed it!
|
|
||||||
|
|
||||||
## Review all the files
|
|
||||||
|
|
||||||
Remember you should have a directory named `my_super_project` (or however you want) that contains a sub-directory called `sql_app`.
|
|
||||||
|
|
||||||
`sql_app` should have the following files:
|
|
||||||
|
|
||||||
* `sql_app/__init__.py`: is an empty file.
|
|
||||||
|
|
||||||
* `sql_app/database.py`:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/database.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
* `sql_app/models.py`:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/models.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
* `sql_app/schemas.py`:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
* `sql_app/crud.py`:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/crud.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
* `sql_app/main.py`:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
{!../../../docs_src/sql_databases_peewee/sql_app/main.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
These are very technical details that you probably don't need.
|
|
||||||
|
|
||||||
### The problem
|
|
||||||
|
|
||||||
Peewee uses <a href="https://docs.python.org/3/library/threading.html#thread-local-data" class="external-link" target="_blank">`threading.local`</a> by default to store it's database "state" data (connection, transactions, etc).
|
|
||||||
|
|
||||||
`threading.local` creates a value exclusive to the current thread, but an async framework would run all the code (e.g. for each request) in the same thread, and possibly not in order.
|
|
||||||
|
|
||||||
On top of that, an async framework could run some sync code in a threadpool (using `asyncio.run_in_executor`), but belonging to the same request.
|
|
||||||
|
|
||||||
This means that, with Peewee's current implementation, multiple tasks could be using the same `threading.local` variable and end up sharing the same connection and data (that they shouldn't), and at the same time, if they execute sync I/O-blocking code in a threadpool (as with normal `def` functions in FastAPI, in *path operations* and dependencies), that code won't have access to the database state variables, even while it's part of the same request and it should be able to get access to the same database state.
|
|
||||||
|
|
||||||
### Context variables
|
|
||||||
|
|
||||||
Python 3.7 has <a href="https://docs.python.org/3/library/contextvars.html" class="external-link" target="_blank">`contextvars`</a> that can create a local variable very similar to `threading.local`, but also supporting these async features.
|
|
||||||
|
|
||||||
There are several things to have in mind.
|
|
||||||
|
|
||||||
The `ContextVar` has to be created at the top of the module, like:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
some_var = ContextVar("some_var", default="default value")
|
|
||||||
```
|
|
||||||
|
|
||||||
To set a value used in the current "context" (e.g. for the current request) use:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
some_var.set("new value")
|
|
||||||
```
|
|
||||||
|
|
||||||
To get a value anywhere inside of the context (e.g. in any part handling the current request) use:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
some_var.get()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Set context variables in the `async` dependency `reset_db_state()`
|
|
||||||
|
|
||||||
If some part of the async code sets the value with `some_var.set("updated in function")` (e.g. like the `async` dependency), the rest of the code in it and the code that goes after (including code inside of `async` functions called with `await`) will see that new value.
|
|
||||||
|
|
||||||
So, in our case, if we set the Peewee state variable (with a default `dict`) in the `async` dependency, all the rest of the internal code in our app will see this value and will be able to reuse it for the whole request.
|
|
||||||
|
|
||||||
And the context variable would be set again for the next request, even if they are concurrent.
|
|
||||||
|
|
||||||
### Set database state in the dependency `get_db()`
|
|
||||||
|
|
||||||
As `get_db()` is a normal `def` function, **FastAPI** will make it run in a threadpool, with a *copy* of the "context", holding the same value for the context variable (the `dict` with the reset database state). Then it can add database state to that `dict`, like the connection, etc.
|
|
||||||
|
|
||||||
But if the value of the context variable (the default `dict`) was set in that normal `def` function, it would create a new value that would stay only in that thread of the threadpool, and the rest of the code (like the *path operation functions*) wouldn't have access to it. In `get_db()` we can only set values in the `dict`, but not the entire `dict` itself.
|
|
||||||
|
|
||||||
So, we need to have the `async` dependency `reset_db_state()` to set the `dict` in the context variable. That way, all the code has access to the same `dict` for the database state for a single request.
|
|
||||||
|
|
||||||
### Connect and disconnect in the dependency `get_db()`
|
|
||||||
|
|
||||||
Then the next question would be, why not just connect and disconnect the database in the `async` dependency itself, instead of in `get_db()`?
|
|
||||||
|
|
||||||
The `async` dependency has to be `async` for the context variable to be preserved for the rest of the request, but creating and closing the database connection is potentially blocking, so it could degrade performance if it was there.
|
|
||||||
|
|
||||||
So we also need the normal `def` dependency `get_db()`.
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
# Sub Applications - Mounts
|
|
||||||
|
|
||||||
If you need to have two independent FastAPI applications, with their own independent OpenAPI and their own docs UIs, you can have a main app and "mount" one (or more) sub-application(s).
|
|
||||||
|
|
||||||
## Mounting a **FastAPI** application
|
|
||||||
|
|
||||||
"Mounting" means adding a completely "independent" application in a specific path, that then takes care of handling everything under that path, with the _path operations_ declared in that sub-application.
|
|
||||||
|
|
||||||
### Top-level application
|
|
||||||
|
|
||||||
First, create the main, top-level, **FastAPI** application, and its *path operations*:
|
|
||||||
|
|
||||||
```Python hl_lines="3 6-8"
|
|
||||||
{!../../../docs_src/sub_applications/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sub-application
|
|
||||||
|
|
||||||
Then, create your sub-application, and its *path operations*.
|
|
||||||
|
|
||||||
This sub-application is just another standard FastAPI application, but this is the one that will be "mounted":
|
|
||||||
|
|
||||||
```Python hl_lines="11 14-16"
|
|
||||||
{!../../../docs_src/sub_applications/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mount the sub-application
|
|
||||||
|
|
||||||
In your top-level application, `app`, mount the sub-application, `subapi`.
|
|
||||||
|
|
||||||
In this case, it will be mounted at the path `/subapi`:
|
|
||||||
|
|
||||||
```Python hl_lines="11 19"
|
|
||||||
{!../../../docs_src/sub_applications/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check the automatic API docs
|
|
||||||
|
|
||||||
Now, run `uvicorn` with the main app, if your file is `main.py`, it would be:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ uvicorn main:app --reload
|
|
||||||
|
|
||||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
And open the docs at <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
|
|
||||||
|
|
||||||
You will see the automatic API docs for the main app, including only its own _path operations_:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/sub-applications/image01.png">
|
|
||||||
|
|
||||||
And then, open the docs for the sub-application, at <a href="http://127.0.0.1:8000/subapi/docs" class="external-link" target="_blank">http://127.0.0.1:8000/subapi/docs</a>.
|
|
||||||
|
|
||||||
You will see the automatic API docs for the sub-application, including only its own _path operations_, all under the correct sub-path prefix `/subapi`:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/sub-applications/image02.png">
|
|
||||||
|
|
||||||
If you try interacting with any of the two user interfaces, they will work correctly, because the browser will be able to talk to each specific app or sub-app.
|
|
||||||
|
|
||||||
### Technical Details: `root_path`
|
|
||||||
|
|
||||||
When you mount a sub-application as described above, FastAPI will take care of communicating the mount path for the sub-application using a mechanism from the ASGI specification called a `root_path`.
|
|
||||||
|
|
||||||
That way, the sub-application will know to use that path prefix for the docs UI.
|
|
||||||
|
|
||||||
And the sub-application could also have its own mounted sub-applications and everything would work correctly, because FastAPI handles all these `root_path`s automatically.
|
|
||||||
|
|
||||||
You will learn more about the `root_path` and how to use it explicitly in the section about [Behind a Proxy](./behind-a-proxy.md){.internal-link target=_blank}.
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
# Testing a Database
|
|
||||||
|
|
||||||
You can use the same dependency overrides from [Testing Dependencies with Overrides](testing-dependencies.md){.internal-link target=_blank} to alter a database for testing.
|
|
||||||
|
|
||||||
You could want to set up a different database for testing, rollback the data after the tests, pre-fill it with some testing data, etc.
|
|
||||||
|
|
||||||
The main idea is exactly the same you saw in that previous chapter.
|
|
||||||
|
|
||||||
## Add tests for the SQL app
|
|
||||||
|
|
||||||
Let's update the example from [SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank} to use a testing database.
|
|
||||||
|
|
||||||
All the app code is the same, you can go back to that chapter check how it was.
|
|
||||||
|
|
||||||
The only changes here are in the new testing file.
|
|
||||||
|
|
||||||
Your normal dependency `get_db()` would return a database session.
|
|
||||||
|
|
||||||
In the test, you could use a dependency override to return your *custom* database session instead of the one that would be used normally.
|
|
||||||
|
|
||||||
In this example we'll create a temporary database only for the tests.
|
|
||||||
|
|
||||||
## File structure
|
|
||||||
|
|
||||||
We create a new file at `sql_app/tests/test_sql_app.py`.
|
|
||||||
|
|
||||||
So the new file structure looks like:
|
|
||||||
|
|
||||||
``` hl_lines="9-11"
|
|
||||||
.
|
|
||||||
└── sql_app
|
|
||||||
├── __init__.py
|
|
||||||
├── crud.py
|
|
||||||
├── database.py
|
|
||||||
├── main.py
|
|
||||||
├── models.py
|
|
||||||
├── schemas.py
|
|
||||||
└── tests
|
|
||||||
├── __init__.py
|
|
||||||
└── test_sql_app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create the new database session
|
|
||||||
|
|
||||||
First, we create a new database session with the new database.
|
|
||||||
|
|
||||||
For the tests we'll use a file `test.db` instead of `sql_app.db`.
|
|
||||||
|
|
||||||
But the rest of the session code is more or less the same, we just copy it.
|
|
||||||
|
|
||||||
```Python hl_lines="8-13"
|
|
||||||
{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
You could reduce duplication in that code by putting it in a function and using it from both `database.py` and `tests/test_sql_app.py`.
|
|
||||||
|
|
||||||
For simplicity and to focus on the specific testing code, we are just copying it.
|
|
||||||
|
|
||||||
## Create the database
|
|
||||||
|
|
||||||
Because now we are going to use a new database in a new file, we need to make sure we create the database with:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
Base.metadata.create_all(bind=engine)
|
|
||||||
```
|
|
||||||
|
|
||||||
That is normally called in `main.py`, but the line in `main.py` uses the database file `sql_app.db`, and we need to make sure we create `test.db` for the tests.
|
|
||||||
|
|
||||||
So we add that line here, with the new file.
|
|
||||||
|
|
||||||
```Python hl_lines="16"
|
|
||||||
{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dependency override
|
|
||||||
|
|
||||||
Now we create the dependency override and add it to the overrides for our app.
|
|
||||||
|
|
||||||
```Python hl_lines="19-24 27"
|
|
||||||
{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The code for `override_get_db()` is almost exactly the same as for `get_db()`, but in `override_get_db()` we use the `TestingSessionLocal` for the testing database instead.
|
|
||||||
|
|
||||||
## Test the app
|
|
||||||
|
|
||||||
Then we can just test the app as normally.
|
|
||||||
|
|
||||||
```Python hl_lines="32-47"
|
|
||||||
{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
And all the modifications we made in the database during the tests will be in the `test.db` database instead of the main `sql_app.db`.
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# Testing Events: startup - shutdown
|
|
||||||
|
|
||||||
When you need your event handlers (`startup` and `shutdown`) to run in your tests, you can use the `TestClient` with a `with` statement:
|
|
||||||
|
|
||||||
```Python hl_lines="9-12 20-24"
|
|
||||||
{!../../../docs_src/app_testing/tutorial003.py!}
|
|
||||||
```
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
# Testing WebSockets
|
|
||||||
|
|
||||||
You can use the same `TestClient` to test WebSockets.
|
|
||||||
|
|
||||||
For this, you use the `TestClient` in a `with` statement, connecting to the WebSocket:
|
|
||||||
|
|
||||||
```Python hl_lines="27-31"
|
|
||||||
{!../../../docs_src/app_testing/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! note
|
|
||||||
For more details, check Starlette's documentation for <a href="https://www.starlette.io/testclient/#testing-websocket-sessions" class="external-link" target="_blank">testing WebSockets</a>.
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
# WebSockets
|
|
||||||
|
|
||||||
You can use <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API" class="external-link" target="_blank">WebSockets</a> with **FastAPI**.
|
|
||||||
|
|
||||||
## WebSockets client
|
|
||||||
|
|
||||||
### In production
|
|
||||||
|
|
||||||
In your production system, you probably have a frontend created with a modern framework like React, Vue.js or Angular.
|
|
||||||
|
|
||||||
And to communicate using WebSockets with your backend you would probably use your frontend's utilities.
|
|
||||||
|
|
||||||
Or you might have a native mobile application that communicates with your WebSocket backend directly, in native code.
|
|
||||||
|
|
||||||
Or you might have any other way to communicate with the WebSocket endpoint.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
But for this example, we'll use a very simple HTML document with some JavaScript, all inside a long string.
|
|
||||||
|
|
||||||
This, of course, is not optimal and you wouldn't use it for production.
|
|
||||||
|
|
||||||
In production you would have one of the options above.
|
|
||||||
|
|
||||||
But it's the simplest way to focus on the server-side of WebSockets and have a working example:
|
|
||||||
|
|
||||||
```Python hl_lines="2 6-38 41-43"
|
|
||||||
{!../../../docs_src/websockets/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create a `websocket`
|
|
||||||
|
|
||||||
In your **FastAPI** application, create a `websocket`:
|
|
||||||
|
|
||||||
```Python hl_lines="1 46-47"
|
|
||||||
{!../../../docs_src/websockets/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! note "Technical Details"
|
|
||||||
You could also use `from starlette.websockets import WebSocket`.
|
|
||||||
|
|
||||||
**FastAPI** provides the same `WebSocket` directly just as a convenience for you, the developer. But it comes directly from Starlette.
|
|
||||||
|
|
||||||
## Await for messages and send messages
|
|
||||||
|
|
||||||
In your WebSocket route you can `await` for messages and send messages.
|
|
||||||
|
|
||||||
```Python hl_lines="48-52"
|
|
||||||
{!../../../docs_src/websockets/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can receive and send binary, text, and JSON data.
|
|
||||||
|
|
||||||
## Try it
|
|
||||||
|
|
||||||
If your file is named `main.py`, run your application with:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ uvicorn main:app --reload
|
|
||||||
|
|
||||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Open your browser at <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>.
|
|
||||||
|
|
||||||
You will see a simple page like:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/websockets/image01.png">
|
|
||||||
|
|
||||||
You can type messages in the input box, and send them:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/websockets/image02.png">
|
|
||||||
|
|
||||||
And your **FastAPI** application with WebSockets will respond back:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/websockets/image03.png">
|
|
||||||
|
|
||||||
You can send (and receive) many messages:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/websockets/image04.png">
|
|
||||||
|
|
||||||
And all of them will use the same WebSocket connection.
|
|
||||||
|
|
||||||
## Using `Depends` and others
|
|
||||||
|
|
||||||
In WebSocket endpoints you can import from `fastapi` and use:
|
|
||||||
|
|
||||||
* `Depends`
|
|
||||||
* `Security`
|
|
||||||
* `Cookie`
|
|
||||||
* `Header`
|
|
||||||
* `Path`
|
|
||||||
* `Query`
|
|
||||||
|
|
||||||
They work the same way as for other FastAPI endpoints/*path operations*:
|
|
||||||
|
|
||||||
```Python hl_lines="58-65 68-83"
|
|
||||||
{!../../../docs_src/websockets/tutorial002.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! info
|
|
||||||
In a WebSocket it doesn't really make sense to raise an `HTTPException`. So it's better to close the WebSocket connection directly.
|
|
||||||
|
|
||||||
You can use a closing code from the <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1" class="external-link" target="_blank">valid codes defined in the specification</a>.
|
|
||||||
|
|
||||||
In the future, there will be a `WebSocketException` that you will be able to `raise` from anywhere, and add exception handlers for it. It depends on the <a href="https://github.com/encode/starlette/pull/527" class="external-link" target="_blank">PR #527</a> in Starlette.
|
|
||||||
|
|
||||||
### Try the WebSockets with dependencies
|
|
||||||
|
|
||||||
If your file is named `main.py`, run your application with:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ uvicorn main:app --reload
|
|
||||||
|
|
||||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Open your browser at <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>.
|
|
||||||
|
|
||||||
There you can set:
|
|
||||||
|
|
||||||
* The "Item ID", used in the path.
|
|
||||||
* The "Token" used as a query parameter.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Notice that the query `token` will be handled by a dependency.
|
|
||||||
|
|
||||||
With that you can connect the WebSocket and then send and receive messages:
|
|
||||||
|
|
||||||
<img src="/img/tutorial/websockets/image05.png">
|
|
||||||
|
|
||||||
## Handling disconnections and multiple clients
|
|
||||||
|
|
||||||
When a WebSocket connection is closed, the `await websocket.receive_text()` will raise a `WebSocketDisconnect` exception, which you can then catch and handle like in this example.
|
|
||||||
|
|
||||||
```Python hl_lines="81-83"
|
|
||||||
{!../../../docs_src/websockets/tutorial003.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
To try it out:
|
|
||||||
|
|
||||||
* Open the app with several browser tabs.
|
|
||||||
* Write messages from them.
|
|
||||||
* Then close one of the tabs.
|
|
||||||
|
|
||||||
That will raise the `WebSocketDisconnect` exception, and all the other clients will receive a message like:
|
|
||||||
|
|
||||||
```
|
|
||||||
Client #1596980209979 left the chat
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The app above is a minimal and simple example to demonstrate how to handle and broadcast messages to several WebSocket connections.
|
|
||||||
|
|
||||||
But have in mind that, as everything is handled in memory, in a single list, it will only work while the process is running, and will only work with a single process.
|
|
||||||
|
|
||||||
If you need something easy to integrate with FastAPI but that is more robust, supported by Redis, PostgreSQL or others, check <a href="https://github.com/encode/broadcaster" class="external-link" target="_blank">encode/broadcaster</a>.
|
|
||||||
|
|
||||||
## More info
|
|
||||||
|
|
||||||
To learn more about the options, check Starlette's documentation for:
|
|
||||||
|
|
||||||
* <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>.
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
# Including WSGI - Flask, Django, others
|
|
||||||
|
|
||||||
You can mount WSGI applications as you saw with [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}, [Behind a Proxy](./behind-a-proxy.md){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
For that, you can use the `WSGIMiddleware` and use it to wrap your WSGI application, for example, Flask, Django, etc.
|
|
||||||
|
|
||||||
## Using `WSGIMiddleware`
|
|
||||||
|
|
||||||
You need to import `WSGIMiddleware`.
|
|
||||||
|
|
||||||
Then wrap the WSGI (e.g. Flask) app with the middleware.
|
|
||||||
|
|
||||||
And then mount that under a path.
|
|
||||||
|
|
||||||
```Python hl_lines="2-3 22"
|
|
||||||
{!../../../docs_src/wsgi/tutorial001.py!}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Check it
|
|
||||||
|
|
||||||
Now, every request under the path `/v1/` will be handled by the Flask application.
|
|
||||||
|
|
||||||
And the rest will be handled by **FastAPI**.
|
|
||||||
|
|
||||||
If you run it with Uvicorn and go to <a href="http://localhost:8000/v1/" class="external-link" target="_blank">http://localhost:8000/v1/</a> you will see the response from Flask:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
Hello, World from Flask!
|
|
||||||
```
|
|
||||||
|
|
||||||
And if you go to <a href="http://localhost:8000/v2" class="external-link" target="_blank">http://localhost:8000/v2</a> you will see the response from FastAPI:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"message": "Hello World"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
@ -1,501 +0,0 @@
|
||||||
# Development - Contributing
|
|
||||||
|
|
||||||
First, you might want to see the basic ways to [help FastAPI and get help](help-fastapi.md){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
## Developing
|
|
||||||
|
|
||||||
If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment.
|
|
||||||
|
|
||||||
### Virtual environment with `venv`
|
|
||||||
|
|
||||||
You can create a virtual environment in a directory using Python's `venv` module:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ python -m venv env
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
That will create a directory `./env/` with the Python binaries and then you will be able to install packages for that isolated environment.
|
|
||||||
|
|
||||||
### Activate the environment
|
|
||||||
|
|
||||||
Activate the new environment with:
|
|
||||||
|
|
||||||
=== "Linux, macOS"
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ source ./env/bin/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
=== "Windows PowerShell"
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ .\env\Scripts\Activate.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
=== "Windows Bash"
|
|
||||||
|
|
||||||
Or if you use Bash for Windows (e.g. <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>):
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ source ./env/Scripts/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
To check it worked, use:
|
|
||||||
|
|
||||||
=== "Linux, macOS, Windows Bash"
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ which pip
|
|
||||||
|
|
||||||
some/directory/fastapi/env/bin/pip
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
=== "Windows PowerShell"
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ Get-Command pip
|
|
||||||
|
|
||||||
some/directory/fastapi/env/bin/pip
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
If it shows the `pip` binary at `env/bin/pip` then it worked. 🎉
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Every time you install a new package with `pip` under that environment, activate the environment again.
|
|
||||||
|
|
||||||
This makes sure that if you use a terminal program installed by that package (like `flit`), you use the one from your local environment and not any other that could be installed globally.
|
|
||||||
|
|
||||||
### Flit
|
|
||||||
|
|
||||||
**FastAPI** uses <a href="https://flit.readthedocs.io/en/latest/index.html" class="external-link" target="_blank">Flit</a> to build, package and publish the project.
|
|
||||||
|
|
||||||
After activating the environment as described above, install `flit`:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ pip install flit
|
|
||||||
|
|
||||||
---> 100%
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Now re-activate the environment to make sure you are using the `flit` you just installed (and not a global one).
|
|
||||||
|
|
||||||
And now use `flit` to install the development dependencies:
|
|
||||||
|
|
||||||
=== "Linux, macOS"
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ flit install --deps develop --symlink
|
|
||||||
|
|
||||||
---> 100%
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
=== "Windows"
|
|
||||||
|
|
||||||
If you are on Windows, use `--pth-file` instead of `--symlink`:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ flit install --deps develop --pth-file
|
|
||||||
|
|
||||||
---> 100%
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
It will install all the dependencies and your local FastAPI in your local environment.
|
|
||||||
|
|
||||||
#### Using your local FastAPI
|
|
||||||
|
|
||||||
If you create a Python file that imports and uses FastAPI, and run it with the Python from your local environment, it will use your local FastAPI source code.
|
|
||||||
|
|
||||||
And if you update that local FastAPI source code, as it is installed with `--symlink` (or `--pth-file` on Windows), when you run that Python file again, it will use the fresh version of FastAPI you just edited.
|
|
||||||
|
|
||||||
That way, you don't have to "install" your local version to be able to test every change.
|
|
||||||
|
|
||||||
### Format
|
|
||||||
|
|
||||||
There is a script that you can run that will format and clean all your code:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ bash scripts/format.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
It will also auto-sort all your imports.
|
|
||||||
|
|
||||||
For it to sort them correctly, you need to have FastAPI installed locally in your environment, with the command in the section above using `--symlink` (or `--pth-file` on Windows).
|
|
||||||
|
|
||||||
### Format imports
|
|
||||||
|
|
||||||
There is another script that formats all the imports and makes sure you don't have unused imports:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ bash scripts/format-imports.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
As it runs one command after the other and modifies and reverts many files, it takes a bit longer to run, so it might be easier to use `scripts/format.sh` frequently and `scripts/format-imports.sh` only before committing.
|
|
||||||
|
|
||||||
## Docs
|
|
||||||
|
|
||||||
First, make sure you set up your environment as described above, that will install all the requirements.
|
|
||||||
|
|
||||||
The documentation uses <a href="https://www.mkdocs.org/" class="external-link" target="_blank">MkDocs</a>.
|
|
||||||
|
|
||||||
And there are extra tools/scripts in place to handle translations in `./scripts/docs.py`.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
You don't need to see the code in `./scripts/docs.py`, you just use it in the command line.
|
|
||||||
|
|
||||||
All the documentation is in Markdown format in the directory `./docs/en/`.
|
|
||||||
|
|
||||||
Many of the tutorials have blocks of code.
|
|
||||||
|
|
||||||
In most of the cases, these blocks of code are actual complete applications that can be run as is.
|
|
||||||
|
|
||||||
In fact, those blocks of code are not written inside the Markdown, they are Python files in the `./docs_src/` directory.
|
|
||||||
|
|
||||||
And those Python files are included/injected in the documentation when generating the site.
|
|
||||||
|
|
||||||
### Docs for tests
|
|
||||||
|
|
||||||
Most of the tests actually run against the example source files in the documentation.
|
|
||||||
|
|
||||||
This helps making sure that:
|
|
||||||
|
|
||||||
* The documentation is up to date.
|
|
||||||
* The documentation examples can be run as is.
|
|
||||||
* Most of the features are covered by the documentation, ensured by test coverage.
|
|
||||||
|
|
||||||
During local development, there is a script that builds the site and checks for any changes, live-reloading:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ python ./scripts/docs.py live
|
|
||||||
|
|
||||||
<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008
|
|
||||||
<span style="color: green;">[INFO]</span> Start watching changes
|
|
||||||
<span style="color: green;">[INFO]</span> Start detecting changes
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
It will serve the documentation on `http://127.0.0.1:8008`.
|
|
||||||
|
|
||||||
That way, you can edit the documentation/source files and see the changes live.
|
|
||||||
|
|
||||||
#### Typer CLI (optional)
|
|
||||||
|
|
||||||
The instructions here show you how to use the script at `./scripts/docs.py` with the `python` program directly.
|
|
||||||
|
|
||||||
But you can also use <a href="https://typer.tiangolo.com/typer-cli/" class="external-link" target="_blank">Typer CLI</a>, and you will get autocompletion in your terminal for the commands after installing completion.
|
|
||||||
|
|
||||||
If you install Typer CLI, you can install completion with:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ typer --install-completion
|
|
||||||
|
|
||||||
zsh completion installed in /home/user/.bashrc.
|
|
||||||
Completion will take effect once you restart the terminal.
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Apps and docs at the same time
|
|
||||||
|
|
||||||
If you run the examples with, e.g.:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ uvicorn tutorial001:app --reload
|
|
||||||
|
|
||||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
as Uvicorn by default will use the port `8000`, the documentation on port `8008` won't clash.
|
|
||||||
|
|
||||||
### Translations
|
|
||||||
|
|
||||||
Help with translations is VERY MUCH appreciated! And it can't be done without the help from the community. 🌎 🚀
|
|
||||||
|
|
||||||
Here are the steps to help with translations.
|
|
||||||
|
|
||||||
#### Tips and guidelines
|
|
||||||
|
|
||||||
* Check the currently <a href="https://github.com/tiangolo/fastapi/pulls" class="external-link" target="_blank">existing pull requests</a> for your language and add reviews requesting changes or approving them.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
You can <a href="https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request" class="external-link" target="_blank">add comments with change suggestions</a> to existing pull requests.
|
|
||||||
|
|
||||||
Check the docs about <a href="https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-reviews" class="external-link" target="_blank">adding a pull request review</a> to approve it or request changes.
|
|
||||||
|
|
||||||
* Check in the <a href="https://github.com/tiangolo/fastapi/issues" class="external-link" target="_blank">issues</a> to see if there's one coordinating translations for your language.
|
|
||||||
|
|
||||||
* Add a single pull request per page translated. That will make it much easier for others to review it.
|
|
||||||
|
|
||||||
For the languages I don't speak, I'll wait for several others to review the translation before merging.
|
|
||||||
|
|
||||||
* You can also check if there are translations for your language and add a review to them, that will help me know that the translation is correct and I can merge it.
|
|
||||||
|
|
||||||
* Use the same Python examples and only translate the text in the docs. You don't have to change anything for this to work.
|
|
||||||
|
|
||||||
* Use the same images, file names, and links. You don't have to change anything for it to work.
|
|
||||||
|
|
||||||
* To check the 2-letter code for the language you want to translate you can use the table <a href="https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes" class="external-link" target="_blank">List of ISO 639-1 codes</a>.
|
|
||||||
|
|
||||||
#### Existing language
|
|
||||||
|
|
||||||
Let's say you want to translate a page for a language that already has translations for some pages, like Spanish.
|
|
||||||
|
|
||||||
In the case of Spanish, the 2-letter code is `es`. So, the directory for Spanish translations is located at `docs/es/`.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The main ("official") language is English, located at `docs/en/`.
|
|
||||||
|
|
||||||
Now run the live server for the docs in Spanish:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
// Use the command "live" and pass the language code as a CLI argument
|
|
||||||
$ python ./scripts/docs.py live es
|
|
||||||
|
|
||||||
<span style="color: green;">[INFO]</span> Serving on http://127.0.0.1:8008
|
|
||||||
<span style="color: green;">[INFO]</span> Start watching changes
|
|
||||||
<span style="color: green;">[INFO]</span> Start detecting changes
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Now you can go to <a href="http://127.0.0.1:8008" class="external-link" target="_blank">http://127.0.0.1:8008</a> and see your changes live.
|
|
||||||
|
|
||||||
If you look at the FastAPI docs website, you will see that every language has all the pages. But some pages are not translated and have a notification about the missing translation.
|
|
||||||
|
|
||||||
But when you run it locally like this, you will only see the pages that are already translated.
|
|
||||||
|
|
||||||
Now let's say that you want to add a translation for the section [Features](features.md){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
* Copy the file at:
|
|
||||||
|
|
||||||
```
|
|
||||||
docs/en/docs/features.md
|
|
||||||
```
|
|
||||||
|
|
||||||
* Paste it in exactly the same location but for the language you want to translate, e.g.:
|
|
||||||
|
|
||||||
```
|
|
||||||
docs/es/docs/features.md
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Notice that the only change in the path and file name is the language code, from `en` to `es`.
|
|
||||||
|
|
||||||
* Now open the MkDocs config file for English at:
|
|
||||||
|
|
||||||
```
|
|
||||||
docs/en/docs/mkdocs.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
* Find the place where that `docs/features.md` is located in the config file. Somewhere like:
|
|
||||||
|
|
||||||
```YAML hl_lines="8"
|
|
||||||
site_name: FastAPI
|
|
||||||
# More stuff
|
|
||||||
nav:
|
|
||||||
- FastAPI: index.md
|
|
||||||
- Languages:
|
|
||||||
- en: /
|
|
||||||
- es: /es/
|
|
||||||
- features.md
|
|
||||||
```
|
|
||||||
|
|
||||||
* Open the MkDocs config file for the language you are editing, e.g.:
|
|
||||||
|
|
||||||
```
|
|
||||||
docs/es/docs/mkdocs.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
* Add it there at the exact same location it was for English, e.g.:
|
|
||||||
|
|
||||||
```YAML hl_lines="8"
|
|
||||||
site_name: FastAPI
|
|
||||||
# More stuff
|
|
||||||
nav:
|
|
||||||
- FastAPI: index.md
|
|
||||||
- Languages:
|
|
||||||
- en: /
|
|
||||||
- es: /es/
|
|
||||||
- features.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure that if there are other entries, the new entry with your translation is exactly in the same order as in the English version.
|
|
||||||
|
|
||||||
If you go to your browser you will see that now the docs show your new section. 🎉
|
|
||||||
|
|
||||||
Now you can translate it all and see how it looks as you save the file.
|
|
||||||
|
|
||||||
#### New Language
|
|
||||||
|
|
||||||
Let's say that you want to add translations for a language that is not yet translated, not even some pages.
|
|
||||||
|
|
||||||
Let's say you want to add translations for Creole, and it's not yet there in the docs.
|
|
||||||
|
|
||||||
Checking the link from above, the code for "Creole" is `ht`.
|
|
||||||
|
|
||||||
The next step is to run the script to generate a new translation directory:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
// Use the command new-lang, pass the language code as a CLI argument
|
|
||||||
$ python ./scripts/docs.py new-lang ht
|
|
||||||
|
|
||||||
Successfully initialized: docs/ht
|
|
||||||
Updating ht
|
|
||||||
Updating en
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Now you can check in your code editor the newly created directory `docs/ht/`.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Create a first pull request with just this, to set up the configuration for the new language, before adding translations.
|
|
||||||
|
|
||||||
That way others can help with other pages while you work on the first one. 🚀
|
|
||||||
|
|
||||||
Start by translating the main page, `docs/ht/index.md`.
|
|
||||||
|
|
||||||
Then you can continue with the previous instructions, for an "Existing Language".
|
|
||||||
|
|
||||||
##### New Language not supported
|
|
||||||
|
|
||||||
If when running the live server script you get an error about the language not being supported, something like:
|
|
||||||
|
|
||||||
```
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
jinja2.exceptions.TemplateNotFound: partials/language/xx.html
|
|
||||||
```
|
|
||||||
|
|
||||||
That means that the theme doesn't support that language (in this case, with a fake 2-letter code of `xx`).
|
|
||||||
|
|
||||||
But don't worry, you can set the theme language to English and then translate the content of the docs.
|
|
||||||
|
|
||||||
If you need to do that, edit the `mkdocs.yml` for your new language, it will have something like:
|
|
||||||
|
|
||||||
```YAML hl_lines="5"
|
|
||||||
site_name: FastAPI
|
|
||||||
# More stuff
|
|
||||||
theme:
|
|
||||||
# More stuff
|
|
||||||
language: xx
|
|
||||||
```
|
|
||||||
|
|
||||||
Change that language from `xx` (from your language code) to `en`.
|
|
||||||
|
|
||||||
Then you can start the live server again.
|
|
||||||
|
|
||||||
#### Preview the result
|
|
||||||
|
|
||||||
When you use the script at `./scripts/docs.py` with the `live` command it only shows the files and translations available for the current language.
|
|
||||||
|
|
||||||
But once you are done, you can test it all as it would look online.
|
|
||||||
|
|
||||||
To do that, first build all the docs:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
// Use the command "build-all", this will take a bit
|
|
||||||
$ python ./scripts/docs.py build-all
|
|
||||||
|
|
||||||
Updating es
|
|
||||||
Updating en
|
|
||||||
Building docs for: en
|
|
||||||
Building docs for: es
|
|
||||||
Successfully built docs for: es
|
|
||||||
Copying en index.md to README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
That generates all the docs at `./docs_build/` for each language. This includes adding any files with missing translations, with a note saying that "this file doesn't have a translation yet". But you don't have to do anything with that directory.
|
|
||||||
|
|
||||||
Then it builds all those independent MkDocs sites for each language, combines them, and generates the final output at `./site/`.
|
|
||||||
|
|
||||||
Then you can serve that with the command `serve`:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
// Use the command "serve" after running "build-all"
|
|
||||||
$ python ./scripts/docs.py serve
|
|
||||||
|
|
||||||
Warning: this is a very simple server. For development, use mkdocs serve instead.
|
|
||||||
This is here only to preview a site with translations already built.
|
|
||||||
Make sure you run the build-all command first.
|
|
||||||
Serving at: http://127.0.0.1:8008
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
There is a script that you can run locally to test all the code and generate coverage reports in HTML:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ bash scripts/test-cov-html.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
This command generates a directory `./htmlcov/`, if you open the file `./htmlcov/index.html` in your browser, you can explore interactively the regions of code that are covered by the tests, and notice if there is any region missing.
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
a.external-link::after {
|
|
||||||
/* \00A0 is a non-breaking space
|
|
||||||
to make the mark be on the same line as the link
|
|
||||||
*/
|
|
||||||
content: "\00A0[↪]";
|
|
||||||
}
|
|
||||||
|
|
||||||
a.internal-link::after {
|
|
||||||
/* \00A0 is a non-breaking space
|
|
||||||
to make the mark be on the same line as the link
|
|
||||||
*/
|
|
||||||
content: "\00A0↪";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Give space to lower icons so Gitter chat doesn't get on top of them */
|
|
||||||
.md-footer-meta {
|
|
||||||
padding-bottom: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-list-center {
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user {
|
|
||||||
margin: 1em;
|
|
||||||
min-width: 7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user .avatar-wrapper {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
margin: 10px auto;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user .avatar-wrapper img {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user .title {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user .count {
|
|
||||||
font-size: 80%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.announce-link:link,
|
|
||||||
a.announce-link:visited {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.announce-link:hover {
|
|
||||||
color: var(--md-accent-fg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.announce-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.announce-wrapper div.item {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.announce-wrapper .sponsor-badge {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
right: 0;
|
|
||||||
font-size: 0.5rem;
|
|
||||||
color: #999;
|
|
||||||
background-color: #666;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 0 10px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.announce-wrapper .sponsor-image {
|
|
||||||
display: block;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.announce-wrapper>div {
|
|
||||||
min-height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
/**
|
|
||||||
* termynal.js
|
|
||||||
*
|
|
||||||
* @author Ines Montani <ines@ines.io>
|
|
||||||
* @version 0.0.1
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--color-bg: #252a33;
|
|
||||||
--color-text: #eee;
|
|
||||||
--color-text-subtle: #a2a2a2;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-termynal] {
|
|
||||||
width: 750px;
|
|
||||||
max-width: 100%;
|
|
||||||
background: var(--color-bg);
|
|
||||||
color: var(--color-text);
|
|
||||||
font-size: 18px;
|
|
||||||
/* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */
|
|
||||||
font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 75px 45px 35px;
|
|
||||||
position: relative;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-termynal]:before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 15px;
|
|
||||||
left: 15px;
|
|
||||||
display: inline-block;
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
border-radius: 50%;
|
|
||||||
/* A little hack to display the window buttons in one pseudo element. */
|
|
||||||
background: #d9515d;
|
|
||||||
-webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
|
|
||||||
box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-termynal]:after {
|
|
||||||
content: 'bash';
|
|
||||||
position: absolute;
|
|
||||||
color: var(--color-text-subtle);
|
|
||||||
top: 5px;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
a[data-terminal-control] {
|
|
||||||
text-align: right;
|
|
||||||
display: block;
|
|
||||||
color: #aebbff;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-ty] {
|
|
||||||
display: block;
|
|
||||||
line-height: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-ty]:before {
|
|
||||||
/* Set up defaults and ensure empty lines are displayed. */
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-ty="input"]:before,
|
|
||||||
[data-ty-prompt]:before {
|
|
||||||
margin-right: 0.75em;
|
|
||||||
color: var(--color-text-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-ty="input"]:before {
|
|
||||||
content: '$';
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-ty][data-ty-prompt]:before {
|
|
||||||
content: attr(data-ty-prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-ty-cursor]:after {
|
|
||||||
content: attr(data-ty-cursor);
|
|
||||||
font-family: monospace;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
-webkit-animation: blink 1s infinite;
|
|
||||||
animation: blink 1s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Cursor animation */
|
|
||||||
|
|
||||||
@-webkit-keyframes blink {
|
|
||||||
50% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink {
|
|
||||||
50% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,240 +0,0 @@
|
||||||
# Deploy FastAPI on Deta
|
|
||||||
|
|
||||||
In this section you will learn how to easily deploy a **FastAPI** application on <a href="https://www.deta.sh/?ref=fastapi" class="external-link" target="_blank">Deta</a> using the free plan. 🎁
|
|
||||||
|
|
||||||
It will take you about **10 minutes**.
|
|
||||||
|
|
||||||
!!! info
|
|
||||||
<a href="https://www.deta.sh/?ref=fastapi" class="external-link" target="_blank">Deta</a> is a **FastAPI** sponsor. 🎉
|
|
||||||
|
|
||||||
## A basic **FastAPI** app
|
|
||||||
|
|
||||||
* Create a directory for your app, for example `./fastapideta/` and enter in it.
|
|
||||||
|
|
||||||
### FastAPI code
|
|
||||||
|
|
||||||
* Create a `main.py` file with:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
def read_root():
|
|
||||||
return {"Hello": "World"}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/items/{item_id}")
|
|
||||||
def read_item(item_id: int):
|
|
||||||
return {"item_id": item_id}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
Now, in the same directory create a file `requirements.txt` with:
|
|
||||||
|
|
||||||
```text
|
|
||||||
fastapi
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
You don't need to install Uvicorn to deploy on Deta, although you would probably want to install it locally to test your app.
|
|
||||||
|
|
||||||
### Directory structure
|
|
||||||
|
|
||||||
You will now have one directory `./fastapideta/` with two files:
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
└── main.py
|
|
||||||
└── requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create a free Deta account
|
|
||||||
|
|
||||||
Now create a <a href="https://www.deta.sh/?ref=fastapi" class="external-link" target="_blank">free account on Deta</a>, you just need an email and password.
|
|
||||||
|
|
||||||
You don't even need a credit card.
|
|
||||||
|
|
||||||
## Install the CLI
|
|
||||||
|
|
||||||
Once you have your account, install the Deta <abbr title="Command Line Interface application">CLI</abbr>:
|
|
||||||
|
|
||||||
=== "Linux, macOS"
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ curl -fsSL https://get.deta.dev/cli.sh | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
=== "Windows PowerShell"
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ iwr https://get.deta.dev/cli.ps1 -useb | iex
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
After installing it, open a new terminal so that the installed CLI is detected.
|
|
||||||
|
|
||||||
In a new terminal, confirm that it was correctly installed with:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ deta --help
|
|
||||||
|
|
||||||
Deta command line interface for managing deta micros.
|
|
||||||
Complete documentation available at https://docs.deta.sh
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
deta [flags]
|
|
||||||
deta [command]
|
|
||||||
|
|
||||||
Available Commands:
|
|
||||||
auth Change auth settings for a deta micro
|
|
||||||
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
If you have problems installing the CLI, check the <a href="https://docs.deta.sh/docs/micros/getting_started?ref=fastapi" class="external-link" target="_blank">official Deta docs</a>.
|
|
||||||
|
|
||||||
## Login with the CLI
|
|
||||||
|
|
||||||
Now login to Deta from the CLI with:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ deta login
|
|
||||||
|
|
||||||
Please, log in from the web page. Waiting..
|
|
||||||
Logged in successfully.
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
This will open a web browser and authenticate automatically.
|
|
||||||
|
|
||||||
## Deploy with Deta
|
|
||||||
|
|
||||||
Next, deploy your application with the Deta CLI:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ deta new
|
|
||||||
|
|
||||||
Successfully created a new micro
|
|
||||||
|
|
||||||
// Notice the "endpoint" 🔍
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "fastapideta",
|
|
||||||
"runtime": "python3.7",
|
|
||||||
"endpoint": "https://qltnci.deta.dev",
|
|
||||||
"visor": "enabled",
|
|
||||||
"http_auth": "enabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
Adding dependencies...
|
|
||||||
|
|
||||||
|
|
||||||
---> 100%
|
|
||||||
|
|
||||||
|
|
||||||
Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
You will see a JSON message similar to:
|
|
||||||
|
|
||||||
```JSON hl_lines="4"
|
|
||||||
{
|
|
||||||
"name": "fastapideta",
|
|
||||||
"runtime": "python3.7",
|
|
||||||
"endpoint": "https://qltnci.deta.dev",
|
|
||||||
"visor": "enabled",
|
|
||||||
"http_auth": "enabled"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
Your deployment will have a different `"endpoint"` URL.
|
|
||||||
|
|
||||||
## Check it
|
|
||||||
|
|
||||||
Now open your browser in your `endpoint` URL. In the example above it was `https://qltnci.deta.dev`, but yours will be different.
|
|
||||||
|
|
||||||
You will see the JSON response from your FastAPI app:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"Hello": "World"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And now go to the `/docs` for your API, in the example above it would be `https://qltnci.deta.dev/docs`.
|
|
||||||
|
|
||||||
It will show your docs like:
|
|
||||||
|
|
||||||
<img src="/img/deployment/deta/image01.png">
|
|
||||||
|
|
||||||
## Enable public access
|
|
||||||
|
|
||||||
By default, Deta will handle authentication using cookies for your account.
|
|
||||||
|
|
||||||
But once you are ready, you can make it public with:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ deta auth disable
|
|
||||||
|
|
||||||
Successfully disabled http auth
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Now you can share that URL with anyone and they will be able to access your API. 🚀
|
|
||||||
|
|
||||||
## HTTPS
|
|
||||||
|
|
||||||
Congrats! You deployed your FastAPI app to Deta! 🎉 🍰
|
|
||||||
|
|
||||||
Also notice that Deta correctly handles HTTPS for you, so you don't have to take care of that and can be sure that your clients will have a secure encrypted connection. ✅ 🔒
|
|
||||||
|
|
||||||
## Check the Visor
|
|
||||||
|
|
||||||
From your docs UI (they will be in a URL like `https://qltnci.deta.dev/docs`) send a request to your *path operation* `/items/{item_id}`.
|
|
||||||
|
|
||||||
For example with ID `5`.
|
|
||||||
|
|
||||||
Now go to <a href="https://web.deta.sh/" class="external-link" target="_blank">https://web.deta.sh</a>.
|
|
||||||
|
|
||||||
You will see there's a section to the left called <abbr title="it comes from Micro(server)">"Micros"</abbr> with each of your apps.
|
|
||||||
|
|
||||||
You will see a tab with "Details", and also a tab "Visor", go to the tab "Visor".
|
|
||||||
|
|
||||||
In there you can inspect the recent requests sent to your app.
|
|
||||||
|
|
||||||
You can also edit them and re-play them.
|
|
||||||
|
|
||||||
<img src="/img/deployment/deta/image02.png">
|
|
||||||
|
|
||||||
## Learn more
|
|
||||||
|
|
||||||
At some point you will probably want to store some data for your app in a way that persists through time. For that you can use <a href="https://docs.deta.sh/docs/base/py_tutorial?ref=fastapi" class="external-link" target="_blank">Deta Base</a>, it also has a generous **free tier**.
|
|
||||||
|
|
||||||
You can also read more in the <a href="https://docs.deta.sh?ref=fastapi" class="external-link" target="_blank">Deta Docs</a>.
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
# Deploy with Docker
|
|
||||||
|
|
||||||
In this section you'll see instructions and links to guides to know how to:
|
|
||||||
|
|
||||||
* Make your **FastAPI** application a Docker image/container with maximum performance. In about **5 min**.
|
|
||||||
* (Optionally) understand what you, as a developer, need to know about HTTPS.
|
|
||||||
* Set up a Docker Swarm mode cluster with automatic HTTPS, even on a simple $5 USD/month server. In about **20 min**.
|
|
||||||
* Generate and deploy a full **FastAPI** application, using your Docker Swarm cluster, with HTTPS, etc. In about **10 min**.
|
|
||||||
|
|
||||||
You can use <a href="https://www.docker.com/" class="external-link" target="_blank">**Docker**</a> for deployment. It has several advantages like security, replicability, development simplicity, etc.
|
|
||||||
|
|
||||||
If you are using Docker, you can use the official Docker image:
|
|
||||||
|
|
||||||
## <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>
|
|
||||||
|
|
||||||
This image has an "auto-tuning" mechanism included, so that you can just add your code and get very high performance automatically. And without making sacrifices.
|
|
||||||
|
|
||||||
But you can still change and update all the configurations with environment variables or configuration files.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
To see all the configurations and options, go to the Docker image page: <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>.
|
|
||||||
|
|
||||||
## Create a `Dockerfile`
|
|
||||||
|
|
||||||
* Go to your project directory.
|
|
||||||
* Create a `Dockerfile` with:
|
|
||||||
|
|
||||||
```Dockerfile
|
|
||||||
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
|
|
||||||
|
|
||||||
COPY ./app /app
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bigger Applications
|
|
||||||
|
|
||||||
If you followed the section about creating [Bigger Applications with Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}, your `Dockerfile` might instead look like:
|
|
||||||
|
|
||||||
```Dockerfile
|
|
||||||
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
|
|
||||||
|
|
||||||
COPY ./app /app/app
|
|
||||||
```
|
|
||||||
|
|
||||||
### Raspberry Pi and other architectures
|
|
||||||
|
|
||||||
If you are running Docker in a Raspberry Pi (that has an ARM processor) or any other architecture, you can create a `Dockerfile` from scratch, based on a Python base image (that is multi-architecture) and use Uvicorn alone.
|
|
||||||
|
|
||||||
In this case, your `Dockerfile` could look like:
|
|
||||||
|
|
||||||
```Dockerfile
|
|
||||||
FROM python:3.7
|
|
||||||
|
|
||||||
RUN pip install fastapi uvicorn
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
COPY ./app /app
|
|
||||||
|
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create the **FastAPI** Code
|
|
||||||
|
|
||||||
* Create an `app` directory and enter in it.
|
|
||||||
* Create a `main.py` file with:
|
|
||||||
|
|
||||||
```Python
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
def read_root():
|
|
||||||
return {"Hello": "World"}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/items/{item_id}")
|
|
||||||
def read_item(item_id: int, q: Optional[str] = None):
|
|
||||||
return {"item_id": item_id, "q": q}
|
|
||||||
```
|
|
||||||
|
|
||||||
* You should now have a directory structure like:
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── app
|
|
||||||
│ └── main.py
|
|
||||||
└── Dockerfile
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build the Docker image
|
|
||||||
|
|
||||||
* Go to the project directory (in where your `Dockerfile` is, containing your `app` directory).
|
|
||||||
* Build your FastAPI image:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ docker build -t myimage .
|
|
||||||
|
|
||||||
---> 100%
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Start the Docker container
|
|
||||||
|
|
||||||
* Run a container based on your image:
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ docker run -d --name mycontainer -p 80:80 myimage
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Now you have an optimized FastAPI server in a Docker container. Auto-tuned for your current server (and number of CPU cores).
|
|
||||||
|
|
||||||
## Check it
|
|
||||||
|
|
||||||
You should be able to check it in your Docker container's URL, for example: <a href="http://192.168.99.100/items/5?q=somequery" class="external-link" target="_blank">http://192.168.99.100/items/5?q=somequery</a> or <a href="http://127.0.0.1/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1/items/5?q=somequery</a> (or equivalent, using your Docker host).
|
|
||||||
|
|
||||||
You will see something like:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{"item_id": 5, "q": "somequery"}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Interactive API docs
|
|
||||||
|
|
||||||
Now you can go to <a href="http://192.168.99.100/docs" class="external-link" target="_blank">http://192.168.99.100/docs</a> or <a href="http://127.0.0.1/docs" class="external-link" target="_blank">http://127.0.0.1/docs</a> (or equivalent, using your Docker host).
|
|
||||||
|
|
||||||
You will see the automatic interactive API documentation (provided by <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>):
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Alternative API docs
|
|
||||||
|
|
||||||
And you can also go to <a href="http://192.168.99.100/redoc" class="external-link" target="_blank">http://192.168.99.100/redoc</a> or <a href="http://127.0.0.1/redoc" class="external-link" target="_blank">http://127.0.0.1/redoc</a> (or equivalent, using your Docker host).
|
|
||||||
|
|
||||||
You will see the alternative automatic documentation (provided by <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>):
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Traefik
|
|
||||||
|
|
||||||
<a href="https://traefik.io/" class="external-link" target="_blank">Traefik</a> is a high performance reverse proxy / load balancer. It can do the "TLS Termination Proxy" job (apart from other features).
|
|
||||||
|
|
||||||
It has integration with Let's Encrypt. So, it can handle all the HTTPS parts, including certificate acquisition and renewal.
|
|
||||||
|
|
||||||
It also has integrations with Docker. So, you can declare your domains in each application configurations and have it read those configurations, generate the HTTPS certificates and serve HTTPS to your application automatically, without requiring any change in its configuration.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
With this information and tools, continue with the next section to combine everything.
|
|
||||||
|
|
||||||
## Docker Swarm mode cluster with Traefik and HTTPS
|
|
||||||
|
|
||||||
You can have a Docker Swarm mode cluster set up in minutes (about 20 min) with a main Traefik handling HTTPS (including certificate acquisition and renewal).
|
|
||||||
|
|
||||||
By using Docker Swarm mode, you can start with a "cluster" of a single machine (it can even be a $5 USD / month server) and then you can grow as much as you need adding more servers.
|
|
||||||
|
|
||||||
To set up a Docker Swarm Mode cluster with Traefik and HTTPS handling, follow this guide:
|
|
||||||
|
|
||||||
### <a href="https://medium.com/@tiangolo/docker-swarm-mode-and-traefik-for-a-https-cluster-20328dba6232" class="external-link" target="_blank">Docker Swarm Mode and Traefik for an HTTPS cluster</a>
|
|
||||||
|
|
||||||
### Deploy a FastAPI application
|
|
||||||
|
|
||||||
The easiest way to set everything up, would be using the [**FastAPI** Project Generators](../project-generation.md){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
It is designed to be integrated with this Docker Swarm cluster with Traefik and HTTPS described above.
|
|
||||||
|
|
||||||
You can generate a project in about 2 min.
|
|
||||||
|
|
||||||
The generated project has instructions to deploy it, doing it takes another 2 min.
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
# About HTTPS
|
|
||||||
|
|
||||||
It is easy to assume that HTTPS is something that is just "enabled" or not.
|
|
||||||
|
|
||||||
But it is way more complex than that.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
If you are in a hurry or don't care, continue with the next sections for step by step instructions to set everything up with different techniques.
|
|
||||||
|
|
||||||
To learn the basics of HTTPS, from a consumer perspective, check <a href="https://howhttps.works/" class="external-link" target="_blank">https://howhttps.works/</a>.
|
|
||||||
|
|
||||||
Now, from a developer's perspective, here are several things to have in mind while thinking about HTTPS:
|
|
||||||
|
|
||||||
* For HTTPS, the server needs to have "certificates" generated by a third party.
|
|
||||||
* Those certificates are actually acquired from the third-party, not "generated".
|
|
||||||
* Certificates have a lifetime.
|
|
||||||
* They expire.
|
|
||||||
* And then they need to be renewed, acquired again from the third party.
|
|
||||||
* The encryption of the connection happens at the TCP level.
|
|
||||||
* That's one layer below HTTP.
|
|
||||||
* So, the certificate and encryption handling is done before HTTP.
|
|
||||||
* TCP doesn't know about "domains". Only about IP addresses.
|
|
||||||
* The information about the specific domain requested goes in the HTTP data.
|
|
||||||
* The HTTPS certificates "certify" a certain domain, but the protocol and encryption happen at the TCP level, before knowing which domain is being dealt with.
|
|
||||||
* By default, that would mean that you can only have one HTTPS certificate per IP address.
|
|
||||||
* No matter how big your server is or how small each application you have on it might be.
|
|
||||||
* There is a solution to this, however.
|
|
||||||
* There's an extension to the TLS protocol (the one handling the encryption at the TCP level, before HTTP) called <a href="https://en.wikipedia.org/wiki/Server_Name_Indication" class="external-link" target="_blank"><abbr title="Server Name Indication">SNI</abbr></a>.
|
|
||||||
* This SNI extension allows one single server (with a single IP address) to have several HTTPS certificates and serve multiple HTTPS domains/applications.
|
|
||||||
* For this to work, a single component (program) running on the server, listening on the public IP address, must have all the HTTPS certificates in the server.
|
|
||||||
* After obtaining a secure connection, the communication protocol is still HTTP.
|
|
||||||
* The contents are encrypted, even though they are being sent with the HTTP protocol.
|
|
||||||
|
|
||||||
It is a common practice to have one program/HTTP server running on the server (the machine, host, etc.) and managing all the HTTPS parts : sending the decrypted HTTP requests to the actual HTTP application running in the same server (the **FastAPI** application, in this case), take the HTTP response from the application, encrypt it using the appropriate certificate and sending it back to the client using HTTPS. This server is often called a <a href="https://en.wikipedia.org/wiki/TLS_termination_proxy" class="external-link" target="_blank">TLS Termination Proxy</a>.
|
|
||||||
|
|
||||||
## Let's Encrypt
|
|
||||||
|
|
||||||
Before Let's Encrypt, these HTTPS certificates were sold by trusted third-parties.
|
|
||||||
|
|
||||||
The process to acquire one of these certificates used to be cumbersome, require quite some paperwork and the certificates were quite expensive.
|
|
||||||
|
|
||||||
But then <a href="https://letsencrypt.org/" class="external-link" target="_blank">Let's Encrypt</a> was created.
|
|
||||||
|
|
||||||
It is a project from the Linux Foundation. It provides HTTPS certificates for free. In an automated way. These certificates use all the standard cryptographic security, and are short lived (about 3 months), so the security is actually better because of their reduced lifespan.
|
|
||||||
|
|
||||||
The domains are securely verified and the certificates are generated automatically. This also allows automating the renewal of these certificates.
|
|
||||||
|
|
||||||
The idea is to automate the acquisition and renewal of these certificates, so that you can have secure HTTPS, for free, forever.
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# Deployment - Intro
|
|
||||||
|
|
||||||
Deploying a **FastAPI** application is relatively easy.
|
|
||||||
|
|
||||||
There are several ways to do it depending on your specific use case and the tools that you use.
|
|
||||||
|
|
||||||
You will see more details to have in mind and some of the techniques to do it in the next sections.
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
# Deploy manually
|
|
||||||
|
|
||||||
You can deploy **FastAPI** manually as well.
|
|
||||||
|
|
||||||
You just need to install an ASGI compatible server like:
|
|
||||||
|
|
||||||
=== "Uvicorn"
|
|
||||||
|
|
||||||
* <a href="https://www.uvicorn.org/" class="external-link" target="_blank">Uvicorn</a>, a lightning-fast ASGI server, built on uvloop and httptools.
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ pip install uvicorn[standard]
|
|
||||||
|
|
||||||
---> 100%
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
By adding the `standard`, Uvicorn will install and use some recommended extra dependencies.
|
|
||||||
|
|
||||||
That including `uvloop`, the high-performance drop-in replacement for `asyncio`, that provides the big concurrency performance boost.
|
|
||||||
|
|
||||||
=== "Hypercorn"
|
|
||||||
|
|
||||||
* <a href="https://gitlab.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>, an ASGI server also compatible with HTTP/2.
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ pip install hypercorn
|
|
||||||
|
|
||||||
---> 100%
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
...or any other ASGI server.
|
|
||||||
|
|
||||||
And run your application the same way you have done in the tutorials, but without the `--reload` option, e.g.:
|
|
||||||
|
|
||||||
=== "Uvicorn"
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ uvicorn main:app --host 0.0.0.0 --port 80
|
|
||||||
|
|
||||||
<span style="color: green;">INFO</span>: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit)
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
=== "Hypercorn"
|
|
||||||
|
|
||||||
<div class="termy">
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ hypercorn main:app --bind 0.0.0.0:80
|
|
||||||
|
|
||||||
Running on 0.0.0.0:8080 over http (CTRL + C to quit)
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
You might want to set up some tooling to make sure it is restarted automatically if it stops.
|
|
||||||
|
|
||||||
You might also want to install <a href="https://gunicorn.org/" class="external-link" target="_blank">Gunicorn</a> and <a href="https://www.uvicorn.org/#running-with-gunicorn" class="external-link" target="_blank">use it as a manager for Uvicorn</a>, or use Hypercorn with multiple workers.
|
|
||||||
|
|
||||||
Making sure to fine-tune the number of workers, etc.
|
|
||||||
|
|
||||||
But if you are doing all that, you might just use the Docker image that does it automatically.
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
# About FastAPI versions
|
|
||||||
|
|
||||||
**FastAPI** is already being used in production in many applications and systems. And the test coverage is kept at 100%. But its development is still moving quickly.
|
|
||||||
|
|
||||||
New features are added frequently, bugs are fixed regularly, and the code is still continuously improving.
|
|
||||||
|
|
||||||
That's why the current versions are still `0.x.x`, this reflects that each version could potentially have breaking changes. This follows the <a href="https://semver.org/" class="external-link" target="_blank">Semantic Versioning</a> conventions.
|
|
||||||
|
|
||||||
You can create production applications with **FastAPI** right now (and you have probably been doing it for some time), you just have to make sure that you use a version that works correctly with the rest of your code.
|
|
||||||
|
|
||||||
## Pin your `fastapi` version
|
|
||||||
|
|
||||||
The first thing you should do is to "pin" the version of **FastAPI** you are using to the specific latest version that you know works correctly for your application.
|
|
||||||
|
|
||||||
For example, let's say you are using version `0.45.0` in your app.
|
|
||||||
|
|
||||||
If you use a `requirements.txt` file you could specify the version with:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
fastapi==0.45.0
|
|
||||||
```
|
|
||||||
|
|
||||||
that would mean that you would use exactly the version `0.45.0`.
|
|
||||||
|
|
||||||
Or you could also pin it with:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
fastapi>=0.45.0,<0.46.0
|
|
||||||
```
|
|
||||||
|
|
||||||
that would mean that you would use the versions `0.45.0` or above, but less than `0.46.0`, for example, a version `0.45.2` would still be accepted.
|
|
||||||
|
|
||||||
If you use any other tool to manage your installations, like Poetry, Pipenv, or others, they all have a way that you can use to define specific versions for your packages.
|
|
||||||
|
|
||||||
## Available versions
|
|
||||||
|
|
||||||
You can see the available versions (e.g. to check what is the current latest) in the [Release Notes](../release-notes.md){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
## About versions
|
|
||||||
|
|
||||||
Following the Semantic Versioning conventions, any version below `1.0.0` could potentially add breaking changes.
|
|
||||||
|
|
||||||
FastAPI also follows the convention that any "PATCH" version change is for bug fixes and non-breaking changes.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The "PATCH" is the last number, for example, in `0.2.3`, the PATCH version is `3`.
|
|
||||||
|
|
||||||
So, you should be able to pin to a version like:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
fastapi>=0.45.0,<0.46.0
|
|
||||||
```
|
|
||||||
|
|
||||||
Breaking changes and new features are added in "MINOR" versions.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The "MINOR" is the number in the middle, for example, in `0.2.3`, the MINOR version is `2`.
|
|
||||||
|
|
||||||
## Upgrading the FastAPI versions
|
|
||||||
|
|
||||||
You should add tests for your app.
|
|
||||||
|
|
||||||
With **FastAPI** it's very easy (thanks to Starlette), check the docs: [Testing](../tutorial/testing.md){.internal-link target=_blank}
|
|
||||||
|
|
||||||
After you have tests, then you can upgrade the **FastAPI** version to a more recent one, and make sure that all your code is working correctly by running your tests.
|
|
||||||
|
|
||||||
If everything is working, or after you make the necessary changes, and all your tests are passing, then you can pin your `fastapi` to that new recent version.
|
|
||||||
|
|
||||||
## About Starlette
|
|
||||||
|
|
||||||
You shouldn't pin the version of `starlette`.
|
|
||||||
|
|
||||||
Different versions of **FastAPI** will use a specific newer version of Starlette.
|
|
||||||
|
|
||||||
So, you can just let **FastAPI** use the correct Starlette version.
|
|
||||||
|
|
||||||
## About Pydantic
|
|
||||||
|
|
||||||
Pydantic includes the tests for **FastAPI** with its own tests, so new versions of Pydantic (above `1.0.0`) are always compatible with FastAPI.
|
|
||||||
|
|
||||||
You can pin Pydantic to any version above `1.0.0` that works for you and below `2.0.0`.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
pydantic>=1.2.0,<2.0.0
|
|
||||||
```
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
# External Links and Articles
|
|
||||||
|
|
||||||
**FastAPI** has a great community constantly growing.
|
|
||||||
|
|
||||||
There are many posts, articles, tools, and projects, related to **FastAPI**.
|
|
||||||
|
|
||||||
Here's an incomplete list of some of them.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
If you have an article, project, tool, or anything related to **FastAPI** that is not yet listed here, create a <a href="https://github.com/tiangolo/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">Pull Request adding it</a>.
|
|
||||||
|
|
||||||
## Articles
|
|
||||||
|
|
||||||
### English
|
|
||||||
|
|
||||||
{% if external_links %}
|
|
||||||
{% for article in external_links.articles.english %}
|
|
||||||
|
|
||||||
* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>.
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
### Japanese
|
|
||||||
|
|
||||||
{% if external_links %}
|
|
||||||
{% for article in external_links.articles.japanese %}
|
|
||||||
|
|
||||||
* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>.
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
### Vietnamese
|
|
||||||
|
|
||||||
{% if external_links %}
|
|
||||||
{% for article in external_links.articles.vietnamese %}
|
|
||||||
|
|
||||||
* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>.
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
### Russian
|
|
||||||
|
|
||||||
{% if external_links %}
|
|
||||||
{% for article in external_links.articles.russian %}
|
|
||||||
|
|
||||||
* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>.
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
### German
|
|
||||||
|
|
||||||
{% if external_links %}
|
|
||||||
{% for article in external_links.articles.german %}
|
|
||||||
|
|
||||||
* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>.
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
## Podcasts
|
|
||||||
|
|
||||||
{% if external_links %}
|
|
||||||
{% for article in external_links.podcasts.english %}
|
|
||||||
|
|
||||||
* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>.
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
## Talks
|
|
||||||
|
|
||||||
{% if external_links %}
|
|
||||||
{% for article in external_links.talks.english %}
|
|
||||||
|
|
||||||
* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>.
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
## Projects
|
|
||||||
|
|
||||||
Latest GitHub projects with the topic `fastapi`:
|
|
||||||
|
|
||||||
<div class="github-topic-projects">
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
# FastAPI People
|
|
||||||
|
|
||||||
FastAPI has an amazing community that welcomes people from all backgrounds.
|
|
||||||
|
|
||||||
## Creator - Maintainer
|
|
||||||
|
|
||||||
Hey! 👋
|
|
||||||
|
|
||||||
This is me:
|
|
||||||
|
|
||||||
{% if people %}
|
|
||||||
<div class="user-list user-list-center">
|
|
||||||
{% for user in people.maintainers %}
|
|
||||||
|
|
||||||
<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Answers: {{ user.answers }}</div><div class="count">Pull Requests: {{ user.prs }}</div></div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
I'm the creator and maintainer of **FastAPI**. You can read more about that in [Help FastAPI - Get Help - Connect with the author](help-fastapi.md#connect-with-the-author){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
...But here I want to show you the community.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**FastAPI** receives a lot of support from the community. And I want to highlight their contributions.
|
|
||||||
|
|
||||||
These are the people that:
|
|
||||||
|
|
||||||
* [Help others with issues (questions) in GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank}.
|
|
||||||
* [Create Pull Requests](help-fastapi.md#create-a-pull-request){.internal-link target=_blank}.
|
|
||||||
* Review Pull Requests, [especially important for translations](contributing.md#translations){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
A round of applause to them. 👏 🙇
|
|
||||||
|
|
||||||
## Most active users last month
|
|
||||||
|
|
||||||
These are the users that have been [helping others the most with issues (questions) in GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} during the last month. ☕
|
|
||||||
|
|
||||||
{% if people %}
|
|
||||||
<div class="user-list user-list-center">
|
|
||||||
{% for user in people.last_month_active %}
|
|
||||||
|
|
||||||
<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Issues replied: {{ user.count }}</div></div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
## Experts
|
|
||||||
|
|
||||||
Here are the **FastAPI Experts**. 🤓
|
|
||||||
|
|
||||||
These are the users that have [helped others the most with issues (questions) in GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} through *all time*.
|
|
||||||
|
|
||||||
They have proven to be experts by helping many others. ✨
|
|
||||||
|
|
||||||
{% if people %}
|
|
||||||
<div class="user-list user-list-center">
|
|
||||||
{% for user in people.experts %}
|
|
||||||
|
|
||||||
<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Issues replied: {{ user.count }}</div></div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
## Top Contributors
|
|
||||||
|
|
||||||
Here are the **Top Contributors**. 👷
|
|
||||||
|
|
||||||
These users have [created the most Pull Requests](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} that have been *merged*.
|
|
||||||
|
|
||||||
They have contributed source code, documentation, translations, etc. 📦
|
|
||||||
|
|
||||||
{% if people %}
|
|
||||||
<div class="user-list user-list-center">
|
|
||||||
{% for user in people.top_contributors %}
|
|
||||||
|
|
||||||
<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Pull Requests: {{ user.count }}</div></div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
There are many other contributors (more than a hundred), you can see them all in the <a href="https://github.com/tiangolo/fastapi/graphs/contributors" class="external-link" target="_blank">FastAPI GitHub Contributors page</a>. 👷
|
|
||||||
|
|
||||||
## Top Reviewers
|
|
||||||
|
|
||||||
These users are the **Top Reviewers**. 🕵️
|
|
||||||
|
|
||||||
### Reviews for Translations
|
|
||||||
|
|
||||||
I only speak a few languages (and not very well 😅). So, the reviewers are the ones that have the [**power to approve translations**](contributing.md#translations){.internal-link target=_blank} of the documentation. Without them, there wouldn't be documentation in several other languages.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
The **Top Reviewers** 🕵️ have reviewed the most Pull Requests from others, ensuring the quality of the code, documentation, and especially, the **translations**.
|
|
||||||
|
|
||||||
{% if people %}
|
|
||||||
<div class="user-list user-list-center">
|
|
||||||
{% for user in people.top_reviewers %}
|
|
||||||
|
|
||||||
<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Reviews: {{ user.count }}</div></div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
## Sponsors
|
|
||||||
|
|
||||||
These are the **Sponsors**. 😎
|
|
||||||
|
|
||||||
They are supporting my work with **FastAPI** (and others), mainly through <a href="https://github.com/sponsors/tiangolo" class="external-link" target="_blank">GitHub Sponsors</a>.
|
|
||||||
|
|
||||||
### Gold Sponsors
|
|
||||||
|
|
||||||
{% if sponsors %}
|
|
||||||
{% for sponsor in sponsors.gold -%}
|
|
||||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
### Silver Sponsors
|
|
||||||
|
|
||||||
{% if sponsors %}
|
|
||||||
{% for sponsor in sponsors.silver -%}
|
|
||||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
### Bronze Sponsors
|
|
||||||
|
|
||||||
{% if sponsors %}
|
|
||||||
{% for sponsor in sponsors.bronze -%}
|
|
||||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}"></a>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
### Individual Sponsors
|
|
||||||
|
|
||||||
{% if people %}
|
|
||||||
{% if people.sponsors_50 %}
|
|
||||||
|
|
||||||
<div class="user-list user-list-center">
|
|
||||||
{% for user in people.sponsors_50 %}
|
|
||||||
|
|
||||||
<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a></div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if people %}
|
|
||||||
<div class="user-list user-list-center">
|
|
||||||
{% for user in people.sponsors %}
|
|
||||||
|
|
||||||
<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a></div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
## About the data - technical details
|
|
||||||
|
|
||||||
The main intention of this page is to highlight the effort of the community to help others.
|
|
||||||
|
|
||||||
Especially including efforts that are normally less visible, and in many cases more arduous, like helping others with issues and reviewing Pull Requests with translations.
|
|
||||||
|
|
||||||
The data is calculated each month, you can read the <a href="https://github.com/tiangolo/fastapi/blob/master/.github/actions/people/app/main.py" class="external-link" target="_blank">source code here</a>.
|
|
||||||
|
|
||||||
Here I'm also highlighting contributions from sponsors.
|
|
||||||
|
|
||||||
I also reserve the right to update the algorithm, sections, thresholds, etc (just in case 🤷).
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
# Help FastAPI - Get Help
|
|
||||||
|
|
||||||
Do you like **FastAPI**?
|
|
||||||
|
|
||||||
Would you like to help FastAPI, other users, and the author?
|
|
||||||
|
|
||||||
Or would you like to get help with **FastAPI**?
|
|
||||||
|
|
||||||
There are very simple ways to help (several involve just one or two clicks).
|
|
||||||
|
|
||||||
And there are several ways to get help too.
|
|
||||||
|
|
||||||
## Subscribe to the newsletter
|
|
||||||
|
|
||||||
You can subscribe to the (infrequent) [**FastAPI and friends** newsletter](/newsletter/){.internal-link target=_blank} to stay updated about:
|
|
||||||
|
|
||||||
* News about FastAPI and friends 🚀
|
|
||||||
* Guides 📝
|
|
||||||
* Features ✨
|
|
||||||
* Breaking changes 🚨
|
|
||||||
* Tips and tricks ✅
|
|
||||||
|
|
||||||
## Star **FastAPI** in GitHub
|
|
||||||
|
|
||||||
You can "star" FastAPI in GitHub (clicking the star button at the top right): <a href="https://github.com/tiangolo/fastapi" class="external-link" target="_blank">https://github.com/tiangolo/fastapi</a>. ⭐️
|
|
||||||
|
|
||||||
By adding a star, other users will be able to find it more easily and see that it has been already useful for others.
|
|
||||||
|
|
||||||
## Watch the GitHub repository for releases
|
|
||||||
|
|
||||||
You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): <a href="https://github.com/tiangolo/fastapi" class="external-link" target="_blank">https://github.com/tiangolo/fastapi</a>. 👀
|
|
||||||
|
|
||||||
There you can select "Releases only".
|
|
||||||
|
|
||||||
Doing it, you will receive notifications (in your email) whenever there's a new release (a new version) of **FastAPI** with bug fixes and new features.
|
|
||||||
|
|
||||||
## Connect with the author
|
|
||||||
|
|
||||||
You can connect with <a href="https://tiangolo.com" class="external-link" target="_blank">me (Sebastián Ramírez / `tiangolo`)</a>, the author.
|
|
||||||
|
|
||||||
You can:
|
|
||||||
|
|
||||||
* <a href="https://github.com/tiangolo" class="external-link" target="_blank">Follow me on **GitHub**</a>.
|
|
||||||
* See other Open Source projects I have created that could help you.
|
|
||||||
* Follow me to see when I create a new Open Source project.
|
|
||||||
* <a href="https://twitter.com/tiangolo" class="external-link" target="_blank">Follow me on **Twitter**</a>.
|
|
||||||
* Tell me how you use FastAPI (I love to hear that).
|
|
||||||
* Hear when I make announcements or release new tools.
|
|
||||||
* <a href="https://www.linkedin.com/in/tiangolo/" class="external-link" target="_blank">Connect with me on **Linkedin**</a>.
|
|
||||||
* Hear when I make announcements or release new tools (although I use Twitter more often 🤷♂).
|
|
||||||
* Read what I write (or follow me) on <a href="https://dev.to/tiangolo" class="external-link" target="_blank">**Dev.to**</a> or <a href="https://medium.com/@tiangolo" class="external-link" target="_blank">**Medium**</a>.
|
|
||||||
* Read other ideas, articles, and about tools I have created.
|
|
||||||
* Follow me to read when I publish something new.
|
|
||||||
|
|
||||||
## Tweet about **FastAPI**
|
|
||||||
|
|
||||||
<a href="https://twitter.com/compose/tweet?text=I'm loving FastAPI because... https://github.com/tiangolo/fastapi cc @tiangolo" class="external-link" target="_blank">Tweet about **FastAPI**</a> and let me and others know why you like it. 🎉
|
|
||||||
|
|
||||||
I love to hear about how **FastAPI** is being used, what have you liked in it, in which project/company are you using it, etc.
|
|
||||||
|
|
||||||
## Vote for FastAPI
|
|
||||||
|
|
||||||
* <a href="https://www.slant.co/options/34241/~fastapi-review" class="external-link" target="_blank">Vote for **FastAPI** in Slant</a>.
|
|
||||||
* <a href="https://alternativeto.net/software/fastapi/" class="external-link" target="_blank">Vote for **FastAPI** in AlternativeTo</a>.
|
|
||||||
|
|
||||||
## Help others with issues in GitHub
|
|
||||||
|
|
||||||
You can see <a href="https://github.com/tiangolo/fastapi/issues" class="external-link" target="_blank">existing issues</a> and try and help others, most of the times they are questions that you might already know the answer for. 🤓
|
|
||||||
|
|
||||||
If you are helping a lot of people on issues you might become an official [FastAPI Expert](fastapi-people.md#experts){.internal-link target=_blank}. 🎉
|
|
||||||
|
|
||||||
## Watch the GitHub repository
|
|
||||||
|
|
||||||
You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): <a href="https://github.com/tiangolo/fastapi" class="external-link" target="_blank">https://github.com/tiangolo/fastapi</a>. 👀
|
|
||||||
|
|
||||||
If you select "Watching" instead of "Releases only", you will receive notifications when someone creates a new issue.
|
|
||||||
|
|
||||||
Then you can try and help them solving those issues.
|
|
||||||
|
|
||||||
## Create issues
|
|
||||||
|
|
||||||
You can <a href="https://github.com/tiangolo/fastapi/issues/new/choose" class="external-link" target="_blank">create a new issue</a> in the GitHub repository, for example to:
|
|
||||||
|
|
||||||
* Ask a **question** or ask about a **problem**.
|
|
||||||
* Suggest a new **feature**.
|
|
||||||
|
|
||||||
**Note**: if you create an issue then I'm going to ask you to also help others. 😉
|
|
||||||
|
|
||||||
## Create a Pull Request
|
|
||||||
|
|
||||||
You can [contribute](contributing.md){.internal-link target=_blank} to the source code with Pull Requests, for example:
|
|
||||||
|
|
||||||
* To fix a typo you found on the documentation.
|
|
||||||
* To share an article, video, or podcast you created or found about FastAPI by <a href="https://github.com/tiangolo/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">editing this file</a>.
|
|
||||||
* Make sure you add your link to the end of the corresponding section.
|
|
||||||
* To help [translate the documentation](contributing.md#translations){.internal-link target=_blank} to your language.
|
|
||||||
* You can also help reviewing the translations created by others.
|
|
||||||
* To propose new documentation sections.
|
|
||||||
* To fix an existing issue/bug.
|
|
||||||
* To add a new feature.
|
|
||||||
|
|
||||||
## Join the chat
|
|
||||||
|
|
||||||
Join the 👥 <a href="https://discord.gg/VQjSZaeJmf" class="external-link" target="_blank">Discord chat server</a> 👥 and hang out with others in the FastAPI community.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
For questions, ask them in <a href="https://github.com/tiangolo/fastapi/issues/new/choose" class="external-link" target="_blank">GitHub issues</a>, there's a much better chance you will receive help by the [FastAPI Experts](fastapi-people.md#experts){.internal-link target=_blank}.
|
|
||||||
|
|
||||||
Use the chat only for other general conversations.
|
|
||||||
|
|
||||||
There is also the previous <a href="https://gitter.im/tiangolo/fastapi" class="external-link" target="_blank">Gitter chat</a>, but as it doesn't have channels and advanced features, conversations are more difficult, so Discord is now the recommended system.
|
|
||||||
|
|
||||||
### Don't use the chat for questions
|
|
||||||
|
|
||||||
Have in mind that as chats allow more "free conversation", it's easy to ask questions that are too general and more difficult to answer, so, you might not receive answers.
|
|
||||||
|
|
||||||
In GitHub issues the template will guide you to write the right question so that you can more easily get a good answer, or even solve the problem yourself even before asking. And in GitHub I can make sure I always answer everything, even if it takes some time. I can't personally do that with the chat systems. 😅
|
|
||||||
|
|
||||||
Conversations in the chat systems are also not as easily searchable as in GitHub, so questions and answers might get lost in the conversation. And only the ones in GitHub issues count to become a [FastAPI Expert](fastapi-people.md#experts){.internal-link target=_blank}, so you will most probably receive more attention in GitHub isssues.
|
|
||||||
|
|
||||||
On the other side, there are thousands of users in the chat systems, so there's a high chance you'll find someone to talk to there, almost all the time. 😄
|
|
||||||
|
|
||||||
## Sponsor the author
|
|
||||||
|
|
||||||
You can also financially support the author (me) through <a href="https://github.com/sponsors/tiangolo" class="external-link" target="_blank">GitHub sponsors</a>.
|
|
||||||
|
|
||||||
There you could buy me a coffee ☕️ to say thanks. 😄
|
|
||||||
|
|
||||||
And you can also become a Silver or Gold sponsor for FastAPI. 🏅🎉
|
|
||||||
|
|
||||||
## Sponsor the tools that power FastAPI
|
|
||||||
|
|
||||||
As you have seen in the documentation, FastAPI stands on the shoulders of giants, Starlette and Pydantic.
|
|
||||||
|
|
||||||
You can also sponsor:
|
|
||||||
|
|
||||||
* <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>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Thanks! 🚀
|
|
||||||
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
|
@ -1,98 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
id="svg8"
|
|
||||||
version="1.1"
|
|
||||||
viewBox="0 0 338.66665 169.33332"
|
|
||||||
height="169.33333mm"
|
|
||||||
width="338.66666mm"
|
|
||||||
sodipodi:docname="github-social-preview.svg"
|
|
||||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
|
||||||
inkscape:export-filename="/home/user/code/fastapi/docs/img/github-social-preview.png"
|
|
||||||
inkscape:export-xdpi="96"
|
|
||||||
inkscape:export-ydpi="96">
|
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1025"
|
|
||||||
id="namedview9"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="0.52249777"
|
|
||||||
inkscape:cx="565.37328"
|
|
||||||
inkscape:cy="403.61034"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="27"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg8" />
|
|
||||||
<defs
|
|
||||||
id="defs2" />
|
|
||||||
<metadata
|
|
||||||
id="metadata5">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<rect
|
|
||||||
style="opacity:0.98000004;fill:#ffffff;fill-opacity:1;stroke-width:0.26458332"
|
|
||||||
id="rect853"
|
|
||||||
width="338.66666"
|
|
||||||
height="169.33333"
|
|
||||||
x="-1.0833333e-05"
|
|
||||||
y="0.71613133"
|
|
||||||
inkscape:export-xdpi="96"
|
|
||||||
inkscape:export-ydpi="96" />
|
|
||||||
<g
|
|
||||||
transform="matrix(0.73259569,0,0,0.73259569,64.842852,-4.5763945)"
|
|
||||||
id="layer1">
|
|
||||||
<path
|
|
||||||
style="opacity:0.98000004;fill:#009688;fill-opacity:1;stroke-width:3.20526505"
|
|
||||||
id="path817"
|
|
||||||
d="m 1.4365174,55.50154 c -17.6610514,0 -31.9886064,14.327532 -31.9886064,31.988554 0,17.661036 14.327555,31.988586 31.9886064,31.988586 17.6609756,0 31.9885196,-14.32755 31.9885196,-31.988586 0,-17.661022 -14.327544,-31.988554 -31.9885196,-31.988554 z m -1.66678692,57.63069 V 93.067264 H -11.384533 L 4.6417437,61.847974 V 81.912929 H 15.379405 Z"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
<text
|
|
||||||
id="text979"
|
|
||||||
y="114.91215"
|
|
||||||
x="52.115433"
|
|
||||||
style="font-style:normal;font-weight:normal;font-size:79.71511078px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#009688;fill-opacity:1;stroke:none;stroke-width:1.99287772"
|
|
||||||
xml:space="preserve"><tspan
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772"
|
|
||||||
y="114.91215"
|
|
||||||
x="52.115433"
|
|
||||||
id="tspan977">FastAPI</tspan></text>
|
|
||||||
</g>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
|
||||||
x="169.60979"
|
|
||||||
y="119.20409"
|
|
||||||
id="text851"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan849"
|
|
||||||
x="169.60979"
|
|
||||||
y="119.20409"
|
|
||||||
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:'Roboto Italic';text-align:center;text-anchor:middle;stroke-width:0.26458332">High performance, easy to learn,</tspan><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
x="169.60979"
|
|
||||||
y="132.53661"
|
|
||||||
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:'Roboto Italic';text-align:center;text-anchor:middle;stroke-width:0.26458332"
|
|
||||||
id="tspan855">fast to code, ready for production</tspan></text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
|
@ -1,293 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="429.96896"
|
|
||||||
height="40"
|
|
||||||
viewBox="0 0 113.76262 10.583333"
|
|
||||||
version="1.1"
|
|
||||||
id="svg853"
|
|
||||||
inkscape:version="1.0.2 (1.0.2+r75+1)"
|
|
||||||
sodipodi:docname="fastapi-course-bundle-banner.svg">
|
|
||||||
<defs
|
|
||||||
id="defs847" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="2.8"
|
|
||||||
inkscape:cx="222.91167"
|
|
||||||
inkscape:cy="33.521565"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:current-layer="g3355"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:snap-object-midpoints="true"
|
|
||||||
inkscape:snap-center="true"
|
|
||||||
inkscape:snap-page="true"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1025"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="27"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
units="px" />
|
|
||||||
<metadata
|
|
||||||
id="metadata850">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-80.634914,-82.650791)">
|
|
||||||
<g
|
|
||||||
id="g3355"
|
|
||||||
transform="matrix(1.4999999,0,0,1.4999999,-40.317455,-41.325393)">
|
|
||||||
<rect
|
|
||||||
style="fill:#ffffff;stroke-width:0.197707;stop-color:#000000"
|
|
||||||
id="rect1784"
|
|
||||||
width="75.841751"
|
|
||||||
height="7.0555558"
|
|
||||||
x="80.634918"
|
|
||||||
y="82.650795" />
|
|
||||||
<g
|
|
||||||
aria-label="FastAPI Course Bundle. Get 3 in-depth courses. Save 10%!"
|
|
||||||
transform="scale(1.0561291,0.94685395)"
|
|
||||||
id="text871"
|
|
||||||
style="font-size:2.43444px;line-height:1.25;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;letter-spacing:0px;word-spacing:0px;stroke-width:0.0608608">
|
|
||||||
<path
|
|
||||||
d="m 84.413426,90.970748 h -0.72629 v 0.764329 h -0.228229 v -1.730735 h 1.0722 v 0.187814 h -0.843971 v 0.591968 h 0.72629 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3922" />
|
|
||||||
<path
|
|
||||||
d="m 85.526042,91.735077 q -0.01902,-0.03804 -0.03091,-0.135511 -0.153342,0.159285 -0.366117,0.159285 -0.190191,0 -0.312626,-0.106982 -0.121247,-0.108171 -0.121247,-0.2734 0,-0.200888 0.152153,-0.311437 0.153341,-0.111737 0.430306,-0.111737 h 0.213965 v -0.101038 q 0,-0.115303 -0.06894,-0.183059 -0.06894,-0.06894 -0.203266,-0.06894 -0.11768,0 -0.197323,0.05943 -0.07964,0.05943 -0.07964,0.143831 h -0.221096 q 0,-0.09628 0.06775,-0.185436 0.06894,-0.09034 0.185436,-0.142643 0.11768,-0.0523 0.257946,-0.0523 0.222285,0 0.348286,0.111737 0.126002,0.110548 0.130756,0.305494 v 0.591968 q 0,0.177115 0.04517,0.28172 v 0.01902 z m -0.364929,-0.167606 q 0.103416,0 0.196134,-0.05349 0.09272,-0.05349 0.134323,-0.139077 v -0.263889 h -0.172361 q -0.404155,0 -0.404155,0.23655 0,0.103416 0.06894,0.161662 0.06894,0.05824 0.177115,0.05824 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3924" />
|
|
||||||
<path
|
|
||||||
d="m 86.807451,91.393922 q 0,-0.08915 -0.06776,-0.137888 -0.06657,-0.04993 -0.234172,-0.08558 -0.166417,-0.03566 -0.265078,-0.08559 -0.09747,-0.04993 -0.14502,-0.118869 -0.04636,-0.06894 -0.04636,-0.16404 0,-0.158096 0.133133,-0.267455 0.134322,-0.10936 0.342343,-0.10936 0.21872,0 0.35423,0.112926 0.1367,0.112925 0.1367,0.288852 h -0.221097 q 0,-0.09034 -0.07726,-0.155719 -0.07608,-0.06538 -0.192568,-0.06538 -0.120058,0 -0.187813,0.0523 -0.06776,0.0523 -0.06776,0.136699 0,0.07964 0.063,0.120058 0.063,0.04042 0.22704,0.07726 0.165228,0.03685 0.267456,0.08796 0.102227,0.05111 0.150964,0.123624 0.04993,0.07132 0.04993,0.174737 0,0.172361 -0.137888,0.276966 -0.137889,0.103416 -0.357797,0.103416 -0.154529,0 -0.273399,-0.05468 -0.118869,-0.05468 -0.186624,-0.152153 -0.06657,-0.09866 -0.06657,-0.212775 h 0.219908 q 0.0059,0.110548 0.08796,0.175926 0.08321,0.06419 0.218719,0.06419 0.124813,0 0.199701,-0.04993 0.07608,-0.05111 0.07608,-0.135511 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3926" />
|
|
||||||
<path
|
|
||||||
d="m 87.612195,90.137476 v 0.311437 h 0.240116 v 0.169983 h -0.240116 v 0.797612 q 0,0.07727 0.03209,0.116491 0.03209,0.03804 0.109359,0.03804 0.03804,0 0.104605,-0.01426 v 0.178304 q -0.08677,0.02377 -0.168794,0.02377 -0.147398,0 -0.222285,-0.08915 -0.07489,-0.08915 -0.07489,-0.253191 v -0.797612 h -0.234172 v -0.169983 h 0.234172 v -0.311437 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3928" />
|
|
||||||
<path
|
|
||||||
d="m 89.098059,91.283374 h -0.725102 l -0.16285,0.451703 h -0.235361 l 0.660912,-1.730735 h 0.1997 l 0.662102,1.730735 h -0.234173 z m -0.656157,-0.187813 h 0.588402 l -0.294796,-0.809499 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3930" />
|
|
||||||
<path
|
|
||||||
d="m 89.958671,91.057523 v 0.677554 h -0.228228 v -1.730735 h 0.638327 q 0.284097,0 0.444571,0.145021 0.161662,0.14502 0.161662,0.383947 0,0.252003 -0.158096,0.388702 -0.156908,0.135511 -0.450514,0.135511 z m 0,-0.186625 h 0.410099 q 0.183058,0 0.280531,-0.08558 0.09747,-0.08678 0.09747,-0.249626 0,-0.15453 -0.09747,-0.247247 -0.09747,-0.09272 -0.267455,-0.09628 h -0.423175 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3932" />
|
|
||||||
<path
|
|
||||||
d="m 91.511103,91.735077 h -0.228229 v -1.730735 h 0.228229 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3934" />
|
|
||||||
<path
|
|
||||||
d="m 93.843315,91.158562 q -0.02021,0.279342 -0.206833,0.439815 -0.185436,0.160474 -0.48974,0.160474 -0.332834,0 -0.524213,-0.223474 -0.190191,-0.224663 -0.190191,-0.615742 v -0.105794 q 0,-0.249625 0.08796,-0.439816 0.08796,-0.19019 0.250814,-0.291229 0.164039,-0.102228 0.380381,-0.102228 0.29955,0 0.482609,0.160474 0.183058,0.160473 0.211587,0.450514 h -0.356607 q -0.01308,-0.167606 -0.09391,-0.242493 -0.07964,-0.07608 -0.243682,-0.07608 -0.178303,0 -0.267455,0.128378 -0.08796,0.12719 -0.09034,0.395834 v 0.130756 q 0,0.280532 0.0844,0.410099 0.08559,0.129567 0.268645,0.129567 0.165228,0 0.246059,-0.07489 0.08202,-0.07608 0.09391,-0.234172 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path3936" />
|
|
||||||
<path
|
|
||||||
d="m 94.001411,91.080108 q 0,-0.191379 0.0737,-0.341154 0.0737,-0.149776 0.211587,-0.231795 0.139077,-0.08202 0.322135,-0.08202 0.260324,0 0.424363,0.159285 0.165228,0.159284 0.184247,0.432683 l 0.0024,0.08796 q 0,0.295985 -0.165228,0.475477 -0.165229,0.178304 -0.443382,0.178304 -0.278154,0 -0.444571,-0.178304 -0.165228,-0.178304 -0.165228,-0.484986 z m 0.343532,0.02496 q 0,0.183059 0.06894,0.280532 0.06894,0.09628 0.197323,0.09628 0.124812,0 0.194945,-0.0951 0.07013,-0.09628 0.07013,-0.306682 0,-0.179492 -0.07013,-0.278154 -0.07013,-0.09866 -0.197323,-0.09866 -0.126001,0 -0.194945,0.09866 -0.06894,0.09747 -0.06894,0.303116 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path3938" />
|
|
||||||
<path
|
|
||||||
d="m 96.204056,91.604321 q -0.12719,0.15453 -0.351853,0.15453 -0.206832,0 -0.316192,-0.118869 -0.108171,-0.11887 -0.110548,-0.348287 v -0.842782 h 0.343532 v 0.830895 q 0,0.200889 0.183058,0.200889 0.174738,0 0.240116,-0.121247 v -0.910537 h 0.34472 v 1.286164 h -0.323324 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path3940" />
|
|
||||||
<path
|
|
||||||
d="m 97.521126,90.771048 q -0.07013,-0.0095 -0.123624,-0.0095 -0.194945,0 -0.255568,0.131944 v 0.841594 h -0.343532 v -1.286164 h 0.324513 l 0.0095,0.153341 q 0.103416,-0.177115 0.286475,-0.177115 0.05706,0 0.106982,0.01545 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path3942" />
|
|
||||||
<path
|
|
||||||
d="m 98.384116,91.379658 q 0,-0.063 -0.063,-0.09866 -0.06181,-0.03685 -0.1997,-0.06538 -0.458835,-0.09628 -0.458835,-0.389891 0,-0.171172 0.141455,-0.285286 0.142643,-0.115303 0.37206,-0.115303 0.244871,0 0.39108,0.115303 0.147397,0.115303 0.147397,0.29955 h -0.343531 q 0,-0.0737 -0.04755,-0.121246 -0.04755,-0.04874 -0.148587,-0.04874 -0.08677,0 -0.134322,0.03923 -0.04755,0.03923 -0.04755,0.09985 0,0.05706 0.05349,0.09272 0.05468,0.03447 0.183058,0.06062 0.128379,0.02496 0.216342,0.05706 0.27221,0.09985 0.27221,0.345909 0,0.175926 -0.150963,0.285286 -0.150964,0.108171 -0.389891,0.108171 -0.161662,0 -0.287663,-0.05706 -0.124813,-0.05825 -0.196135,-0.158096 -0.07132,-0.101039 -0.07132,-0.217531 h 0.325701 q 0.0048,0.09153 0.06776,0.140266 0.063,0.04874 0.168794,0.04874 0.09866,0 0.148586,-0.03685 0.05111,-0.03804 0.05111,-0.09866 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path3944" />
|
|
||||||
<path
|
|
||||||
d="m 99.530014,91.758851 q -0.282909,0 -0.461212,-0.173549 -0.177115,-0.173549 -0.177115,-0.462401 v -0.03328 q 0,-0.193756 0.07489,-0.345909 0.07489,-0.153341 0.211587,-0.235361 0.137888,-0.08321 0.313815,-0.08321 0.263889,0 0.414853,0.166417 0.152151,0.166417 0.152151,0.47191 v 0.140266 h -0.819007 q 0.01664,0.126001 0.09985,0.202077 0.0844,0.07608 0.212776,0.07608 0.198511,0 0.310248,-0.143832 l 0.168793,0.189002 q -0.07726,0.10936 -0.209208,0.171171 -0.131945,0.06062 -0.292418,0.06062 z m -0.03923,-1.055558 q -0.102227,0 -0.166417,0.06894 -0.063,0.06894 -0.08083,0.197323 h 0.477854 v -0.02734 q -0.0024,-0.114115 -0.06181,-0.175927 -0.05943,-0.063 -0.168794,-0.063 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path3946" />
|
|
||||||
<path
|
|
||||||
d="m 100.88631,91.735077 v -1.730735 h 0.60623 q 0.31501,0 0.47786,0.121247 0.16285,0.120058 0.16285,0.353041 0,0.12719 -0.0654,0.224663 -0.0654,0.09628 -0.18187,0.141454 0.13313,0.03328 0.20921,0.134322 0.0773,0.101039 0.0773,0.247248 0,0.249625 -0.15928,0.378004 -0.15928,0.128379 -0.45408,0.130756 z m 0.35661,-0.75363 v 0.467155 h 0.30549 q 0.126,0 0.19614,-0.05943 0.0713,-0.06062 0.0713,-0.166417 0,-0.237738 -0.24606,-0.241304 z m 0,-0.252003 h 0.26389 q 0.26983,-0.0048 0.26983,-0.215153 0,-0.117681 -0.0689,-0.168794 -0.0678,-0.0523 -0.21516,-0.0523 h -0.24962 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path3948" />
|
|
||||||
<path
|
|
||||||
d="m 103.18881,91.604321 q -0.12719,0.15453 -0.35186,0.15453 -0.20683,0 -0.31619,-0.118869 -0.10817,-0.11887 -0.11055,-0.348287 v -0.842782 h 0.34353 v 0.830895 q 0,0.200889 0.18306,0.200889 0.17474,0 0.24012,-0.121247 v -0.910537 h 0.34472 v 1.286164 h -0.32333 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path3950" />
|
|
||||||
<path
|
|
||||||
d="m 104.09934,90.448913 0.0107,0.148586 q 0.13789,-0.17236 0.36968,-0.17236 0.20446,0 0.30431,0.120058 0.0999,0.120058 0.10223,0.358985 v 0.830895 h -0.34353 v -0.822575 q 0,-0.109359 -0.0476,-0.158095 -0.0476,-0.04993 -0.1581,-0.04993 -0.14502,0 -0.21753,0.123624 v 0.906972 h -0.34353 v -1.286164 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path3952" />
|
|
||||||
<path
|
|
||||||
d="m 105.09428,91.082485 q 0,-0.300739 0.13432,-0.479042 0.13551,-0.178304 0.36968,-0.178304 0.18782,0 0.31025,0.140266 v -0.656158 h 0.34472 v 1.82583 H 105.943 l -0.0166,-0.1367 q -0.12838,0.160474 -0.33045,0.160474 -0.22704,0 -0.36493,-0.178304 -0.1367,-0.179492 -0.1367,-0.498062 z m 0.34353,0.02496 q 0,0.180681 0.063,0.276965 0.063,0.09628 0.18306,0.09628 0.15928,0 0.22466,-0.134322 v -0.507571 q -0.0642,-0.134323 -0.22228,-0.134323 -0.24844,0 -0.24844,0.402967 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path3954" />
|
|
||||||
<path
|
|
||||||
d="m 106.88445,91.735077 h -0.34472 v -1.82583 h 0.34472 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path3956" />
|
|
||||||
<path
|
|
||||||
d="m 107.76051,91.758851 q -0.28291,0 -0.46121,-0.173549 -0.17711,-0.173549 -0.17711,-0.462401 v -0.03328 q 0,-0.193756 0.0749,-0.345909 0.0749,-0.153341 0.21159,-0.235361 0.13789,-0.08321 0.31382,-0.08321 0.26388,0 0.41485,0.166417 0.15215,0.166417 0.15215,0.47191 v 0.140266 h -0.81901 q 0.0166,0.126001 0.0998,0.202077 0.0844,0.07608 0.21278,0.07608 0.19851,0 0.31025,-0.143832 l 0.16879,0.189002 q -0.0773,0.10936 -0.20921,0.171171 -0.13194,0.06062 -0.29242,0.06062 z m -0.0392,-1.055558 q -0.10223,0 -0.16642,0.06894 -0.063,0.06894 -0.0808,0.197323 h 0.47785 v -0.02734 q -0.002,-0.114115 -0.0618,-0.175927 -0.0594,-0.063 -0.16879,-0.063 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path3958" />
|
|
||||||
<path
|
|
||||||
d="m 108.52484,91.619774 q 0,-0.05706 0.0333,-0.0951 0.0345,-0.03804 0.10222,-0.03804 0.0678,0 0.10223,0.03804 0.0357,0.03804 0.0357,0.0951 0,0.05468 -0.0357,0.09153 -0.0345,0.03685 -0.10223,0.03685 -0.0677,0 -0.10222,-0.03685 -0.0333,-0.03685 -0.0333,-0.09153 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3960" />
|
|
||||||
<path
|
|
||||||
d="m 111.07815,91.508037 q -0.088,0.126001 -0.24606,0.189002 -0.15691,0.06181 -0.36612,0.06181 -0.21158,0 -0.37562,-0.09866 -0.16404,-0.09985 -0.25438,-0.282908 -0.0892,-0.183059 -0.0915,-0.424363 v -0.150964 q 0,-0.391079 0.18187,-0.606232 0.18306,-0.215154 0.51351,-0.215154 0.27103,0 0.43625,0.139077 0.16523,0.137889 0.20208,0.392268 h -0.22823 q -0.0642,-0.343531 -0.40891,-0.343531 -0.22942,0 -0.34828,0.161662 -0.11768,0.160473 -0.11887,0.465967 v 0.141454 q 0,0.291229 0.13313,0.46359 0.13313,0.171171 0.36017,0.171171 0.12838,0 0.22467,-0.02853 0.0963,-0.02853 0.15928,-0.09628 v -0.388702 h -0.40059 v -0.185436 h 0.62763 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3962" />
|
|
||||||
<path
|
|
||||||
d="m 111.95897,91.758851 q -0.26151,0 -0.42555,-0.171172 -0.16404,-0.17236 -0.16404,-0.460023 v -0.04042 q 0,-0.191379 0.0725,-0.341154 0.0737,-0.150964 0.20445,-0.235361 0.13195,-0.08559 0.28529,-0.08559 0.25081,0 0.38989,0.165228 0.13908,0.165228 0.13908,0.473099 v 0.09153 h -0.87131 q 0.005,0.19019 0.11055,0.307871 0.10698,0.116491 0.27102,0.116491 0.11649,0 0.19732,-0.04755 0.0808,-0.04755 0.14145,-0.126002 l 0.13433,0.104605 q -0.16167,0.248437 -0.48499,0.248437 z m -0.0273,-1.153031 q -0.13313,0 -0.22347,0.09747 -0.0903,0.09628 -0.11174,0.271021 h 0.64427 v -0.01664 q -0.01,-0.167606 -0.0903,-0.259135 -0.0808,-0.09272 -0.21872,-0.09272 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3964" />
|
|
||||||
<path
|
|
||||||
d="m 113.01215,90.137476 v 0.311437 h 0.24012 v 0.169983 h -0.24012 v 0.797612 q 0,0.07727 0.0321,0.116491 0.0321,0.03804 0.10936,0.03804 0.038,0 0.1046,-0.01426 v 0.178304 q -0.0868,0.02377 -0.16879,0.02377 -0.1474,0 -0.22229,-0.08915 -0.0749,-0.08915 -0.0749,-0.253191 v -0.797612 h -0.23417 v -0.169983 h 0.23417 v -0.311437 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3966" />
|
|
||||||
<path
|
|
||||||
d="m 114.40886,90.762727 h 0.16523 q 0.15572,-0.0024 0.24487,-0.08202 0.0891,-0.07964 0.0891,-0.215153 0,-0.304305 -0.30311,-0.304305 -0.14264,0 -0.22823,0.08202 -0.0844,0.08083 -0.0844,0.215153 h -0.21991 q 0,-0.205643 0.14978,-0.341154 0.15096,-0.1367 0.38276,-0.1367 0.24487,0 0.38395,0.129568 0.13907,0.129567 0.13907,0.360173 0,0.112926 -0.0737,0.21872 -0.0725,0.105793 -0.19851,0.158095 0.14264,0.04517 0.21991,0.149776 0.0784,0.104604 0.0784,0.255568 0,0.232984 -0.15215,0.369683 -0.15215,0.1367 -0.39583,0.1367 -0.24368,0 -0.39703,-0.131945 -0.15215,-0.131945 -0.15215,-0.348287 h 0.2211 q 0,0.1367 0.0892,0.21872 0.0892,0.08202 0.23893,0.08202 0.15928,0 0.24368,-0.08321 0.0844,-0.08321 0.0844,-0.238927 0,-0.150964 -0.0927,-0.231795 -0.0927,-0.08083 -0.26746,-0.08321 h -0.16523 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3968" />
|
|
||||||
<path
|
|
||||||
d="m 116.32385,91.735077 h -0.21991 v -1.286164 h 0.21991 z m -0.23774,-1.627319 q 0,-0.05349 0.0321,-0.09034 0.0333,-0.03685 0.0975,-0.03685 0.0642,0 0.0975,0.03685 0.0333,0.03685 0.0333,0.09034 0,0.05349 -0.0333,0.08915 -0.0333,0.03566 -0.0975,0.03566 -0.0642,0 -0.0975,-0.03566 -0.0321,-0.03566 -0.0321,-0.08915 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3970" />
|
|
||||||
<path
|
|
||||||
d="m 116.88253,90.448913 0.007,0.161662 q 0.1474,-0.185436 0.38514,-0.185436 0.40772,0 0.41129,0.460024 v 0.849914 h -0.21991 v -0.851103 q -0.001,-0.139077 -0.0642,-0.205644 -0.0618,-0.06657 -0.19376,-0.06657 -0.10698,0 -0.18781,0.05706 -0.0808,0.05706 -0.126,0.149775 v 0.916481 h -0.21991 v -1.286164 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3972" />
|
|
||||||
<path
|
|
||||||
d="m 118.47775,91.089617 h -0.58008 v -0.179492 h 0.58008 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3974" />
|
|
||||||
<path
|
|
||||||
d="m 118.63704,91.081297 q 0,-0.295984 0.14026,-0.475477 0.14027,-0.180681 0.36731,-0.180681 0.22585,0 0.3578,0.15453 v -0.670422 h 0.2199 v 1.82583 h -0.20207 l -0.0107,-0.137888 q -0.13195,0.161662 -0.36731,0.161662 -0.22347,0 -0.36493,-0.183059 -0.14026,-0.183058 -0.14026,-0.477854 z m 0.21991,0.02496 q 0,0.218719 0.0903,0.342343 0.0903,0.123624 0.24962,0.123624 0.20921,0 0.3055,-0.187813 v -0.59078 q -0.0987,-0.181869 -0.30312,-0.181869 -0.16166,0 -0.252,0.124812 -0.0903,0.124813 -0.0903,0.369683 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3976" />
|
|
||||||
<path
|
|
||||||
d="m 120.59838,91.758851 q -0.26151,0 -0.42555,-0.171172 -0.16404,-0.17236 -0.16404,-0.460023 v -0.04042 q 0,-0.191379 0.0725,-0.341154 0.0737,-0.150964 0.20445,-0.235361 0.13195,-0.08559 0.28529,-0.08559 0.25081,0 0.38989,0.165228 0.13908,0.165228 0.13908,0.473099 v 0.09153 h -0.87131 q 0.005,0.19019 0.11054,0.307871 0.10699,0.116491 0.27103,0.116491 0.11649,0 0.19732,-0.04755 0.0808,-0.04755 0.14145,-0.126002 l 0.13432,0.104605 q -0.16166,0.248437 -0.48498,0.248437 z m -0.0273,-1.153031 q -0.13313,0 -0.22348,0.09747 -0.0903,0.09628 -0.11173,0.271021 h 0.64427 v -0.01664 q -0.01,-0.167606 -0.0903,-0.259135 -0.0808,-0.09272 -0.21872,-0.09272 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3978" />
|
|
||||||
<path
|
|
||||||
d="m 122.43966,91.106259 q 0,0.293607 -0.13432,0.473099 -0.13432,0.179493 -0.36374,0.179493 -0.23417,0 -0.36849,-0.148587 v 0.619309 h -0.21991 v -1.78066 h 0.20089 l 0.0107,0.142643 q 0.13432,-0.166417 0.37325,-0.166417 0.23179,0 0.36611,0.174738 0.13551,0.174737 0.13551,0.486174 z m -0.2199,-0.02496 q 0,-0.217531 -0.0927,-0.343532 -0.0927,-0.126001 -0.25438,-0.126001 -0.1997,0 -0.29955,0.177115 v 0.614553 q 0.0987,0.175926 0.30193,0.175926 0.15809,0 0.25081,-0.124812 0.0939,-0.126002 0.0939,-0.373249 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3980" />
|
|
||||||
<path
|
|
||||||
d="m 123.01618,90.137476 v 0.311437 h 0.24011 v 0.169983 h -0.24011 v 0.797612 q 0,0.07727 0.0321,0.116491 0.0321,0.03804 0.10936,0.03804 0.038,0 0.10461,-0.01426 v 0.178304 q -0.0868,0.02377 -0.1688,0.02377 -0.1474,0 -0.22228,-0.08915 -0.0749,-0.08915 -0.0749,-0.253191 v -0.797612 h -0.23417 v -0.169983 h 0.23417 v -0.311437 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3982" />
|
|
||||||
<path
|
|
||||||
d="m 123.73177,90.604631 q 0.14621,-0.179492 0.38038,-0.179492 0.40772,0 0.41129,0.460024 v 0.849914 h -0.21991 v -0.851103 q -0.001,-0.139077 -0.0642,-0.205644 -0.0618,-0.06657 -0.19376,-0.06657 -0.10698,0 -0.18781,0.05706 -0.0808,0.05706 -0.126,0.149775 v 0.916481 h -0.21991 v -1.82583 h 0.21991 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3984" />
|
|
||||||
<path
|
|
||||||
d="m 125.97245,91.579358 q 0.11769,0 0.20565,-0.07132 0.088,-0.07132 0.0975,-0.178304 h 0.20802 q -0.006,0.110548 -0.0761,0.210399 -0.0701,0.09985 -0.18782,0.159284 -0.11649,0.05944 -0.24725,0.05944 -0.2627,0 -0.41841,-0.174738 -0.15453,-0.175926 -0.15453,-0.480231 v -0.03685 q 0,-0.187813 0.0689,-0.334022 0.0689,-0.146209 0.19732,-0.22704 0.12957,-0.08083 0.3055,-0.08083 0.21634,0 0.35898,0.129567 0.14383,0.129568 0.15334,0.3364 h -0.20802 q -0.01,-0.124813 -0.0951,-0.204455 -0.0844,-0.08083 -0.20921,-0.08083 -0.16761,0 -0.26033,0.121247 -0.0915,0.120057 -0.0915,0.348286 v 0.0416 q 0,0.222286 0.0915,0.342344 0.0915,0.120057 0.26151,0.120057 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3986" />
|
|
||||||
<path
|
|
||||||
d="m 126.67259,91.080108 q 0,-0.189002 0.0737,-0.339966 0.0749,-0.150964 0.20683,-0.232983 0.13314,-0.08202 0.30312,-0.08202 0.2627,0 0.42436,0.18187 0.16285,0.18187 0.16285,0.483797 v 0.01545 q 0,0.187813 -0.0725,0.337589 -0.0713,0.148586 -0.20564,0.231794 -0.13313,0.08321 -0.30668,0.08321 -0.26151,0 -0.42436,-0.18187 -0.16167,-0.18187 -0.16167,-0.48142 z m 0.2211,0.02615 q 0,0.213965 0.0987,0.343532 0.0999,0.129567 0.26627,0.129567 0.1676,0 0.26627,-0.130756 0.0987,-0.131944 0.0987,-0.368494 0,-0.211587 -0.10104,-0.342343 -0.0999,-0.131945 -0.26627,-0.131945 -0.16285,0 -0.2627,0.129567 -0.0998,0.129568 -0.0998,0.370872 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3988" />
|
|
||||||
<path
|
|
||||||
d="m 128.91327,91.607887 q -0.12837,0.150964 -0.37681,0.150964 -0.20564,0 -0.31381,-0.118869 -0.10699,-0.120058 -0.10818,-0.35423 v -0.836839 h 0.21991 v 0.830895 q 0,0.292418 0.23774,0.292418 0.252,0 0.33521,-0.187813 v -0.9355 h 0.21991 v 1.286164 h -0.20921 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3990" />
|
|
||||||
<path
|
|
||||||
d="m 130.08176,90.646236 q -0.0499,-0.0083 -0.10817,-0.0083 -0.21634,0 -0.29361,0.184247 v 0.912915 h -0.21991 v -1.286164 h 0.21397 l 0.004,0.148586 q 0.10817,-0.17236 0.30669,-0.17236 0.0642,0 0.0975,0.01664 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3992" />
|
|
||||||
<path
|
|
||||||
d="m 131.03152,91.393922 q 0,-0.08915 -0.0677,-0.137888 -0.0666,-0.04993 -0.23418,-0.08558 -0.16641,-0.03566 -0.26507,-0.08559 -0.0975,-0.04993 -0.14502,-0.118869 -0.0464,-0.06894 -0.0464,-0.16404 0,-0.158096 0.13313,-0.267455 0.13432,-0.10936 0.34234,-0.10936 0.21872,0 0.35423,0.112926 0.1367,0.112925 0.1367,0.288852 h -0.22109 q 0,-0.09034 -0.0773,-0.155719 -0.0761,-0.06538 -0.19257,-0.06538 -0.12005,0 -0.18781,0.0523 -0.0677,0.0523 -0.0677,0.136699 0,0.07964 0.063,0.120058 0.063,0.04042 0.22704,0.07726 0.16522,0.03685 0.26745,0.08796 0.10223,0.05111 0.15097,0.123624 0.0499,0.07132 0.0499,0.174737 0,0.172361 -0.13789,0.276966 -0.13789,0.103416 -0.35779,0.103416 -0.15453,0 -0.2734,-0.05468 -0.11887,-0.05468 -0.18663,-0.152153 -0.0666,-0.09866 -0.0666,-0.212775 h 0.2199 q 0.006,0.110548 0.088,0.175926 0.0832,0.06419 0.21872,0.06419 0.12481,0 0.1997,-0.04993 0.0761,-0.05111 0.0761,-0.135511 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3994" />
|
|
||||||
<path
|
|
||||||
d="m 132.07163,91.758851 q -0.26151,0 -0.42555,-0.171172 -0.16404,-0.17236 -0.16404,-0.460023 v -0.04042 q 0,-0.191379 0.0725,-0.341154 0.0737,-0.150964 0.20445,-0.235361 0.13195,-0.08559 0.28529,-0.08559 0.25081,0 0.38989,0.165228 0.13908,0.165228 0.13908,0.473099 v 0.09153 h -0.87131 q 0.005,0.19019 0.11054,0.307871 0.10699,0.116491 0.27103,0.116491 0.11649,0 0.19732,-0.04755 0.0808,-0.04755 0.14145,-0.126002 l 0.13433,0.104605 q -0.16167,0.248437 -0.48499,0.248437 z m -0.0273,-1.153031 q -0.13313,0 -0.22347,0.09747 -0.0903,0.09628 -0.11174,0.271021 h 0.64427 v -0.01664 q -0.01,-0.167606 -0.0903,-0.259135 -0.0808,-0.09272 -0.21872,-0.09272 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3996" />
|
|
||||||
<path
|
|
||||||
d="m 133.57532,91.393922 q 0,-0.08915 -0.0678,-0.137888 -0.0666,-0.04993 -0.23417,-0.08558 -0.16642,-0.03566 -0.26508,-0.08559 -0.0975,-0.04993 -0.14502,-0.118869 -0.0464,-0.06894 -0.0464,-0.16404 0,-0.158096 0.13313,-0.267455 0.13432,-0.10936 0.34234,-0.10936 0.21872,0 0.35423,0.112926 0.1367,0.112925 0.1367,0.288852 h -0.22109 q 0,-0.09034 -0.0773,-0.155719 -0.0761,-0.06538 -0.19257,-0.06538 -0.12005,0 -0.18781,0.0523 -0.0678,0.0523 -0.0678,0.136699 0,0.07964 0.063,0.120058 0.063,0.04042 0.22704,0.07726 0.16522,0.03685 0.26745,0.08796 0.10223,0.05111 0.15097,0.123624 0.0499,0.07132 0.0499,0.174737 0,0.172361 -0.13789,0.276966 -0.13789,0.103416 -0.35779,0.103416 -0.15453,0 -0.2734,-0.05468 -0.11887,-0.05468 -0.18663,-0.152153 -0.0666,-0.09866 -0.0666,-0.212775 h 0.2199 q 0.006,0.110548 0.088,0.175926 0.0832,0.06419 0.21872,0.06419 0.12481,0 0.1997,-0.04993 0.0761,-0.05111 0.0761,-0.135511 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path3998" />
|
|
||||||
<path
|
|
||||||
d="m 134.08646,91.619774 q 0,-0.05706 0.0333,-0.0951 0.0345,-0.03804 0.10223,-0.03804 0.0677,0 0.10223,0.03804 0.0357,0.03804 0.0357,0.0951 0,0.05468 -0.0357,0.09153 -0.0345,0.03685 -0.10223,0.03685 -0.0678,0 -0.10223,-0.03685 -0.0333,-0.03685 -0.0333,-0.09153 z"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;stroke-width:0.0608608"
|
|
||||||
id="path4000" />
|
|
||||||
<path
|
|
||||||
d="m 136.21303,91.280997 q 0,-0.101039 -0.0713,-0.15453 -0.0713,-0.05468 -0.25676,-0.114114 -0.18544,-0.06062 -0.29361,-0.11887 -0.29479,-0.159284 -0.29479,-0.429117 0,-0.140266 0.0785,-0.249625 0.0796,-0.110549 0.22704,-0.172361 0.14859,-0.06181 0.33284,-0.06181 0.18543,0 0.33045,0.06776 0.14502,0.06657 0.22466,0.189002 0.0808,0.122435 0.0808,0.278154 h -0.35661 q 0,-0.11887 -0.0749,-0.184248 -0.0749,-0.06657 -0.2104,-0.06657 -0.13075,0 -0.20326,0.05587 -0.0725,0.05468 -0.0725,0.145021 0,0.0844 0.0844,0.141454 0.0856,0.05706 0.25082,0.106982 0.3043,0.09153 0.44338,0.22704 0.13908,0.135511 0.13908,0.337588 0,0.224663 -0.16999,0.353042 -0.16998,0.12719 -0.45764,0.12719 -0.1997,0 -0.36374,-0.07251 -0.16404,-0.0737 -0.25082,-0.200889 -0.0856,-0.12719 -0.0856,-0.294796 h 0.35779 q 0,0.286475 0.34235,0.286475 0.12719,0 0.19851,-0.05111 0.0713,-0.0523 0.0713,-0.14502 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path4002" />
|
|
||||||
<path
|
|
||||||
d="m 137.53604,91.735077 q -0.0238,-0.04636 -0.0345,-0.115303 -0.12481,0.139077 -0.32451,0.139077 -0.18901,0 -0.31382,-0.10936 -0.12362,-0.109359 -0.12362,-0.275776 0,-0.204455 0.15096,-0.313815 0.15215,-0.109359 0.43863,-0.110548 h 0.15809 v -0.0737 q 0,-0.08915 -0.0464,-0.142643 -0.0452,-0.05349 -0.14384,-0.05349 -0.0868,0 -0.1367,0.0416 -0.0487,0.0416 -0.0487,0.114115 h -0.34353 q 0,-0.111737 0.0689,-0.206833 0.0689,-0.0951 0.19495,-0.148586 0.126,-0.05468 0.2829,-0.05468 0.23774,0 0.37682,0.120058 0.14027,0.118869 0.14027,0.335211 v 0.557496 q 0.001,0.183059 0.0511,0.276965 v 0.02021 z m -0.2841,-0.238927 q 0.0761,0 0.14027,-0.03328 0.0642,-0.03447 0.0951,-0.09153 v -0.221096 h -0.12837 q -0.25795,0 -0.27459,0.178303 l -0.001,0.02021 q 0,0.06419 0.0452,0.105794 0.0452,0.0416 0.12362,0.0416 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path4004" />
|
|
||||||
<path
|
|
||||||
d="m 138.5607,91.315469 0.23892,-0.866556 h 0.35899 l -0.43388,1.286164 h -0.32807 l -0.43388,-1.286164 h 0.35899 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path4006" />
|
|
||||||
<path
|
|
||||||
d="m 139.88846,91.758851 q -0.2829,0 -0.46121,-0.173549 -0.17711,-0.173549 -0.17711,-0.462401 v -0.03328 q 0,-0.193756 0.0749,-0.345909 0.0749,-0.153341 0.21159,-0.235361 0.13789,-0.08321 0.31382,-0.08321 0.26389,0 0.41485,0.166417 0.15215,0.166417 0.15215,0.47191 v 0.140266 h -0.81901 q 0.0166,0.126001 0.0999,0.202077 0.0844,0.07608 0.21278,0.07608 0.19851,0 0.31025,-0.143832 l 0.16879,0.189002 q -0.0773,0.10936 -0.20921,0.171171 -0.13194,0.06062 -0.29242,0.06062 z m -0.0392,-1.055558 q -0.10223,0 -0.16642,0.06894 -0.063,0.06894 -0.0808,0.197323 h 0.47785 v -0.02734 q -0.002,-0.114115 -0.0618,-0.175927 -0.0594,-0.063 -0.16879,-0.063 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path4008" />
|
|
||||||
<path
|
|
||||||
d="m 142.04237,91.735077 h -0.34353 v -1.324202 l -0.4101,0.12719 v -0.279343 l 0.71678,-0.256757 h 0.0369 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path4010" />
|
|
||||||
<path
|
|
||||||
d="m 143.77073,91.019485 q 0,0.358984 -0.14859,0.549175 -0.14858,0.190191 -0.43506,0.190191 -0.28291,0 -0.43268,-0.186625 -0.14978,-0.186624 -0.15334,-0.534911 v -0.318569 q 0,-0.362551 0.14977,-0.550364 0.15097,-0.187814 0.43388,-0.187814 0.2829,0 0.43268,0.186625 0.14977,0.185436 0.15334,0.533722 z m -0.34353,-0.349476 q 0,-0.215153 -0.0594,-0.312625 -0.0582,-0.09866 -0.18305,-0.09866 -0.12125,0 -0.1795,0.09391 -0.0571,0.09272 -0.0606,0.291229 v 0.420797 q 0,0.211587 0.0571,0.315003 0.0582,0.102228 0.18543,0.102228 0.126,0 0.18187,-0.09866 0.0559,-0.09866 0.0582,-0.301927 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path4012" />
|
|
||||||
<path
|
|
||||||
d="m 143.99896,90.337176 q 0,-0.159285 0.10341,-0.257946 0.10342,-0.09985 0.27103,-0.09985 0.16998,0 0.27339,0.09866 0.10342,0.09747 0.10342,0.265078 v 0.08559 q 0,0.160473 -0.10342,0.257946 -0.10341,0.09747 -0.27102,0.09747 -0.16879,0 -0.2734,-0.09747 -0.10341,-0.09866 -0.10341,-0.265078 z m 0.22823,0.09153 q 0,0.07132 0.0404,0.115303 0.0416,0.04279 0.10817,0.04279 0.0666,0 0.1058,-0.04398 0.0392,-0.04398 0.0392,-0.11768 v -0.08796 q 0,-0.07132 -0.0392,-0.115303 -0.0392,-0.04398 -0.10817,-0.04398 -0.0654,0 -0.1058,0.04398 -0.0404,0.04279 -0.0404,0.120058 z m 0.5991,0.882009 q 0,-0.160473 0.1046,-0.257946 0.10461,-0.09866 0.27102,-0.09866 0.1688,0 0.27221,0.09747 0.10461,0.09628 0.10461,0.266267 v 0.08559 q 0,0.159285 -0.10223,0.257946 -0.10223,0.09747 -0.27221,0.09747 -0.17117,0 -0.27459,-0.09866 -0.10341,-0.09866 -0.10341,-0.261512 z m 0.22823,0.09272 q 0,0.06538 0.0428,0.111737 0.0428,0.04636 0.10698,0.04636 0.14502,0 0.14502,-0.160473 v -0.09034 q 0,-0.07132 -0.0404,-0.114114 -0.0404,-0.04398 -0.10699,-0.04398 -0.0666,0 -0.10698,0.04398 -0.0404,0.04279 -0.0404,0.11768 z m -0.61099,0.202078 -0.16761,-0.09034 0.84516,-1.352731 0.16761,0.09034 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path4014" />
|
|
||||||
<path
|
|
||||||
d="m 146.16475,91.203732 h -0.28528 l -0.0404,-1.19939 h 0.36612 z m -0.14264,0.187813 q 0.0868,0 0.13908,0.05111 0.0535,0.05111 0.0535,0.130756 0,0.07845 -0.0535,0.129567 -0.0523,0.05111 -0.13908,0.05111 -0.0856,0 -0.13908,-0.05111 -0.0523,-0.05111 -0.0523,-0.129567 0,-0.07845 0.0523,-0.129567 0.0535,-0.0523 0.13908,-0.0523 z"
|
|
||||||
style="font-weight:bold"
|
|
||||||
id="path4016" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g896"
|
|
||||||
transform="matrix(0.11457918,0,0,0.11457918,82.917314,84.127369)">
|
|
||||||
<path
|
|
||||||
fill="#fdd888"
|
|
||||||
d="m 33,31 c 0,2.2 -1.8,4 -4,4 H 7 C 4.8,35 3,33.2 3,31 V 14 c 0,-2.2 1.8,-4 4,-4 h 22 c 2.2,0 4,1.8 4,4 z"
|
|
||||||
id="path873" />
|
|
||||||
<path
|
|
||||||
fill="#fdd888"
|
|
||||||
d="m 36,11 c 0,2.2 -1.8,4 -4,4 H 4 C 1.8,15 0,13.2 0,11 0,8.8 1.8,7 4,7 h 28 c 2.2,0 4,1.8 4,4 z"
|
|
||||||
id="path875" />
|
|
||||||
<path
|
|
||||||
fill="#fcab40"
|
|
||||||
d="m 3,15 h 30 v 2 H 3 Z"
|
|
||||||
id="path877" />
|
|
||||||
<path
|
|
||||||
fill="#da2f47"
|
|
||||||
d="m 19,3 h -2 c -1.657,0 -3,1.343 -3,3 v 29 h 8 V 6 C 22,4.344 20.657,3 19,3 Z"
|
|
||||||
id="path879" />
|
|
||||||
<path
|
|
||||||
fill="#da2f47"
|
|
||||||
d="m 16,7 c 1.1,0 1.263,-0.516 0.361,-1.147 L 9.639,1.147 C 8.737,0.516 7.554,0.781 7.008,1.736 L 4.992,5.264 C 4.446,6.219 4.9,7 6,7 Z m 4,0 C 18.9,7 18.737,6.484 19.639,5.853 l 6.723,-4.706 c 0.901,-0.631 2.085,-0.366 2.631,0.589 l 2.016,3.527 C 31.554,6.219 31.1,7 30,7 Z"
|
|
||||||
id="path881" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 34 KiB |
|
|
@ -1,124 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="240"
|
|
||||||
height="100"
|
|
||||||
viewBox="0 0 63.5 26.458334"
|
|
||||||
version="1.1"
|
|
||||||
id="svg975">
|
|
||||||
<defs
|
|
||||||
id="defs969">
|
|
||||||
<clipPath
|
|
||||||
id="clip0">
|
|
||||||
<rect
|
|
||||||
width="770"
|
|
||||||
height="222.03999"
|
|
||||||
fill="#ffffff"
|
|
||||||
id="rect14"
|
|
||||||
x="0"
|
|
||||||
y="0" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
id="clip0-7">
|
|
||||||
<rect
|
|
||||||
width="770"
|
|
||||||
height="222.03999"
|
|
||||||
fill="#ffffff"
|
|
||||||
id="rect14-5"
|
|
||||||
x="0"
|
|
||||||
y="0" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
id="clip0-2">
|
|
||||||
<rect
|
|
||||||
width="770"
|
|
||||||
height="222.03999"
|
|
||||||
fill="#ffffff"
|
|
||||||
id="rect14-0"
|
|
||||||
x="0"
|
|
||||||
y="0" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
id="clip0-3">
|
|
||||||
<rect
|
|
||||||
width="770"
|
|
||||||
height="222.03999"
|
|
||||||
fill="#ffffff"
|
|
||||||
id="rect14-6"
|
|
||||||
x="0"
|
|
||||||
y="0" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<metadata
|
|
||||||
id="metadata972">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<rect
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.342717;stop-color:#000000"
|
|
||||||
id="rect945"
|
|
||||||
width="63.5"
|
|
||||||
height="26.458334"
|
|
||||||
x="4e-07"
|
|
||||||
y="-1.5122477e-07" />
|
|
||||||
<g
|
|
||||||
id="g967"
|
|
||||||
transform="translate(0.17210258,1.0583333)">
|
|
||||||
<g
|
|
||||||
id="g955"
|
|
||||||
transform="matrix(0.65843845,0,0,0.65843845,0.27576501,1.1223943)">
|
|
||||||
<path
|
|
||||||
d="m 8.9812622,1.4378695 1.2474488,0.00173 7.302287,7.3022874 0.0018,1.3131081 -7.022162,7.02216 -1.7676605,-0.0025 -6.8470822,-6.847084 -0.00242,-1.7019627 7.0877822,-7.087706 z"
|
|
||||||
fill="#d10622"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
id="path884"
|
|
||||||
style="fill:#b3b3b3;stroke-width:0.0383523" />
|
|
||||||
<path
|
|
||||||
d="m 12.889758,1.4378695 1.24741,0.00173 7.302287,7.3022874 0.0018,1.3131081 -7.022161,7.022161 -1.76766,-0.0025 -6.8470835,-6.847084 -0.00242,-1.7019627 7.0878215,-7.0877058 z"
|
|
||||||
fill="#f7c0bd"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
id="path895"
|
|
||||||
style="stroke-width:0.0383523" />
|
|
||||||
<path
|
|
||||||
d="m 16.798207,1.4378695 1.24741,0.00173 7.302287,7.3022874 0.0018,1.3131081 -7.022162,7.022161 -1.767659,-0.0025 -6.8470836,-6.847084 -0.0024,-1.7019626 7.0877436,-7.0877059 z"
|
|
||||||
fill="#0000ff"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
opacity="0.8"
|
|
||||||
id="path906"
|
|
||||||
style="stroke-width:0.0383523" />
|
|
||||||
<path
|
|
||||||
d="m 20.706643,1.4378692 1.247449,0.00173 7.302287,7.3022875 0.0018,1.3131083 -7.022161,7.022161 -1.76766,-0.0025 -6.847122,-6.847084 -0.0024,-1.7019627 7.087783,-7.0877061 z"
|
|
||||||
fill="#ffffff"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
id="path917"
|
|
||||||
style="fill:#e7285d;fill-opacity:1;stroke-width:0.0383523" />
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
d="m 58.713926,8.3808577 h 2.793359 l 0.084,-0.04316 c 0.04201,-0.09527 0.04201,-0.173052 0.04201,-0.216608 0,-1.168599 -0.713215,-2.120589 -1.761649,-2.120589 -1.21643,0 -2.013255,0.95199 -2.013255,2.129144 0,1.2036 0.964434,2.1991443 2.180866,2.1991443 0.629215,0 1.216431,-0.30333 1.594038,-0.5627163 v -0.909211 l -0.04201,-0.04277 c -0.419605,0.432439 -1.006435,0.692215 -1.55165,0.692215 -0.713214,0 -1.216431,-0.475995 -1.325708,-1.125434 z m -0.04667,-0.757546 c 0.124051,-0.446441 0.565828,-0.811214 1.184543,-0.811214 0.486495,0 0.928657,0.364773 0.972601,0.811214 z M 56.35728,10.328782 c 0.279997,0 0.726048,-0.118994 0.962101,-0.255108 V 9.2671277 l -0.04394,-0.04277 a 1.5905386,1.5905386 0 0 1 -0.917766,0.297497 c -0.349996,0 -0.656048,-0.254721 -0.656048,-0.934492 v -1.529096 h 1.573817 l 0.04395,-0.04277 v -0.764547 l -0.04395,-0.04239 h -1.57382 v -0.968322 l -0.04395,-0.05094 h -0.787101 l -0.04355,0.04239 v 0.976879 h -0.6996 l -0.04355,0.04277 v 0.764158 l 0.04355,0.04239 h 0.699603 v 1.571483 c 0,1.104433 0.56855,1.6994263 1.530262,1.6994263 z m -3.580069,-4.8696143 0.497773,-0.499328 v -0.08322 l -0.497773,-0.49933 h -0.08284 l -0.497769,0.499329 v 0.08322 l 0.497772,0.499328 z m -1.431873,4.7443943 0.04201,-0.04201 V 6.3127617 l -0.04201,-0.04201 h -0.754437 l -0.04238,0.04201 v 2.802693 c -0.20961,0.25122 -0.629216,0.418441 -1.048434,0.418441 -0.713214,0 -1.048434,-0.418441 -1.048434,-1.087712 v -2.133422 l -0.04201,-0.04201 h -0.755214 l -0.04161,0.04201 v 2.175421 c 0,1.128934 0.628827,1.8405923 1.887258,1.8405923 0.377218,0 0.754825,-0.125609 1.048433,-0.292829 v 0.125608 l 0.04201,0.04161 h 0.754826 z M 44.157191,5.6403867 c 0,-0.373329 0.24772,-0.62377 0.581384,-0.636603 v -0.0016 c 0.510994,0.0062 0.811992,0.302941 1.069433,0.851657 h 0.08711 l 0.6506,-0.511772 c -0.303326,-0.766816 -0.953931,-1.235422 -1.82231,-1.235422 v 0.0023 c -0.88238,0.02061 -1.564093,0.692603 -1.564093,1.532206 0,1.661704 2.646748,1.576538 2.646748,2.769636 0,0.59655 -0.390828,1.022768 -0.954711,1.022768 -0.520717,0 -0.824437,-0.298274 -1.08499,-0.852047 h -0.08672 l -0.650608,0.511383 c 0.30333,0.76727 0.954712,1.2358763 1.822315,1.2358763 1.084987,0 1.9522,-0.8524353 1.9522,-1.9179803 0,-1.959979 -2.646361,-1.917589 -2.646361,-2.770025 z m -2.629248,4.6883953 c 0.279218,0 0.725268,-0.118994 0.961323,-0.255108 V 9.2671277 l -0.04355,-0.04277 c -0.34105,0.237998 -0.673548,0.297497 -0.917766,0.297497 -0.349997,0 -0.656438,-0.254721 -0.656438,-0.934492 v -1.529096 h 1.574204 l 0.04355,-0.04277 v -0.764547 l -0.04355,-0.04239 h -1.574212 v -0.968322 l -0.04355,-0.05094 H 40.04085 l -0.04355,0.04239 v 0.976879 h -0.699992 l -0.04355,0.04277 v 0.764158 l 0.04395,0.04239 h 0.699603 v 1.571483 c 0,1.104433 0.568162,1.6994263 1.530263,1.6994263 z M 38.984247,9.0299077 c 0,-1.68776 -2.341086,-1.081878 -2.341086,-1.817648 0,-0.21622 0.21272,-0.389662 0.595772,-0.389662 0.425439,0 0.851268,0.129886 1.277097,0.432828 l 0.04277,-0.04317 v -0.865656 c -0.340654,-0.216609 -0.808872,-0.346108 -1.319865,-0.346108 -0.894436,0 -1.490206,0.51916 -1.490206,1.211764 0,1.601427 2.341085,0.995546 2.341085,1.818037 0,0.302942 -0.169943,0.475995 -0.766103,0.475995 -0.467828,0 -1.0216,-0.173053 -1.489428,-0.475995 l -0.04277,0.04317 v 0.865658 c 0.383051,0.2597753 1.0216,0.3896613 1.532206,0.3896613 1.021989,0 1.660538,-0.5627163 1.660538,-1.2988743 z m -5.63805,1.2988743 c 0.629603,0 1.216431,-0.30333 1.594038,-0.5627163 v -0.909212 l -0.04199,-0.04277 c -0.419218,0.432439 -1.006434,0.692214 -1.551651,0.692214 -0.713213,0 -1.216431,-0.475995 -1.325319,-1.125433 h 2.79297 l 0.084,-0.04316 c 0.04199,-0.09527 0.04199,-0.173053 0.04199,-0.216609 0,-1.168599 -0.712826,-2.120589 -1.761649,-2.120589 -1.21643,0 -2.013257,0.95199 -2.013257,2.129145 0,1.203599 0.964825,2.1991443 2.180868,2.1991443 z M 31.974211,7.6233117 c 0.124051,-0.446441 0.565828,-0.811214 1.184543,-0.811214 0.486106,0 0.928268,0.364773 0.972601,0.811214 z m -2.409531,2.6623043 1.600649,-3.9716793 -0.04201,-0.04277 h -0.842714 l -0.04201,0.04277 -1.095488,2.849359 -1.0951,-2.849359 -0.04238,-0.04277 h -0.842324 l -0.04238,0.04277 1.601039,3.9720693 0.04201,0.04316 h 0.758316 l 0.04238,-0.04277 z m -2.755247,-0.227498 0.04201,-0.04161 V 7.8410877 c 0,-1.129711 -0.629216,-1.840592 -1.887258,-1.840592 -0.377608,0 -0.755215,0.125227 -1.048824,0.29283 v -0.125609 l -0.04161,-0.04201 h -0.755599 l -0.04201,0.04201 v 3.8487913 l 0.04201,0.04161 h 0.755214 l 0.04161,-0.04161 V 7.2134267 c 0.209997,-0.250831 0.629215,-0.418051 1.048821,-0.418051 0.712825,0 1.048433,0.418051 1.048433,1.087322 v 2.1338103 l 0.04201,0.04161 h 0.754824 z M 21.996206,5.9604407 h 0.0019 v -1.583151 h -0.115497 l -0.693381,0.730715 v 5.1780013 l 0.04044,0.04277 h 0.725659 l 0.04045,-0.04277 V 5.9608287 Z m 30.510342,0.310329 -0.04045,0.04317 v 3.9720693 l 0.04045,0.04316 h 0.727992 l 0.04045,-0.04277 V 6.3135577 l -0.04045,-0.04277 h -0.728382 z"
|
|
||||||
fill="#f7c0bd"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
id="path934"
|
|
||||||
style="fill:#e7285d;fill-opacity:1;stroke-width:0.388885" />
|
|
||||||
</g>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333px;line-height:1.25;font-family:Roboto, sans-serif;-inkscape-font-specification:'Roboto, sans-serif';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;stroke-width:0.116313"
|
|
||||||
x="31.874025"
|
|
||||||
y="21.589594"
|
|
||||||
id="text959"><tspan
|
|
||||||
id="tspan957"
|
|
||||||
x="31.874023"
|
|
||||||
y="21.589594"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333px;font-family:Roboto, sans-serif;-inkscape-font-specification:'Roboto, sans-serif';text-align:center;text-anchor:middle;stroke-width:0.116313">Wealthtech jobs with FastAPI</tspan></text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
|
@ -1,118 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="240"
|
|
||||||
height="100"
|
|
||||||
viewBox="0 0 63.5 26.458334"
|
|
||||||
version="1.1"
|
|
||||||
id="svg975">
|
|
||||||
<defs
|
|
||||||
id="defs969">
|
|
||||||
<clipPath
|
|
||||||
id="clip0">
|
|
||||||
<rect
|
|
||||||
width="770"
|
|
||||||
height="222.03999"
|
|
||||||
fill="#ffffff"
|
|
||||||
id="rect14"
|
|
||||||
x="0"
|
|
||||||
y="0" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
id="clip0-7">
|
|
||||||
<rect
|
|
||||||
width="770"
|
|
||||||
height="222.03999"
|
|
||||||
fill="#ffffff"
|
|
||||||
id="rect14-5"
|
|
||||||
x="0"
|
|
||||||
y="0" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<metadata
|
|
||||||
id="metadata972">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-75.455132,-135.96141)">
|
|
||||||
<rect
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.364576;stop-color:#000000"
|
|
||||||
id="rect901"
|
|
||||||
width="63.5"
|
|
||||||
height="26.458334"
|
|
||||||
x="75.455132"
|
|
||||||
y="135.96141" />
|
|
||||||
<rect
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.404023;stop-color:#000000"
|
|
||||||
id="rect1758"
|
|
||||||
width="60.854168"
|
|
||||||
height="23.8125"
|
|
||||||
x="76.778053"
|
|
||||||
y="137.28433" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:4.23333px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;stroke-width:0.133861"
|
|
||||||
x="107.22684"
|
|
||||||
y="154.45795"
|
|
||||||
id="text1712"><tspan
|
|
||||||
x="107.22684"
|
|
||||||
y="154.45795"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333px;font-family:Roboto;-inkscape-font-specification:Roboto;text-align:center;text-anchor:middle;fill:#000000;stroke-width:0.133861"
|
|
||||||
id="tspan2004">We deal in big ideas. You in?</tspan><tspan
|
|
||||||
x="107.22684"
|
|
||||||
y="159.79095"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333px;font-family:Roboto;-inkscape-font-specification:Roboto;text-align:center;text-anchor:middle;fill:#000000;stroke-width:0.133861"
|
|
||||||
id="tspan874" /></text>
|
|
||||||
<g
|
|
||||||
id="g1003"
|
|
||||||
transform="matrix(0.02061785,0,0,0.02061785,70.802235,134.33866)">
|
|
||||||
<g
|
|
||||||
id="g1015"
|
|
||||||
transform="translate(4.0510205)">
|
|
||||||
<path
|
|
||||||
d="m 900.5,226.1 c -21.9,23 -28.6,35.3 -44.3,70.1 L 710.9,630 608.2,396.1 506.6,630 360.2,296.2 c -15.2,-34.8 -21.9,-46 -44.3,-70.1 h 185.7 c -14,11.8 -21.3,26.9 -21.3,46 0,13.4 2.3,23 9.5,40.4 l 55.5,132.4 41,-98.7 -23,-53.3 c -13.5,-30.8 -23,-49.9 -36.5,-66.7 h 185.7 c -18,11.8 -26.9,26.9 -26.9,46 0,13.4 2.3,23 9.5,40.4 L 751.8,445 807.9,312.6 c 7.3,-17.4 10.1,-26.9 10.1,-39.8 0,-16.8 -11.8,-37 -25.3,-46.5 z"
|
|
||||||
id="path966" />
|
|
||||||
<path
|
|
||||||
d="m 1093.5,523.4 c -18,78 -61.2,103.8 -124,103.8 -71.8,0 -139.2,-52.2 -139.2,-150.3 0,-94.2 59.5,-161 137.5,-161 51.1,0 122.9,26.4 122.9,137.4 H 921.3 c 11.8,59.4 50.5,90.3 104.4,90.3 27.4,0 46.5,-5 67.8,-20.2 z M 917.9,414.6 v 7.3 H 992 c 0,-56.1 -12.4,-73.5 -35.9,-73.5 -24.2,0 -38.2,24.1 -38.2,66.2 z"
|
|
||||||
id="path968" />
|
|
||||||
<path
|
|
||||||
d="M 1433.6,327.6 C 1387,283.3 1341,260.3 1305.7,260.3 v 247.4 c 0,49.9 12.9,80.2 47.1,111.1 h -212.1 c 34.2,-30.8 47.1,-61.1 47.1,-111.1 V 260.3 c -35.4,0 -83.6,23 -127.9,67.3 l 22.5,-118.4 c 15.2,11.8 43.2,16.8 74.6,16.8 h 179.6 c 31.4,0 59.5,-5.1 74.6,-16.8 z"
|
|
||||||
id="path970" />
|
|
||||||
<path
|
|
||||||
d="m 1562.6,618.8 h -178.4 c 21.9,-25.8 31.4,-42.6 31.4,-83.6 v -90.9 c 0,-32 -4.5,-47.1 -25.8,-66.7 l -12.3,-11.2 134.7,-50.5 V 386 c 12.9,-40.4 33.1,-70.1 66.2,-70.1 26.9,0 44.3,19.1 44.3,47.1 0,29.7 -18.5,50.5 -44.3,50.5 -5.6,-12.9 -19.1,-19.1 -31.4,-19.1 -10.1,0 -19.6,2.8 -24.7,7.9 v 129.6 c -0.1,43.1 7.2,54.9 40.3,86.9 z"
|
|
||||||
id="path972" />
|
|
||||||
<path
|
|
||||||
d="m 1931.3,567.2 c -6.7,35.3 -35.9,60 -75.8,60 -33.7,0 -59.5,-20.2 -67.3,-43.2 -16.8,30.3 -48.3,43.2 -81.9,43.2 -46.6,0 -78,-30.8 -78,-72.9 0,-49.4 35.4,-83.6 116.7,-103.2 l 37.6,-9 v -50.5 c 0,-31.4 -10.7,-44.3 -29.7,-44.3 -17.9,0 -29.7,11.8 -29.7,28 0,14 6.7,23 18.5,34.2 0,17.9 -28.1,36.4 -57.2,36.4 -28.6,0 -48.8,-21.9 -48.8,-48.8 0,-47.7 52.7,-81.3 130.2,-81.3 80.8,0 123.4,34.2 123.4,110.5 v 120.6 c 0,15.1 9,25.2 23,25.2 8.3,0.1 14.5,-1.6 19,-4.9 z m -148.7,-8.4 v -90.3 l -7.3,2.2 c -24.1,7.3 -40.4,23 -40.4,54.4 0,26.9 10.7,43.2 28.1,43.2 8.9,0 16.2,-2.8 19.6,-9.5 z"
|
|
||||||
id="path974" />
|
|
||||||
<path
|
|
||||||
d="m 2083.3,618.8 h -150.9 c 15.7,-15.1 25.8,-37.6 25.8,-83.6 v -90.9 c 0,-30.8 -3.9,-46.5 -22.4,-63.9 l -12.9,-11.8 139.2,-52.7 v 43.7 c 16.3,-26.9 50.5,-43.7 84.7,-43.7 53.9,0 83.6,29.2 83.6,81.3 v 157.1 c 0,32 8.4,51.6 21.9,64.5 h -147 c 14.6,-14 18.5,-33.1 18.5,-48.2 V 410.1 c 0,-23.5 -7.8,-35.3 -29.7,-35.3 -12.3,0 -22.5,4.5 -29.2,11.2 v 184.5 c -0.1,15.2 3.8,33.7 18.4,48.3 z"
|
|
||||||
id="path976" />
|
|
||||||
<path
|
|
||||||
d="m 2279.7,602.5 v -95.9 c 40.4,52.7 82.5,81.9 121.8,81.9 19.1,0 28.1,-10.1 28.1,-24.1 0,-13.5 -7.3,-21.3 -20.2,-26.4 l -65.1,-24.1 c -49.9,-18.5 -72.4,-49.4 -72.4,-95.3 0,-59.4 48.8,-102.6 118.4,-102.6 37.6,0 79.1,10.6 99.9,25.8 v 81.9 c -25.8,-45.4 -67.9,-70.7 -108.9,-70.7 -20.8,0 -31.4,6.7 -31.4,19.6 0,12.9 7.9,17.9 25.2,25.2 l 76.9,31.4 c 38.7,15.7 56.1,48.8 56.1,88.6 0,65.6 -47.7,109.4 -119.5,109.4 -36,0 -73,-7.9 -108.9,-24.7 z"
|
|
||||||
id="path978" />
|
|
||||||
<path
|
|
||||||
d="m 2519.3,352.3 32.5,-49.9 c 41.5,-62.8 74.7,-93.1 130.8,-93.1 43.8,0 71.8,19.1 71.8,47.7 0,24.7 -20.2,42.6 -62.9,42.6 0,-34.8 -19.6,-47.7 -39.3,-47.7 -19.6,0 -34.8,14 -34.8,37 0,15.7 8.4,31.4 34.2,35.3 h 62.3 l -22.5,28 h -38.2 v 166.6 c 0,31.4 5.1,59.4 37.6,88.1 l 13.5,11.8 h -193.6 c 22.5,-22.4 35.9,-49.9 35.9,-90.3 V 352.3 Z"
|
|
||||||
id="path980" />
|
|
||||||
<path
|
|
||||||
d="m 2959.2,523.4 c -18,78 -61.2,103.8 -124,103.8 -71.8,0 -139.2,-52.2 -139.2,-150.3 0,-94.2 59.5,-161 137.5,-161 51.1,0 122.9,26.4 122.9,137.4 H 2787 c 11.8,59.4 50.5,90.3 104.4,90.3 27.4,0 46.5,-5 67.8,-20.2 z M 2783.6,414.6 v 7.3 h 74.1 c 0,-56.1 -12.4,-73.5 -35.9,-73.5 -24.2,0 -38.2,24.1 -38.2,66.2 z"
|
|
||||||
id="path982" />
|
|
||||||
<path
|
|
||||||
d="m 3147.2,618.8 h -178.4 c 21.9,-25.8 31.4,-42.6 31.4,-83.6 v -90.9 c 0,-32 -4.5,-47.1 -25.8,-66.7 L 2962,366.4 3096.7,315.9 V 386 c 12.9,-40.4 33.1,-70.1 66.2,-70.1 26.9,0 44.3,19.1 44.3,47.1 0,29.7 -18.5,50.5 -44.3,50.5 -5.6,-12.9 -19.1,-19.1 -31.4,-19.1 -10.1,0 -19.6,2.8 -24.7,7.9 v 129.6 c 0,43.1 7.3,54.9 40.4,86.9 z"
|
|
||||||
id="path984" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
|
@ -1,43 +0,0 @@
|
||||||
<mxfile host="65bd71144e" modified="2020-11-28T18:13:19.199Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.51.1 Chrome/83.0.4103.122 Electron/9.3.3 Safari/537.36" etag="KPHuXUeExV3PdWouu_3U" version="13.6.5">
|
|
||||||
<diagram id="zB4-QXJZ7ScUzHSLnJ1i" name="Page-1">
|
|
||||||
<mxGraphModel dx="1154" dy="780" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1920" pageHeight="1200" math="0" shadow="0" extFonts="Roboto^https://fonts.googleapis.com/css?family=Roboto|Roboto Mono, mono^https://fonts.googleapis.com/css?family=Roboto+Mono%2C+mono">
|
|
||||||
<root>
|
|
||||||
<mxCell id="0"/>
|
|
||||||
<mxCell id="1" parent="0"/>
|
|
||||||
<mxCell id="2" value="" style="rounded=0;whiteSpace=wrap;html=1;fontStyle=1;strokeWidth=4;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="110" y="280" width="1350" height="620" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="3" value="<font style="font-size: 24px" face="Roboto">Package app<br>app/__init__.py</font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;strokeWidth=3;fontFamily=Roboto Mono, mono;FType=g;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="635" y="310" width="300" height="80" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="15" value="<span style="font-family: &#34;roboto&#34; ; font-size: 24px">Module app.main</span><br style="font-family: &#34;roboto&#34; ; font-size: 24px"><span style="font-family: &#34;roboto&#34; ; font-size: 24px">app/main.py</span>" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;strokeWidth=3;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="140" y="430" width="360" height="100" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="16" value="<span style="font-family: &#34;roboto&#34; ; font-size: 24px">Module app.dependencies</span><br style="font-family: &#34;roboto&#34; ; font-size: 24px"><span style="font-family: &#34;roboto&#34; ; font-size: 24px">app/dependencies.py</span>" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;strokeWidth=3;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="130" y="565" width="370" height="100" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="5" value="" style="rounded=0;whiteSpace=wrap;html=1;fontStyle=1;strokeWidth=4;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="1030" y="430" width="400" height="260" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="8" value="<font style="font-size: 24px" face="Roboto">Subpackage app.internal<br></font><span style="font-family: &#34;roboto&#34; ; font-size: 24px">app/internal/__init__.py</span><font style="font-size: 24px" face="Roboto"><br></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;strokeWidth=3;fontFamily=Roboto Mono, mono;FType=g;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="1083.8438461538462" y="460" width="292.3076923076923" height="80" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="19" value="<span style="font-family: &#34;roboto&#34; ; font-size: 24px">Module app.internal.admin</span><br style="font-family: &#34;roboto&#34; ; font-size: 24px"><span style="font-family: &#34;roboto&#34; ; font-size: 24px">app/internal/admin.py</span>" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;strokeWidth=3;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="1050" y="570" width="360" height="100" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="4" value="" style="rounded=0;whiteSpace=wrap;html=1;fontStyle=1;strokeWidth=4;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="540" y="430" width="440" height="410" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="7" value="<font style="font-size: 24px" face="Roboto">Subpackage app.routers<br>app/routers/__init__.py<br></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;strokeWidth=3;fontFamily=Roboto Mono, mono;FType=g;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="599.2307692307693" y="460" width="321.53846153846155" height="80" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="17" value="<span style="font-family: &#34;roboto&#34; ; font-size: 24px">Module app.routers.items</span><br style="font-family: &#34;roboto&#34; ; font-size: 24px"><span style="font-family: &#34;roboto&#34; ; font-size: 24px">app/routers/items.py</span>" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;strokeWidth=3;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="580" y="570" width="360" height="100" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="18" value="<span style="font-family: &#34;roboto&#34; ; font-size: 24px">Module app.routers.users</span><br style="font-family: &#34;roboto&#34; ; font-size: 24px"><span style="font-family: &#34;roboto&#34; ; font-size: 24px">app/routers/users.py</span>" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;strokeWidth=3;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="580" y="700" width="360" height="100" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
</root>
|
|
||||||
</mxGraphModel>
|
|
||||||
</diagram>
|
|
||||||
</mxfile>
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 87 KiB |