From faf88cea0b5c1b0f6ff1f00306fae6e00f877f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 19 Mar 2020 14:39:29 +0100 Subject: [PATCH 001/153] :art: Update terminal examples and Typer note (#1139) * :art: Update terminal examples with Termynal * :bento: Add Termynal scripts and styles from Typer for terminal examples --- README.md | 42 +++- docs/advanced/extending-openapi.md | 11 +- docs/advanced/sql-databases-peewee.md | 10 +- docs/advanced/sub-applications-proxy.md | 10 +- docs/advanced/templates.md | 20 +- docs/advanced/websockets.md | 10 +- docs/contributing.md | 3 +- docs/css/termynal.css | 108 ++++++++++ docs/deployment.md | 58 ++++-- docs/index.md | 42 +++- docs/js/custom.js | 110 ++++++++++ docs/js/termynal.js | 264 ++++++++++++++++++++++++ docs/tutorial/bigger-applications.md | 10 +- docs/tutorial/debugging.md | 16 +- docs/tutorial/first-steps.md | 43 ++-- docs/tutorial/index.md | 24 ++- docs/tutorial/security/first-steps.md | 10 +- docs/tutorial/security/oauth2-jwt.md | 30 ++- docs/tutorial/sql-databases.md | 15 +- docs/tutorial/static-files.md | 10 +- docs/tutorial/testing.md | 30 ++- mkdocs.yml | 2 + 22 files changed, 801 insertions(+), 77 deletions(-) create mode 100644 docs/css/termynal.css create mode 100644 docs/js/termynal.js diff --git a/README.md b/README.md index 8801c1af57b63..11c9453720dc7 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,14 @@ The key features are: --- +## **Typer**, the FastAPI of CLIs + + + +If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. + +**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 + ## Requirements Python 3.6+ @@ -88,16 +96,28 @@ FastAPI stands on the shoulders of giants: ## Installation -```bash -pip install fastapi +
+ +```console +$ pip install fastapi + +---> 100% ``` +
+ You will also need an ASGI server, for production such as Uvicorn or Hypercorn. -```bash -pip install uvicorn +
+ +```console +$ pip install uvicorn + +---> 100% ``` +
+ ## Example ### Create it @@ -151,10 +171,20 @@ If you don't know, check the _"In a hurry?"_ section about + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. ``` + +
About the command uvicorn main:app --reload... diff --git a/docs/advanced/extending-openapi.md b/docs/advanced/extending-openapi.md index 91fb29db87466..4841c92134d94 100644 --- a/docs/advanced/extending-openapi.md +++ b/docs/advanced/extending-openapi.md @@ -155,10 +155,17 @@ After that, your file structure could look like: Now you need to install `aiofiles`: -```bash -pip install aiofiles + +
+ +```console +$ pip install aiofiles + +---> 100% ``` +
+ ### Serve the static files * Import `StaticFiles`. diff --git a/docs/advanced/sql-databases-peewee.md b/docs/advanced/sql-databases-peewee.md index 4c05daf7710f8..ae957bec640c3 100644 --- a/docs/advanced/sql-databases-peewee.md +++ b/docs/advanced/sql-databases-peewee.md @@ -369,10 +369,16 @@ async def reset_db_state(): Then run your app with Uvicorn: -```bash -uvicorn sql_app.main:app --reload +
+ +```console +$ uvicorn sql_app.main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` +
+ Open your browser at
http://127.0.0.1:8000/docs and create a couple of users. Then open 10 tabs at http://127.0.0.1:8000/docs#/default/read_slow_users_slowusers__get at the same time. diff --git a/docs/advanced/sub-applications-proxy.md b/docs/advanced/sub-applications-proxy.md index e75a4e9d3aaf8..333ef9ae2de76 100644 --- a/docs/advanced/sub-applications-proxy.md +++ b/docs/advanced/sub-applications-proxy.md @@ -73,10 +73,16 @@ Here you need to make sure you use the same path that you used for the `openapi_ Now, run `uvicorn`, if your file is at `main.py`, it would be: -```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) ``` +
+ And open the docs at http://127.0.0.1:8000/docs. You will see the automatic API docs for the main app, including only its own paths: diff --git a/docs/advanced/templates.md b/docs/advanced/templates.md index 9b8f750b5923c..da4752e4a598d 100644 --- a/docs/advanced/templates.md +++ b/docs/advanced/templates.md @@ -8,16 +8,28 @@ There are utilities to configure it easily that you can use directly in your **F Install `jinja2`: -```bash -pip install jinja2 +
+ +```console +$ pip install jinja2 + +---> 100% ``` +
+ If you need to also serve static files (as in this example), install `aiofiles`: -```bash -pip install aiofiles +
+ +```console +$ pip install aiofiles + +---> 100% ``` +
+ ## Using `Jinja2Templates` * Import `Jinja2Templates`. diff --git a/docs/advanced/websockets.md b/docs/advanced/websockets.md index ac6d93864ff3c..a76eab59c815e 100644 --- a/docs/advanced/websockets.md +++ b/docs/advanced/websockets.md @@ -85,10 +85,16 @@ To learn more about the options, check Starlette's documentation for: If your file is named `main.py`, run your application with: -```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) ``` +
+ Open your browser at http://127.0.0.1:8000. You will see a simple page like: diff --git a/docs/contributing.md b/docs/contributing.md index 9cf663605bc85..4ca50db9e831a 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -51,6 +51,7 @@ $ Get-Command pip some/directory/fastapi/env/bin/pip ``` + !!! tip Every time you install a new package with `pip` under that environment, activate the environment again. @@ -60,7 +61,7 @@ some/directory/fastapi/env/bin/pip **FastAPI** uses Flit to build, package and publish the project. -After activating the environment as described above, install `flit`: +After activating the environment as described above, install `flit`: ```console $ pip install flit diff --git a/docs/css/termynal.css b/docs/css/termynal.css new file mode 100644 index 0000000000000..0484e65d4d8eb --- /dev/null +++ b/docs/css/termynal.css @@ -0,0 +1,108 @@ +/** + * termynal.js + * + * @author Ines Montani + * @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; + } +} diff --git a/docs/deployment.md b/docs/deployment.md index 4973bc21569fe..8d4c6fcdba464 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -188,18 +188,28 @@ def read_item(item_id: int, q: str = None): * Go to the project directory (in where your `Dockerfile` is, containing your `app` directory). * Build your FastAPI image: -```bash -docker build -t myimage . +
+ +```console +$ docker build -t myimage . + +---> 100% ``` +
+ ### Start the Docker container * Run a container based on your image: -```bash -docker run -d --name mycontainer -p 80:80 myimage +
+ +```console +$ 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 @@ -319,30 +329,54 @@ You just need to install an ASGI compatible server like: * Uvicorn, a lightning-fast ASGI server, built on uvloop and httptools. -```bash -pip install uvicorn +
+ +```console +$ pip install uvicorn + +---> 100% ``` +
+ * Hypercorn, an ASGI server also compatible with HTTP/2. -```bash -pip install hypercorn +
+ +```console +$ pip install hypercorn + +---> 100% ``` +
+ ...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 +
+ +```console +$ uvicorn main:app --host 0.0.0.0 --port 80 + +INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) ``` +
+ or with Hypercorn: -```bash -hypercorn main:app --bind 0.0.0.0:80 +
+ +```console +$ hypercorn main:app --bind 0.0.0.0:80 + +Running on 0.0.0.0:8080 over http (CTRL + C to quit) ``` +
+ You might want to set up some tooling to make sure it is restarted automatically if it stops. You might also want to install Gunicorn and use it as a manager for Uvicorn, or use Hypercorn with multiple workers. diff --git a/docs/index.md b/docs/index.md index 8801c1af57b63..11c9453720dc7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -77,6 +77,14 @@ The key features are: --- +## **Typer**, the FastAPI of CLIs + + + +If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. + +**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 + ## Requirements Python 3.6+ @@ -88,16 +96,28 @@ FastAPI stands on the shoulders of giants: ## Installation -```bash -pip install fastapi +
+ +```console +$ pip install fastapi + +---> 100% ``` +
+ You will also need an ASGI server, for production such as Uvicorn or Hypercorn. -```bash -pip install uvicorn +
+ +```console +$ pip install uvicorn + +---> 100% ``` +
+ ## Example ### Create it @@ -151,10 +171,20 @@ If you don't know, check the _"In a hurry?"_ section about + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. ``` + +
About the command uvicorn main:app --reload... diff --git a/docs/js/custom.js b/docs/js/custom.js index 54bd53d2700b7..0f1d485216167 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -20,6 +20,114 @@ async function getData() { return data } +function setupTermynal() { + document.querySelectorAll(".use-termynal").forEach(node => { + node.style.display = "block"; + new Termynal(node, { + lineDelay: 500 + }); + }); + const progressLiteralStart = "---> 100%"; + const promptLiteralStart = "$ "; + const customPromptLiteralStart = "# "; + const termynalActivateClass = "termy"; + let termynals = []; + + function createTermynals() { + document + .querySelectorAll(`.${termynalActivateClass} .codehilite`) + .forEach(node => { + const text = node.textContent; + const lines = text.split("\n"); + const useLines = []; + let buffer = []; + function saveBuffer() { + if (buffer.length) { + let isBlankSpace = true; + buffer.forEach(line => { + if (line) { + isBlankSpace = false; + } + }); + dataValue = {}; + if (isBlankSpace) { + dataValue["delay"] = 0; + } + if (buffer[buffer.length - 1] === "") { + // A last single
won't have effect + // so put an additional one + buffer.push(""); + } + const bufferValue = buffer.join("
"); + dataValue["value"] = bufferValue; + useLines.push(dataValue); + buffer = []; + } + } + for (let line of lines) { + if (line === progressLiteralStart) { + saveBuffer(); + useLines.push({ + type: "progress" + }); + } else if (line.startsWith(promptLiteralStart)) { + saveBuffer(); + const value = line.replace(promptLiteralStart, "").trimEnd(); + useLines.push({ + type: "input", + value: value + }); + } else if (line.startsWith("// ")) { + saveBuffer(); + const value = "💬 " + line.replace("// ", "").trimEnd(); + useLines.push({ + value: value, + class: "termynal-comment", + delay: 0 + }); + } else if (line.startsWith(customPromptLiteralStart)) { + saveBuffer(); + const promptStart = line.indexOf(promptLiteralStart); + if (promptStart === -1) { + console.error("Custom prompt found but no end delimiter", line) + } + const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") + let value = line.slice(promptStart + promptLiteralStart.length); + useLines.push({ + type: "input", + value: value, + prompt: prompt + }); + } else { + buffer.push(line); + } + } + saveBuffer(); + const div = document.createElement("div"); + node.replaceWith(div); + const termynal = new Termynal(div, { + lineData: useLines, + noInit: true, + lineDelay: 500 + }); + termynals.push(termynal); + }); + } + + function loadVisibleTermynals() { + termynals = termynals.filter(termynal => { + if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) { + termynal.init(); + return false; + } + return true; + }); + } + window.addEventListener("scroll", loadVisibleTermynals); + createTermynals(); + loadVisibleTermynals(); +} + async function main() { if (div) { data = await getData() @@ -34,6 +142,8 @@ async function main() { ul.append(li) }) } + + setupTermynal(); } main() diff --git a/docs/js/termynal.js b/docs/js/termynal.js new file mode 100644 index 0000000000000..8b0e9339e8772 --- /dev/null +++ b/docs/js/termynal.js @@ -0,0 +1,264 @@ +/** + * termynal.js + * A lightweight, modern and extensible animated terminal window, using + * async/await. + * + * @author Ines Montani + * @version 0.0.1 + * @license MIT + */ + +'use strict'; + +/** Generate a terminal widget. */ +class Termynal { + /** + * Construct the widget's settings. + * @param {(string|Node)=} container - Query selector or container element. + * @param {Object=} options - Custom settings. + * @param {string} options.prefix - Prefix to use for data attributes. + * @param {number} options.startDelay - Delay before animation, in ms. + * @param {number} options.typeDelay - Delay between each typed character, in ms. + * @param {number} options.lineDelay - Delay between each line, in ms. + * @param {number} options.progressLength - Number of characters displayed as progress bar. + * @param {string} options.progressChar – Character to use for progress bar, defaults to █. + * @param {number} options.progressPercent - Max percent of progress. + * @param {string} options.cursor – Character to use for cursor, defaults to ▋. + * @param {Object[]} lineData - Dynamically loaded line data objects. + * @param {boolean} options.noInit - Don't initialise the animation. + */ + constructor(container = '#termynal', options = {}) { + this.container = (typeof container === 'string') ? document.querySelector(container) : container; + this.pfx = `data-${options.prefix || 'ty'}`; + this.originalStartDelay = this.startDelay = options.startDelay + || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600; + this.originalTypeDelay = this.typeDelay = options.typeDelay + || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90; + this.originalLineDelay = this.lineDelay = options.lineDelay + || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500; + this.progressLength = options.progressLength + || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40; + this.progressChar = options.progressChar + || this.container.getAttribute(`${this.pfx}-progressChar`) || '█'; + this.progressPercent = options.progressPercent + || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100; + this.cursor = options.cursor + || this.container.getAttribute(`${this.pfx}-cursor`) || '▋'; + this.lineData = this.lineDataToElements(options.lineData || []); + this.loadLines() + if (!options.noInit) this.init() + } + + loadLines() { + // Load all the lines and create the container so that the size is fixed + // Otherwise it would be changing and the user viewport would be constantly + // moving as she/he scrolls + const finish = this.generateFinish() + finish.style.visibility = 'hidden' + this.container.appendChild(finish) + // Appends dynamically loaded lines to existing line elements. + this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData); + for (let line of this.lines) { + line.style.visibility = 'hidden' + this.container.appendChild(line) + } + const restart = this.generateRestart() + restart.style.visibility = 'hidden' + this.container.appendChild(restart) + this.container.setAttribute('data-termynal', ''); + } + + /** + * Initialise the widget, get lines, clear container and start animation. + */ + init() { + /** + * Calculates width and height of Termynal container. + * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. + */ + const containerStyle = getComputedStyle(this.container); + this.container.style.width = containerStyle.width !== '0px' ? + containerStyle.width : undefined; + this.container.style.minHeight = containerStyle.height !== '0px' ? + containerStyle.height : undefined; + + this.container.setAttribute('data-termynal', ''); + this.container.innerHTML = ''; + for (let line of this.lines) { + line.style.visibility = 'visible' + } + this.start(); + } + + /** + * Start the animation and rener the lines depending on their data attributes. + */ + async start() { + this.addFinish() + await this._wait(this.startDelay); + + for (let line of this.lines) { + const type = line.getAttribute(this.pfx); + const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay; + + if (type == 'input') { + line.setAttribute(`${this.pfx}-cursor`, this.cursor); + await this.type(line); + await this._wait(delay); + } + + else if (type == 'progress') { + await this.progress(line); + await this._wait(delay); + } + + else { + this.container.appendChild(line); + await this._wait(delay); + } + + line.removeAttribute(`${this.pfx}-cursor`); + } + this.addRestart() + this.finishElement.style.visibility = 'hidden' + this.lineDelay = this.originalLineDelay + this.typeDelay = this.originalTypeDelay + this.startDelay = this.originalStartDelay + } + + generateRestart() { + const restart = document.createElement('a') + restart.onclick = (e) => { + e.preventDefault() + this.container.innerHTML = '' + this.init() + } + restart.href = '#' + restart.setAttribute('data-terminal-control', '') + restart.innerHTML = "restart ↻" + return restart + } + + generateFinish() { + const finish = document.createElement('a') + finish.onclick = (e) => { + e.preventDefault() + this.lineDelay = 0 + this.typeDelay = 0 + this.startDelay = 0 + } + finish.href = '#' + finish.setAttribute('data-terminal-control', '') + finish.innerHTML = "fast →" + this.finishElement = finish + return finish + } + + addRestart() { + const restart = this.generateRestart() + this.container.appendChild(restart) + } + + addFinish() { + const finish = this.generateFinish() + this.container.appendChild(finish) + } + + /** + * Animate a typed line. + * @param {Node} line - The line element to render. + */ + async type(line) { + const chars = [...line.textContent]; + line.textContent = ''; + this.container.appendChild(line); + + for (let char of chars) { + const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay; + await this._wait(delay); + line.textContent += char; + } + } + + /** + * Animate a progress bar. + * @param {Node} line - The line element to render. + */ + async progress(line) { + const progressLength = line.getAttribute(`${this.pfx}-progressLength`) + || this.progressLength; + const progressChar = line.getAttribute(`${this.pfx}-progressChar`) + || this.progressChar; + const chars = progressChar.repeat(progressLength); + const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`) + || this.progressPercent; + line.textContent = ''; + this.container.appendChild(line); + + for (let i = 1; i < chars.length + 1; i++) { + await this._wait(this.typeDelay); + const percent = Math.round(i / chars.length * 100); + line.textContent = `${chars.slice(0, i)} ${percent}%`; + if (percent>progressPercent) { + break; + } + } + } + + /** + * Helper function for animation delays, called with `await`. + * @param {number} time - Timeout, in ms. + */ + _wait(time) { + return new Promise(resolve => setTimeout(resolve, time)); + } + + /** + * Converts line data objects into line elements. + * + * @param {Object[]} lineData - Dynamically loaded lines. + * @param {Object} line - Line data object. + * @returns {Element[]} - Array of line elements. + */ + lineDataToElements(lineData) { + return lineData.map(line => { + let div = document.createElement('div'); + div.innerHTML = `${line.value || ''}`; + + return div.firstElementChild; + }); + } + + /** + * Helper function for generating attributes string. + * + * @param {Object} line - Line data object. + * @returns {string} - String of attributes. + */ + _attributes(line) { + let attrs = ''; + for (let prop in line) { + // Custom add class + if (prop === 'class') { + attrs += ` class=${line[prop]} ` + continue + } + if (prop === 'type') { + attrs += `${this.pfx}="${line[prop]}" ` + } else if (prop !== 'value') { + attrs += `${this.pfx}-${prop}="${line[prop]}" ` + } + } + + return attrs; + } +} + +/** +* HTML API: If current script has container(s) specified, initialise Termynal. +*/ +if (document.currentScript.hasAttribute('data-termynal-container')) { + const containers = document.currentScript.getAttribute('data-termynal-container'); + containers.split('|') + .forEach(container => new Termynal(container)) +} diff --git a/docs/tutorial/bigger-applications.md b/docs/tutorial/bigger-applications.md index 6ee298337fe4f..f6c623b840b3a 100644 --- a/docs/tutorial/bigger-applications.md +++ b/docs/tutorial/bigger-applications.md @@ -292,10 +292,16 @@ The end result is that the item paths are now: Now, run `uvicorn`, using the module `app.main` and the variable `app`: -```bash -uvicorn app.main:app --reload +
+ +```console +$ uvicorn app.main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` +
+ And open the docs at
http://127.0.0.1:8000/docs. You will see the automatic API docs, including the paths from all the submodules, using the correct paths (and prefixes) and the correct tags: diff --git a/docs/tutorial/debugging.md b/docs/tutorial/debugging.md index 3800abd1b70ee..e527bf9712363 100644 --- a/docs/tutorial/debugging.md +++ b/docs/tutorial/debugging.md @@ -12,10 +12,14 @@ In your FastAPI application, import and run `uvicorn` directly: The main purpose of the `__name__ == "__main__"` is to have some code that is executed when your file is called with: -```bash -python myapp.py +
+ +```console +$ python myapp.py ``` +
+ but is not called when another file imports it, like in: ```Python @@ -28,10 +32,14 @@ Let's say your file is named `myapp.py`. If you run it with: -```bash -python myapp.py +
+ +```console +$ python myapp.py ``` +
+ then the internal variable `__name__` in your file, created automatically by Python, will have as value the string `"__main__"`. So, the section: diff --git a/docs/tutorial/first-steps.md b/docs/tutorial/first-steps.md index dd916f3304a80..639cfed395658 100644 --- a/docs/tutorial/first-steps.md +++ b/docs/tutorial/first-steps.md @@ -8,10 +8,20 @@ Copy that to a file `main.py`. Run the live server: -```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. ``` +
+ !!! note The command `uvicorn main:app` refers to: @@ -19,16 +29,13 @@ uvicorn main:app --reload * `app`: the object created inside of `main.py` with the line `app = FastAPI()`. * `--reload`: make the server restart after code changes. Only use for development. -You will see an output like: +In the output, there's a line with something like: ```hl_lines="4" -INFO: Started reloader process [17961] -INFO: Started server process [17962] -INFO: Waiting for application startup. -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` -That last line shows the URL where your app is being served, in your local machine. +That line shows the URL where your app is being served, in your local machine. ### Check it @@ -144,10 +151,16 @@ This will be the main point of interaction to create all your API. This `app` is the same one referred by `uvicorn` in the command: -```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) ``` +
+ If you create your app like: ```Python hl_lines="3" @@ -156,10 +169,16 @@ If you create your app like: And put it in a file `main.py`, then you would call `uvicorn` like: -```bash -uvicorn main:my_awesome_api --reload +
+ +```console +$ uvicorn main:my_awesome_api --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` +
+ ### Step 3: create a *path operation* #### Path diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 0aa572c88e9d4..ae073518ae486 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -12,10 +12,20 @@ All the code blocks can be copied and used directly (they are actually tested Py To run any of the examples, copy the code to a file `main.py`, and start `uvicorn` with: -```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. ``` +
+ It is **HIGHLY encouraged** that you write or copy the code, edit it and run it locally. Using it in your editor is what really shows you the benefits of FastAPI, seeing how little code you have to write, all the type checks, autocompletion, etc. @@ -28,10 +38,16 @@ The first step is to install FastAPI. For the tutorial, you might want to install it with all the optional dependencies and features: -```bash -pip install fastapi[all] +
+ +```console +$ pip install fastapi[all] + +---> 100% ``` +
+ ...that also includes `uvicorn`, that you can use as the server that runs your code. !!! note diff --git a/docs/tutorial/security/first-steps.md b/docs/tutorial/security/first-steps.md index 5d215bdd0540a..6fef16235585c 100644 --- a/docs/tutorial/security/first-steps.md +++ b/docs/tutorial/security/first-steps.md @@ -33,10 +33,16 @@ Copy the example in a file `main.py`: Run the example with: -```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) ``` +
+ ## Check it Go to the interactive docs at: http://127.0.0.1:8000/docs. diff --git a/docs/tutorial/security/oauth2-jwt.md b/docs/tutorial/security/oauth2-jwt.md index 70945f2368bb3..cd15cb9d293fc 100644 --- a/docs/tutorial/security/oauth2-jwt.md +++ b/docs/tutorial/security/oauth2-jwt.md @@ -28,10 +28,16 @@ If you want to play with JWT tokens and see how they work, check + +```console +$ pip install pyjwt + +---> 100% ``` + + ## Password hashing "Hashing" means converting some content (a password in this case) into a sequence of bytes (just a string) that looks like gibberish. @@ -56,10 +62,16 @@ The recommended algorithm is "Bcrypt". So, install PassLib with Bcrypt: -```bash -pip install passlib[bcrypt] +
+ +```console +$ pip install passlib[bcrypt] + +---> 100% ``` +
+ !!! tip With `passlib`, you could even configure it to be able to read passwords created by **Django**, a **Flask** security plug-in or many others. @@ -101,10 +113,16 @@ Create a random secret key that will be used to sign the JWT tokens. To generate a secure random secret key use the command: -```bash -openssl rand -hex 32 +
+ +```console +$ openssl rand -hex 32 + +09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7 ``` +
+ And copy the output to the variable `SECRET_KEY` (don't use the one in the example). Create a variable `ALGORITHM` with the algorithm used to sign the JWT token and set it to `"HS256"`. diff --git a/docs/tutorial/sql-databases.md b/docs/tutorial/sql-databases.md index 77fd2c8f46ab5..ba4b836483140 100644 --- a/docs/tutorial/sql-databases.md +++ b/docs/tutorial/sql-databases.md @@ -440,8 +440,8 @@ A "migration" is the set of steps needed whenever you change the structure of yo !!! info For this to work, you need to use **Python 3.7** or above, or in **Python 3.6**, install the "backports": - ```bash - pip install async-exit-stack async-generator + ```console + $ pip install async-exit-stack async-generator ``` This installs
async-exit-stack and async-generator. @@ -596,10 +596,17 @@ You can copy this code and use it as is. Then you can run it with Uvicorn: -```bash -uvicorn sql_app.main:app --reload + +
+ +```console +$ uvicorn sql_app.main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` +
+ And then, you can open your browser at http://127.0.0.1:8000/docs. And you will be able to interact with your **FastAPI** application, reading data from a real database: diff --git a/docs/tutorial/static-files.md b/docs/tutorial/static-files.md index 1b0725291311d..0a1d639565344 100644 --- a/docs/tutorial/static-files.md +++ b/docs/tutorial/static-files.md @@ -4,10 +4,16 @@ You can serve static files automatically from a directory using `StaticFiles`. First you need to install `aiofiles`: -```bash -pip install aiofiles +
+ +```console +$ pip install aiofiles + +---> 100% ``` +
+ ## Use `StaticFiles` * Import `StaticFiles`. diff --git a/docs/tutorial/testing.md b/docs/tutorial/testing.md index 1b1da2ab28a9f..6733527728c94 100644 --- a/docs/tutorial/testing.md +++ b/docs/tutorial/testing.md @@ -103,14 +103,36 @@ For more information about how to pass data to the backend (using `requests` or After that, you just need to install `pytest`: -```bash -pip install pytest +
+ +```console +$ pip install pytest + +---> 100% ``` +
+ It will detect the files and tests automatically, execute them, and report the results back to you. Run the tests with: -```bash -pytest +
+ +```console +$ pytest + +================ test session starts ================ +platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 +rootdir: /home/user/code/superawesome-cli/app +plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 +collected 6 items + +---> 100% + +test_main.py ...... [100%] + +================= 1 passed in 0.03s ================= ``` + +
diff --git a/mkdocs.yml b/mkdocs.yml index b1f88184f10ee..bacae9baca886 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -141,8 +141,10 @@ extra: link: 'https://tiangolo.com' extra_css: + - 'css/termynal.css' - 'css/custom.css' extra_javascript: - 'https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js' + - 'js/termynal.js' - 'js/custom.js' From 5fd5b6e72d6a0731ae649dd0221acc08ac6f78df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 19 Mar 2020 14:40:58 +0100 Subject: [PATCH 002/153] :memo: Update release notes --- docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 49095f66e03f4..08d8806505371 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,7 @@ ## Latest changes +* Update terminal styles in docs and add note about [**Typer**, the FastAPI of CLIs](https://typer.tiangolo.com/). PR [#1139](https://github.com/tiangolo/fastapi/pull/1139). + ## 0.52.0 * Add new high-performance JSON response class using `orjson`. New docs: [Custom Response - HTML, Stream, File, others: `ORJSONResponse`](https://fastapi.tiangolo.com/advanced/custom-response/#use-orjsonresponse). PR [#1065](https://github.com/tiangolo/fastapi/pull/1065). From 6205935323ded4767438ee81623892621b353415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 26 Mar 2020 20:09:53 +0100 Subject: [PATCH 003/153] :sparkles: Add support for docs translations (#1168) * :globe_with_meridians: Refactor file structure to support internationalization * :white_check_mark: Update tests changed after i18n * :twisted_rightwards_arrows: Merge Typer style from master * :wrench: Update MkConfig with Typer-styles * :art: Format mkdocs.yml with cannonical form * :art: Format mkdocs.yml * :wrench: Update MkDocs config * :heavy_plus_sign: Add docs translation scripts dependencies * :sparkles: Add Typer scripts to handle translations * :sparkles: Add missing translation snippet to include * :sparkles: Update contributing docs, add docs for translations * :see_no_evil: Add docs_build to gitignore * :wrench: Update scripts with new locations and docs scripts * :construction_worker: Update docs deploy action with translations * :memo: Add note about languages not supported in the theme * :sparkles: Add first translation, for Spanish --- .github/workflows/deploy-docs.yml | 4 +- .gitignore | 1 + docs/contributing.md | 176 ------- .../docs}/advanced/additional-responses.md | 10 +- .../docs}/advanced/additional-status-codes.md | 4 +- .../docs}/advanced/advanced-dependencies.md | 10 +- .../docs}/advanced/async-sql-databases.md | 16 +- .../advanced/custom-request-and-route.md | 14 +- .../{ => en/docs}/advanced/custom-response.md | 24 +- docs/{ => en/docs}/advanced/events.md | 5 +- .../docs}/advanced/extending-openapi.md | 20 +- docs/{ => en/docs}/advanced/graphql.md | 5 +- docs/{ => en/docs}/advanced/index.md | 2 + docs/{ => en/docs}/advanced/middleware.md | 8 +- .../{ => en/docs}/advanced/nosql-databases.md | 18 +- .../docs}/advanced/openapi-callbacks.md | 10 +- .../path-operation-advanced-configuration.md | 10 +- .../advanced/response-change-status-code.md | 4 +- .../docs}/advanced/response-cookies.md | 6 +- .../docs}/advanced/response-directly.md | 6 +- .../docs}/advanced/response-headers.md | 6 +- .../advanced/security/http-basic-auth.md | 8 +- docs/{ => en/docs}/advanced/security/index.md | 2 + .../docs}/advanced/security/oauth2-scopes.md | 18 +- .../docs}/advanced/sql-databases-peewee.md | 36 +- .../docs}/advanced/sub-applications-proxy.md | 8 +- docs/{ => en/docs}/advanced/templates.md | 10 +- .../docs}/advanced/testing-dependencies.md | 4 +- docs/{ => en/docs}/advanced/testing-events.md | 6 +- .../docs}/advanced/testing-websockets.md | 4 +- .../docs}/advanced/using-request-directly.md | 4 +- docs/{ => en/docs}/advanced/websockets.md | 9 +- docs/{ => en/docs}/advanced/wsgi.md | 4 +- docs/{ => en/docs}/alternatives.md | 2 + docs/{ => en/docs}/async.md | 2 + docs/{ => en/docs}/benchmarks.md | 2 + docs/en/docs/contributing.md | 441 ++++++++++++++++++ docs/{ => en/docs}/css/custom.css | 0 docs/{ => en/docs}/css/termynal.css | 0 docs/{ => en/docs}/deployment.md | 2 + docs/{ => en/docs}/external-links.md | 2 + docs/{ => en/docs}/features.md | 9 +- docs/{ => en/docs}/help-fastapi.md | 2 + docs/{ => en/docs}/history-design-future.md | 2 + docs/{ => en/docs}/img/favicon.png | Bin .../docs}/img/github-social-preview.png | Bin .../docs}/img/github-social-preview.svg | 0 .../{ => en/docs}/img/icon-transparent-bg.png | Bin docs/{ => en/docs}/img/icon-white-bg.png | Bin docs/{ => en/docs}/img/icon-white.svg | 0 .../img/index/index-01-swagger-ui-simple.png | Bin .../docs}/img/index/index-02-redoc-simple.png | Bin .../docs}/img/index/index-03-swagger-02.png | Bin .../docs}/img/index/index-04-swagger-03.png | Bin .../docs}/img/index/index-05-swagger-04.png | Bin .../docs}/img/index/index-06-redoc-02.png | Bin .../img/logo-margin/logo-teal-vector.svg | 0 .../docs}/img/logo-margin/logo-teal.png | Bin .../docs}/img/logo-margin/logo-teal.svg | 0 .../docs}/img/logo-margin/logo-white-bg.png | Bin docs/{ => en/docs}/img/logo-teal-vector.svg | 0 docs/{ => en/docs}/img/logo-teal.svg | 0 docs/{ => en/docs}/img/pycharm-completion.png | Bin .../docs}/img/python-types/image01.png | Bin .../docs}/img/python-types/image02.png | Bin .../docs}/img/python-types/image03.png | Bin .../docs}/img/python-types/image04.png | Bin .../docs}/img/python-types/image05.png | Bin .../docs}/img/python-types/image06.png | Bin .../tutorial/additional-responses/image01.png | Bin .../application-configuration/image01.png | Bin .../tutorial/async-sql-databases/image01.png | Bin .../tutorial/bigger-applications/image01.png | Bin .../img/tutorial/body-fields/image01.png | Bin .../tutorial/body-nested-models/image01.png | Bin .../docs}/img/tutorial/body/image01.png | Bin .../docs}/img/tutorial/body/image02.png | Bin .../docs}/img/tutorial/body/image03.png | Bin .../docs}/img/tutorial/body/image04.png | Bin .../docs}/img/tutorial/body/image05.png | Bin .../img/tutorial/custom-response/image01.png | Bin .../docs}/img/tutorial/debugging/image01.png | Bin .../img/tutorial/dependencies/image01.png | Bin .../img/tutorial/dependencies/image02.png | Bin .../tutorial/extending-openapi/image01.png | Bin .../docs}/img/tutorial/graphql/image01.png | Bin .../tutorial/openapi-callbacks/image01.png | Bin .../path-operation-configuration/image01.png | Bin .../path-operation-configuration/image02.png | Bin .../path-operation-configuration/image03.png | Bin .../path-operation-configuration/image04.png | Bin .../path-operation-configuration/image05.png | Bin .../img/tutorial/path-params/image01.png | Bin .../img/tutorial/path-params/image02.png | Bin .../img/tutorial/path-params/image03.png | Bin .../query-params-str-validations/image01.png | Bin .../query-params-str-validations/image02.png | Bin .../img/tutorial/response-model/image01.png | Bin .../img/tutorial/response-model/image02.png | Bin .../tutorial/response-status-code/image01.png | Bin .../tutorial/response-status-code/image02.png | Bin .../docs}/img/tutorial/security/image01.png | Bin .../docs}/img/tutorial/security/image02.png | Bin .../docs}/img/tutorial/security/image03.png | Bin .../docs}/img/tutorial/security/image04.png | Bin .../docs}/img/tutorial/security/image05.png | Bin .../docs}/img/tutorial/security/image06.png | Bin .../docs}/img/tutorial/security/image07.png | Bin .../docs}/img/tutorial/security/image08.png | Bin .../docs}/img/tutorial/security/image09.png | Bin .../docs}/img/tutorial/security/image10.png | Bin .../docs}/img/tutorial/security/image11.png | Bin .../docs}/img/tutorial/security/image12.png | Bin .../img/tutorial/sql-databases/image01.png | Bin .../img/tutorial/sql-databases/image02.png | Bin .../img/tutorial/sub-applications/image01.png | Bin .../img/tutorial/sub-applications/image02.png | Bin .../docs}/img/tutorial/websockets/image01.png | Bin .../docs}/img/tutorial/websockets/image02.png | Bin .../docs}/img/tutorial/websockets/image03.png | Bin .../docs}/img/tutorial/websockets/image04.png | Bin docs/{ => en/docs}/img/vscode-completion.png | Bin docs/{ => en/docs}/index.md | 0 docs/{ => en/docs}/js/custom.js | 0 docs/{ => en/docs}/js/termynal.js | 0 docs/{ => en/docs}/project-generation.md | 2 + docs/{ => en/docs}/python-types.md | 28 +- docs/{ => en/docs}/release-notes.md | 2 + .../tutorial/application-configuration.md | 8 +- .../docs}/tutorial/background-tasks.md | 10 +- .../docs}/tutorial/bigger-applications.md | 20 +- docs/{ => en/docs}/tutorial/body-fields.md | 8 +- .../docs}/tutorial/body-multiple-params.md | 12 +- .../docs}/tutorial/body-nested-models.md | 24 +- docs/{ => en/docs}/tutorial/body-updates.md | 10 +- docs/{ => en/docs}/tutorial/body.md | 14 +- docs/{ => en/docs}/tutorial/cookie-params.md | 6 +- docs/{ => en/docs}/tutorial/cors.md | 4 +- docs/{ => en/docs}/tutorial/debugging.md | 4 +- .../dependencies/classes-as-dependencies.md | 16 +- ...pendencies-in-path-operation-decorators.md | 10 +- .../dependencies/dependencies-with-yield.md | 16 +- .../docs}/tutorial/dependencies/index.md | 8 +- .../tutorial/dependencies/sub-dependencies.md | 8 +- docs/{ => en/docs}/tutorial/encoder.md | 4 +- .../docs}/tutorial/extra-data-types.md | 6 +- docs/{ => en/docs}/tutorial/extra-models.md | 12 +- docs/{ => en/docs}/tutorial/first-steps.md | 18 +- .../{ => en/docs}/tutorial/handling-errors.md | 18 +- docs/{ => en/docs}/tutorial/header-params.md | 10 +- docs/{ => en/docs}/tutorial/index.md | 2 + docs/{ => en/docs}/tutorial/middleware.md | 6 +- .../tutorial/path-operation-configuration.md | 14 +- .../path-params-numeric-validations.md | 16 +- docs/{ => en/docs}/tutorial/path-params.md | 20 +- .../tutorial/query-params-str-validations.md | 30 +- docs/{ => en/docs}/tutorial/query-params.md | 16 +- docs/{ => en/docs}/tutorial/request-files.md | 10 +- .../docs}/tutorial/request-forms-and-files.md | 6 +- docs/{ => en/docs}/tutorial/request-forms.md | 6 +- docs/{ => en/docs}/tutorial/response-model.md | 22 +- .../docs}/tutorial/response-status-code.md | 8 +- .../docs}/tutorial/security/first-steps.md | 8 +- .../tutorial/security/get-current-user.md | 14 +- docs/{ => en/docs}/tutorial/security/index.md | 2 + .../docs}/tutorial/security/oauth2-jwt.md | 10 +- .../docs}/tutorial/security/simple-oauth2.md | 12 +- docs/{ => en/docs}/tutorial/sql-databases.md | 48 +- docs/{ => en/docs}/tutorial/static-files.md | 4 +- docs/{ => en/docs}/tutorial/testing.md | 12 +- docs/en/mkdocs.yml | 147 ++++++ docs/es/docs/index.md | 437 +++++++++++++++++ docs/es/mkdocs.yml | 58 +++ docs/missing-translation.md | 4 + .../additional_responses/tutorial001.py | 0 .../additional_responses/tutorial002.py | 0 .../additional_responses/tutorial003.py | 0 .../additional_responses/tutorial004.py | 0 .../additional_status_codes/tutorial001.py | 0 .../advanced_middleware/tutorial001.py | 0 .../advanced_middleware/tutorial002.py | 0 .../advanced_middleware/tutorial003.py | 0 .../src => docs_src}/app_testing/__init__.py | 0 {docs/src => docs_src}/app_testing/main.py | 0 {docs/src => docs_src}/app_testing/main_b.py | 0 .../src => docs_src}/app_testing/test_main.py | 0 .../app_testing/test_main_b.py | 0 .../app_testing/tutorial001.py | 0 .../app_testing/tutorial002.py | 0 .../app_testing/tutorial003.py | 0 .../application_configuration/tutorial001.py | 0 .../application_configuration/tutorial002.py | 0 .../application_configuration/tutorial003.py | 0 .../async_sql_databases/tutorial001.py | 0 .../background_tasks/tutorial001.py | 0 .../background_tasks/tutorial002.py | 0 .../bigger_applications/__init__.py | 0 .../bigger_applications/app/__init__.py | 0 .../bigger_applications/app/main.py | 0 .../app/routers/__init__.py | 0 .../bigger_applications/app/routers/items.py | 0 .../bigger_applications/app/routers/users.py | 0 {docs/src => docs_src}/body/tutorial001.py | 0 {docs/src => docs_src}/body/tutorial002.py | 0 {docs/src => docs_src}/body/tutorial003.py | 0 {docs/src => docs_src}/body/tutorial004.py | 0 .../body_fields/tutorial001.py | 0 .../body_fields/tutorial002.py | 0 .../body_multiple_params/tutorial001.py | 0 .../body_multiple_params/tutorial002.py | 0 .../body_multiple_params/tutorial003.py | 0 .../body_multiple_params/tutorial004.py | 0 .../body_multiple_params/tutorial005.py | 0 .../body_nested_models/tutorial001.py | 0 .../body_nested_models/tutorial002.py | 0 .../body_nested_models/tutorial003.py | 0 .../body_nested_models/tutorial004.py | 0 .../body_nested_models/tutorial005.py | 0 .../body_nested_models/tutorial006.py | 0 .../body_nested_models/tutorial007.py | 0 .../body_nested_models/tutorial008.py | 0 .../body_nested_models/tutorial009.py | 0 .../body_updates/tutorial001.py | 0 .../body_updates/tutorial002.py | 0 .../cookie_params/tutorial001.py | 0 {docs/src => docs_src}/cors/tutorial001.py | 0 .../custom_request_and_route/tutorial001.py | 0 .../custom_request_and_route/tutorial002.py | 0 .../custom_request_and_route/tutorial003.py | 0 .../custom_response/tutorial001.py | 0 .../custom_response/tutorial001b.py | 0 .../custom_response/tutorial002.py | 0 .../custom_response/tutorial003.py | 0 .../custom_response/tutorial004.py | 0 .../custom_response/tutorial005.py | 0 .../custom_response/tutorial006.py | 0 .../custom_response/tutorial007.py | 0 .../custom_response/tutorial008.py | 0 .../custom_response/tutorial009.py | 0 .../src => docs_src}/debugging/tutorial001.py | 0 .../dependencies/tutorial001.py | 0 .../dependencies/tutorial002.py | 0 .../dependencies/tutorial003.py | 0 .../dependencies/tutorial004.py | 0 .../dependencies/tutorial005.py | 0 .../dependencies/tutorial006.py | 0 .../dependencies/tutorial007.py | 0 .../dependencies/tutorial008.py | 0 .../dependencies/tutorial009.py | 0 .../dependencies/tutorial010.py | 0 .../dependencies/tutorial011.py | 0 .../dependency_testing/tutorial001.py | 0 {docs/src => docs_src}/encoder/tutorial001.py | 0 {docs/src => docs_src}/events/tutorial001.py | 0 {docs/src => docs_src}/events/tutorial002.py | 0 .../extending_openapi/tutorial001.py | 0 .../extending_openapi/tutorial002.py | 0 .../extra_data_types/tutorial001.py | 0 .../extra_models/tutorial001.py | 0 .../extra_models/tutorial002.py | 0 .../extra_models/tutorial003.py | 0 .../extra_models/tutorial004.py | 0 .../extra_models/tutorial005.py | 0 .../first_steps/tutorial001.py | 0 .../first_steps/tutorial002.py | 0 .../first_steps/tutorial003.py | 0 {docs/src => docs_src}/graphql/tutorial001.py | 0 .../handling_errors/tutorial001.py | 0 .../handling_errors/tutorial002.py | 0 .../handling_errors/tutorial003.py | 0 .../handling_errors/tutorial004.py | 0 .../handling_errors/tutorial005.py | 0 .../handling_errors/tutorial006.py | 0 .../header_params/tutorial001.py | 0 .../header_params/tutorial002.py | 0 .../header_params/tutorial003.py | 0 .../middleware/tutorial001.py | 0 .../nosql_databases/tutorial001.py | 0 .../openapi_callbacks/tutorial001.py | 0 .../tutorial001.py | 0 .../tutorial002.py | 0 .../tutorial003.py | 0 .../tutorial004.py | 0 .../tutorial001.py | 0 .../tutorial002.py | 0 .../tutorial003.py | 0 .../tutorial004.py | 0 .../tutorial005.py | 0 .../tutorial006.py | 0 .../path_params/tutorial001.py | 0 .../path_params/tutorial002.py | 0 .../path_params/tutorial003.py | 0 .../path_params/tutorial004.py | 0 .../path_params/tutorial005.py | 0 .../tutorial001.py | 0 .../tutorial002.py | 0 .../tutorial003.py | 0 .../tutorial004.py | 0 .../tutorial005.py | 0 .../tutorial006.py | 0 .../python_types/tutorial001.py | 0 .../python_types/tutorial002.py | 0 .../python_types/tutorial003.py | 0 .../python_types/tutorial004.py | 0 .../python_types/tutorial005.py | 0 .../python_types/tutorial006.py | 0 .../python_types/tutorial007.py | 0 .../python_types/tutorial008.py | 0 .../python_types/tutorial009.py | 0 .../python_types/tutorial010.py | 0 .../query_params/tutorial001.py | 0 .../query_params/tutorial002.py | 0 .../query_params/tutorial003.py | 0 .../query_params/tutorial004.py | 0 .../query_params/tutorial005.py | 0 .../query_params/tutorial006.py | 0 .../query_params/tutorial007.py | 0 .../tutorial001.py | 0 .../tutorial002.py | 0 .../tutorial003.py | 0 .../tutorial004.py | 0 .../tutorial005.py | 0 .../tutorial006.py | 0 .../tutorial007.py | 0 .../tutorial008.py | 0 .../tutorial009.py | 0 .../tutorial010.py | 0 .../tutorial011.py | 0 .../tutorial012.py | 0 .../tutorial013.py | 0 .../request_files/tutorial001.py | 0 .../request_files/tutorial002.py | 0 .../request_forms/tutorial001.py | 0 .../request_forms_and_files/tutorial001.py | 0 .../tutorial001.py | 0 .../response_cookies/tutorial001.py | 0 .../response_cookies/tutorial002.py | 0 .../response_directly/tutorial001.py | 0 .../response_directly/tutorial002.py | 0 .../response_headers/tutorial001.py | 0 .../response_headers/tutorial002.py | 0 .../response_model/tutorial001.py | 0 .../response_model/tutorial002.py | 0 .../response_model/tutorial003.py | 0 .../response_model/tutorial004.py | 0 .../response_model/tutorial005.py | 0 .../response_model/tutorial006.py | 0 .../response_status_code/tutorial001.py | 0 .../response_status_code/tutorial002.py | 0 .../src => docs_src}/security/tutorial001.py | 0 .../src => docs_src}/security/tutorial002.py | 0 .../src => docs_src}/security/tutorial003.py | 0 .../src => docs_src}/security/tutorial004.py | 0 .../src => docs_src}/security/tutorial005.py | 0 .../src => docs_src}/security/tutorial006.py | 0 .../src => docs_src}/security/tutorial007.py | 0 .../sql_databases/__init__.py | 0 .../sql_databases/sql_app/__init__.py | 0 .../sql_databases/sql_app/alt_main.py | 0 .../sql_databases/sql_app/crud.py | 0 .../sql_databases/sql_app/database.py | 0 .../sql_databases/sql_app/main.py | 0 .../sql_databases/sql_app/models.py | 0 .../sql_databases/sql_app/schemas.py | 0 .../sql_databases_peewee/__init__.py | 0 .../sql_databases_peewee/sql_app/__init__.py | 0 .../sql_databases_peewee/sql_app/crud.py | 0 .../sql_databases_peewee/sql_app/database.py | 0 .../sql_databases_peewee/sql_app/main.py | 0 .../sql_databases_peewee/sql_app/models.py | 0 .../sql_databases_peewee/sql_app/schemas.py | 0 .../static_files/tutorial001.py | 0 .../sub_applications/tutorial001.py | 0 .../templates/static/styles.css | 0 .../templates/templates/item.html | 0 .../src => docs_src}/templates/tutorial001.py | 0 .../using_request_directly/tutorial001.py | 0 {docs/src => docs_src}/websockets/__init__.py | 0 .../websockets/tutorial001.py | 0 .../websockets/tutorial002.py | 0 {docs/src => docs_src}/wsgi/tutorial001.py | 0 mkdocs.yml | 150 ------ pyproject.toml | 5 +- scripts/build-docs.sh | 5 +- scripts/docs.py | 340 ++++++++++++++ scripts/format-imports.sh | 2 +- scripts/format.sh | 6 +- scripts/test.sh | 6 +- .../test_tutorial002.py | 2 +- .../test_tutorial004.py | 2 +- .../test_templates/test_tutorial001.py | 4 +- 391 files changed, 1955 insertions(+), 693 deletions(-) delete mode 100644 docs/contributing.md rename docs/{ => en/docs}/advanced/additional-responses.md (97%) rename docs/{ => en/docs}/advanced/additional-status-codes.md (95%) rename docs/{ => en/docs}/advanced/advanced-dependencies.md (91%) rename docs/{ => en/docs}/advanced/async-sql-databases.md (90%) rename docs/{ => en/docs}/advanced/custom-request-and-route.md (91%) rename docs/{ => en/docs}/advanced/custom-response.md (92%) rename docs/{ => en/docs}/advanced/events.md (93%) rename docs/{ => en/docs}/advanced/extending-openapi.md (94%) rename docs/{ => en/docs}/advanced/graphql.md (93%) rename docs/{ => en/docs}/advanced/index.md (95%) rename docs/{ => en/docs}/advanced/middleware.md (95%) rename docs/{ => en/docs}/advanced/nosql-databases.md (91%) rename docs/{ => en/docs}/advanced/openapi-callbacks.md (97%) rename docs/{ => en/docs}/advanced/path-operation-advanced-configuration.md (82%) rename docs/{ => en/docs}/advanced/response-change-status-code.md (93%) rename docs/{ => en/docs}/advanced/response-cookies.md (94%) rename docs/{ => en/docs}/advanced/response-directly.md (95%) rename docs/{ => en/docs}/advanced/response-headers.md (94%) rename docs/{ => en/docs}/advanced/security/http-basic-auth.md (96%) rename docs/{ => en/docs}/advanced/security/index.md (95%) rename docs/{ => en/docs}/advanced/security/oauth2-scopes.md (97%) rename docs/{ => en/docs}/advanced/sql-databases-peewee.md (95%) rename docs/{ => en/docs}/advanced/sub-applications-proxy.md (94%) rename docs/{ => en/docs}/advanced/templates.md (91%) rename docs/{ => en/docs}/advanced/testing-dependencies.md (96%) rename docs/{ => en/docs}/advanced/testing-events.md (67%) rename docs/{ => en/docs}/advanced/testing-websockets.md (72%) rename docs/{ => en/docs}/advanced/using-request-directly.md (96%) rename docs/{ => en/docs}/advanced/websockets.md (94%) rename docs/{ => en/docs}/advanced/wsgi.md (92%) rename docs/{ => en/docs}/alternatives.md (99%) rename docs/{ => en/docs}/async.md (99%) rename docs/{ => en/docs}/benchmarks.md (99%) create mode 100644 docs/en/docs/contributing.md rename docs/{ => en/docs}/css/custom.css (100%) rename docs/{ => en/docs}/css/termynal.css (100%) rename docs/{ => en/docs}/deployment.md (99%) rename docs/{ => en/docs}/external-links.md (99%) rename docs/{ => en/docs}/features.md (96%) rename docs/{ => en/docs}/help-fastapi.md (99%) rename docs/{ => en/docs}/history-design-future.md (99%) rename docs/{ => en/docs}/img/favicon.png (100%) rename docs/{ => en/docs}/img/github-social-preview.png (100%) rename docs/{ => en/docs}/img/github-social-preview.svg (100%) rename docs/{ => en/docs}/img/icon-transparent-bg.png (100%) rename docs/{ => en/docs}/img/icon-white-bg.png (100%) rename docs/{ => en/docs}/img/icon-white.svg (100%) rename docs/{ => en/docs}/img/index/index-01-swagger-ui-simple.png (100%) rename docs/{ => en/docs}/img/index/index-02-redoc-simple.png (100%) rename docs/{ => en/docs}/img/index/index-03-swagger-02.png (100%) rename docs/{ => en/docs}/img/index/index-04-swagger-03.png (100%) rename docs/{ => en/docs}/img/index/index-05-swagger-04.png (100%) rename docs/{ => en/docs}/img/index/index-06-redoc-02.png (100%) rename docs/{ => en/docs}/img/logo-margin/logo-teal-vector.svg (100%) rename docs/{ => en/docs}/img/logo-margin/logo-teal.png (100%) rename docs/{ => en/docs}/img/logo-margin/logo-teal.svg (100%) rename docs/{ => en/docs}/img/logo-margin/logo-white-bg.png (100%) rename docs/{ => en/docs}/img/logo-teal-vector.svg (100%) rename docs/{ => en/docs}/img/logo-teal.svg (100%) rename docs/{ => en/docs}/img/pycharm-completion.png (100%) rename docs/{ => en/docs}/img/python-types/image01.png (100%) rename docs/{ => en/docs}/img/python-types/image02.png (100%) rename docs/{ => en/docs}/img/python-types/image03.png (100%) rename docs/{ => en/docs}/img/python-types/image04.png (100%) rename docs/{ => en/docs}/img/python-types/image05.png (100%) rename docs/{ => en/docs}/img/python-types/image06.png (100%) rename docs/{ => en/docs}/img/tutorial/additional-responses/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/application-configuration/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/async-sql-databases/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/bigger-applications/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/body-fields/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/body-nested-models/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/body/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/body/image02.png (100%) rename docs/{ => en/docs}/img/tutorial/body/image03.png (100%) rename docs/{ => en/docs}/img/tutorial/body/image04.png (100%) rename docs/{ => en/docs}/img/tutorial/body/image05.png (100%) rename docs/{ => en/docs}/img/tutorial/custom-response/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/debugging/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/dependencies/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/dependencies/image02.png (100%) rename docs/{ => en/docs}/img/tutorial/extending-openapi/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/graphql/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/openapi-callbacks/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/path-operation-configuration/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/path-operation-configuration/image02.png (100%) rename docs/{ => en/docs}/img/tutorial/path-operation-configuration/image03.png (100%) rename docs/{ => en/docs}/img/tutorial/path-operation-configuration/image04.png (100%) rename docs/{ => en/docs}/img/tutorial/path-operation-configuration/image05.png (100%) rename docs/{ => en/docs}/img/tutorial/path-params/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/path-params/image02.png (100%) rename docs/{ => en/docs}/img/tutorial/path-params/image03.png (100%) rename docs/{ => en/docs}/img/tutorial/query-params-str-validations/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/query-params-str-validations/image02.png (100%) rename docs/{ => en/docs}/img/tutorial/response-model/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/response-model/image02.png (100%) rename docs/{ => en/docs}/img/tutorial/response-status-code/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/response-status-code/image02.png (100%) rename docs/{ => en/docs}/img/tutorial/security/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/security/image02.png (100%) rename docs/{ => en/docs}/img/tutorial/security/image03.png (100%) rename docs/{ => en/docs}/img/tutorial/security/image04.png (100%) rename docs/{ => en/docs}/img/tutorial/security/image05.png (100%) rename docs/{ => en/docs}/img/tutorial/security/image06.png (100%) rename docs/{ => en/docs}/img/tutorial/security/image07.png (100%) rename docs/{ => en/docs}/img/tutorial/security/image08.png (100%) rename docs/{ => en/docs}/img/tutorial/security/image09.png (100%) rename docs/{ => en/docs}/img/tutorial/security/image10.png (100%) rename docs/{ => en/docs}/img/tutorial/security/image11.png (100%) rename docs/{ => en/docs}/img/tutorial/security/image12.png (100%) rename docs/{ => en/docs}/img/tutorial/sql-databases/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/sql-databases/image02.png (100%) rename docs/{ => en/docs}/img/tutorial/sub-applications/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/sub-applications/image02.png (100%) rename docs/{ => en/docs}/img/tutorial/websockets/image01.png (100%) rename docs/{ => en/docs}/img/tutorial/websockets/image02.png (100%) rename docs/{ => en/docs}/img/tutorial/websockets/image03.png (100%) rename docs/{ => en/docs}/img/tutorial/websockets/image04.png (100%) rename docs/{ => en/docs}/img/vscode-completion.png (100%) rename docs/{ => en/docs}/index.md (100%) rename docs/{ => en/docs}/js/custom.js (100%) rename docs/{ => en/docs}/js/termynal.js (100%) rename docs/{ => en/docs}/project-generation.md (99%) rename docs/{ => en/docs}/python-types.md (92%) rename docs/{ => en/docs}/release-notes.md (99%) rename docs/{ => en/docs}/tutorial/application-configuration.md (87%) rename docs/{ => en/docs}/tutorial/background-tasks.md (95%) rename docs/{ => en/docs}/tutorial/bigger-applications.md (95%) rename docs/{ => en/docs}/tutorial/body-fields.md (93%) rename docs/{ => en/docs}/tutorial/body-multiple-params.md (93%) rename docs/{ => en/docs}/tutorial/body-nested-models.md (90%) rename docs/{ => en/docs}/tutorial/body-updates.md (94%) rename docs/{ => en/docs}/tutorial/body.md (95%) rename docs/{ => en/docs}/tutorial/cookie-params.md (88%) rename docs/{ => en/docs}/tutorial/cors.md (98%) rename docs/{ => en/docs}/tutorial/debugging.md (97%) rename docs/{ => en/docs}/tutorial/dependencies/classes-as-dependencies.md (93%) rename docs/{ => en/docs}/tutorial/dependencies/dependencies-in-path-operation-decorators.md (89%) rename docs/{ => en/docs}/tutorial/dependencies/dependencies-with-yield.md (96%) rename docs/{ => en/docs}/tutorial/dependencies/index.md (97%) rename docs/{ => en/docs}/tutorial/dependencies/sub-dependencies.md (95%) rename docs/{ => en/docs}/tutorial/encoder.md (95%) rename docs/{ => en/docs}/tutorial/extra-data-types.md (94%) rename docs/{ => en/docs}/tutorial/extra-models.md (95%) rename docs/{ => en/docs}/tutorial/first-steps.md (95%) rename docs/{ => en/docs}/tutorial/handling-errors.md (95%) rename docs/{ => en/docs}/tutorial/header-params.md (92%) rename docs/{ => en/docs}/tutorial/index.md (98%) rename docs/{ => en/docs}/tutorial/middleware.md (96%) rename docs/{ => en/docs}/tutorial/path-operation-configuration.md (88%) rename docs/{ => en/docs}/tutorial/path-params-numeric-validations.md (89%) rename docs/{ => en/docs}/tutorial/path-params.md (94%) rename docs/{ => en/docs}/tutorial/query-params-str-validations.md (87%) rename docs/{ => en/docs}/tutorial/query-params.md (92%) rename docs/{ => en/docs}/tutorial/request-files.md (96%) rename docs/{ => en/docs}/tutorial/request-forms-and-files.md (88%) rename docs/{ => en/docs}/tutorial/request-forms.md (95%) rename docs/{ => en/docs}/tutorial/response-model.md (92%) rename docs/{ => en/docs}/tutorial/response-status-code.md (94%) rename docs/{ => en/docs}/tutorial/security/first-steps.md (97%) rename docs/{ => en/docs}/tutorial/security/get-current-user.md (92%) rename docs/{ => en/docs}/tutorial/security/index.md (99%) rename docs/{ => en/docs}/tutorial/security/oauth2-jwt.md (97%) rename docs/{ => en/docs}/tutorial/security/simple-oauth2.md (97%) rename docs/{ => en/docs}/tutorial/sql-databases.md (95%) rename docs/{ => en/docs}/tutorial/static-files.md (96%) rename docs/{ => en/docs}/tutorial/testing.md (94%) create mode 100644 docs/en/mkdocs.yml create mode 100644 docs/es/docs/index.md create mode 100644 docs/es/mkdocs.yml create mode 100644 docs/missing-translation.md rename {docs/src => docs_src}/additional_responses/tutorial001.py (100%) rename {docs/src => docs_src}/additional_responses/tutorial002.py (100%) rename {docs/src => docs_src}/additional_responses/tutorial003.py (100%) rename {docs/src => docs_src}/additional_responses/tutorial004.py (100%) rename {docs/src => docs_src}/additional_status_codes/tutorial001.py (100%) rename {docs/src => docs_src}/advanced_middleware/tutorial001.py (100%) rename {docs/src => docs_src}/advanced_middleware/tutorial002.py (100%) rename {docs/src => docs_src}/advanced_middleware/tutorial003.py (100%) rename {docs/src => docs_src}/app_testing/__init__.py (100%) rename {docs/src => docs_src}/app_testing/main.py (100%) rename {docs/src => docs_src}/app_testing/main_b.py (100%) rename {docs/src => docs_src}/app_testing/test_main.py (100%) rename {docs/src => docs_src}/app_testing/test_main_b.py (100%) rename {docs/src => docs_src}/app_testing/tutorial001.py (100%) rename {docs/src => docs_src}/app_testing/tutorial002.py (100%) rename {docs/src => docs_src}/app_testing/tutorial003.py (100%) rename {docs/src => docs_src}/application_configuration/tutorial001.py (100%) rename {docs/src => docs_src}/application_configuration/tutorial002.py (100%) rename {docs/src => docs_src}/application_configuration/tutorial003.py (100%) rename {docs/src => docs_src}/async_sql_databases/tutorial001.py (100%) rename {docs/src => docs_src}/background_tasks/tutorial001.py (100%) rename {docs/src => docs_src}/background_tasks/tutorial002.py (100%) rename {docs/src => docs_src}/bigger_applications/__init__.py (100%) rename {docs/src => docs_src}/bigger_applications/app/__init__.py (100%) rename {docs/src => docs_src}/bigger_applications/app/main.py (100%) rename {docs/src => docs_src}/bigger_applications/app/routers/__init__.py (100%) rename {docs/src => docs_src}/bigger_applications/app/routers/items.py (100%) rename {docs/src => docs_src}/bigger_applications/app/routers/users.py (100%) rename {docs/src => docs_src}/body/tutorial001.py (100%) rename {docs/src => docs_src}/body/tutorial002.py (100%) rename {docs/src => docs_src}/body/tutorial003.py (100%) rename {docs/src => docs_src}/body/tutorial004.py (100%) rename {docs/src => docs_src}/body_fields/tutorial001.py (100%) rename {docs/src => docs_src}/body_fields/tutorial002.py (100%) rename {docs/src => docs_src}/body_multiple_params/tutorial001.py (100%) rename {docs/src => docs_src}/body_multiple_params/tutorial002.py (100%) rename {docs/src => docs_src}/body_multiple_params/tutorial003.py (100%) rename {docs/src => docs_src}/body_multiple_params/tutorial004.py (100%) rename {docs/src => docs_src}/body_multiple_params/tutorial005.py (100%) rename {docs/src => docs_src}/body_nested_models/tutorial001.py (100%) rename {docs/src => docs_src}/body_nested_models/tutorial002.py (100%) rename {docs/src => docs_src}/body_nested_models/tutorial003.py (100%) rename {docs/src => docs_src}/body_nested_models/tutorial004.py (100%) rename {docs/src => docs_src}/body_nested_models/tutorial005.py (100%) rename {docs/src => docs_src}/body_nested_models/tutorial006.py (100%) rename {docs/src => docs_src}/body_nested_models/tutorial007.py (100%) rename {docs/src => docs_src}/body_nested_models/tutorial008.py (100%) rename {docs/src => docs_src}/body_nested_models/tutorial009.py (100%) rename {docs/src => docs_src}/body_updates/tutorial001.py (100%) rename {docs/src => docs_src}/body_updates/tutorial002.py (100%) rename {docs/src => docs_src}/cookie_params/tutorial001.py (100%) rename {docs/src => docs_src}/cors/tutorial001.py (100%) rename {docs/src => docs_src}/custom_request_and_route/tutorial001.py (100%) rename {docs/src => docs_src}/custom_request_and_route/tutorial002.py (100%) rename {docs/src => docs_src}/custom_request_and_route/tutorial003.py (100%) rename {docs/src => docs_src}/custom_response/tutorial001.py (100%) rename {docs/src => docs_src}/custom_response/tutorial001b.py (100%) rename {docs/src => docs_src}/custom_response/tutorial002.py (100%) rename {docs/src => docs_src}/custom_response/tutorial003.py (100%) rename {docs/src => docs_src}/custom_response/tutorial004.py (100%) rename {docs/src => docs_src}/custom_response/tutorial005.py (100%) rename {docs/src => docs_src}/custom_response/tutorial006.py (100%) rename {docs/src => docs_src}/custom_response/tutorial007.py (100%) rename {docs/src => docs_src}/custom_response/tutorial008.py (100%) rename {docs/src => docs_src}/custom_response/tutorial009.py (100%) rename {docs/src => docs_src}/debugging/tutorial001.py (100%) rename {docs/src => docs_src}/dependencies/tutorial001.py (100%) rename {docs/src => docs_src}/dependencies/tutorial002.py (100%) rename {docs/src => docs_src}/dependencies/tutorial003.py (100%) rename {docs/src => docs_src}/dependencies/tutorial004.py (100%) rename {docs/src => docs_src}/dependencies/tutorial005.py (100%) rename {docs/src => docs_src}/dependencies/tutorial006.py (100%) rename {docs/src => docs_src}/dependencies/tutorial007.py (100%) rename {docs/src => docs_src}/dependencies/tutorial008.py (100%) rename {docs/src => docs_src}/dependencies/tutorial009.py (100%) rename {docs/src => docs_src}/dependencies/tutorial010.py (100%) rename {docs/src => docs_src}/dependencies/tutorial011.py (100%) rename {docs/src => docs_src}/dependency_testing/tutorial001.py (100%) rename {docs/src => docs_src}/encoder/tutorial001.py (100%) rename {docs/src => docs_src}/events/tutorial001.py (100%) rename {docs/src => docs_src}/events/tutorial002.py (100%) rename {docs/src => docs_src}/extending_openapi/tutorial001.py (100%) rename {docs/src => docs_src}/extending_openapi/tutorial002.py (100%) rename {docs/src => docs_src}/extra_data_types/tutorial001.py (100%) rename {docs/src => docs_src}/extra_models/tutorial001.py (100%) rename {docs/src => docs_src}/extra_models/tutorial002.py (100%) rename {docs/src => docs_src}/extra_models/tutorial003.py (100%) rename {docs/src => docs_src}/extra_models/tutorial004.py (100%) rename {docs/src => docs_src}/extra_models/tutorial005.py (100%) rename {docs/src => docs_src}/first_steps/tutorial001.py (100%) rename {docs/src => docs_src}/first_steps/tutorial002.py (100%) rename {docs/src => docs_src}/first_steps/tutorial003.py (100%) rename {docs/src => docs_src}/graphql/tutorial001.py (100%) rename {docs/src => docs_src}/handling_errors/tutorial001.py (100%) rename {docs/src => docs_src}/handling_errors/tutorial002.py (100%) rename {docs/src => docs_src}/handling_errors/tutorial003.py (100%) rename {docs/src => docs_src}/handling_errors/tutorial004.py (100%) rename {docs/src => docs_src}/handling_errors/tutorial005.py (100%) rename {docs/src => docs_src}/handling_errors/tutorial006.py (100%) rename {docs/src => docs_src}/header_params/tutorial001.py (100%) rename {docs/src => docs_src}/header_params/tutorial002.py (100%) rename {docs/src => docs_src}/header_params/tutorial003.py (100%) rename {docs/src => docs_src}/middleware/tutorial001.py (100%) rename {docs/src => docs_src}/nosql_databases/tutorial001.py (100%) rename {docs/src => docs_src}/openapi_callbacks/tutorial001.py (100%) rename {docs/src => docs_src}/path_operation_advanced_configuration/tutorial001.py (100%) rename {docs/src => docs_src}/path_operation_advanced_configuration/tutorial002.py (100%) rename {docs/src => docs_src}/path_operation_advanced_configuration/tutorial003.py (100%) rename {docs/src => docs_src}/path_operation_advanced_configuration/tutorial004.py (100%) rename {docs/src => docs_src}/path_operation_configuration/tutorial001.py (100%) rename {docs/src => docs_src}/path_operation_configuration/tutorial002.py (100%) rename {docs/src => docs_src}/path_operation_configuration/tutorial003.py (100%) rename {docs/src => docs_src}/path_operation_configuration/tutorial004.py (100%) rename {docs/src => docs_src}/path_operation_configuration/tutorial005.py (100%) rename {docs/src => docs_src}/path_operation_configuration/tutorial006.py (100%) rename {docs/src => docs_src}/path_params/tutorial001.py (100%) rename {docs/src => docs_src}/path_params/tutorial002.py (100%) rename {docs/src => docs_src}/path_params/tutorial003.py (100%) rename {docs/src => docs_src}/path_params/tutorial004.py (100%) rename {docs/src => docs_src}/path_params/tutorial005.py (100%) rename {docs/src => docs_src}/path_params_numeric_validations/tutorial001.py (100%) rename {docs/src => docs_src}/path_params_numeric_validations/tutorial002.py (100%) rename {docs/src => docs_src}/path_params_numeric_validations/tutorial003.py (100%) rename {docs/src => docs_src}/path_params_numeric_validations/tutorial004.py (100%) rename {docs/src => docs_src}/path_params_numeric_validations/tutorial005.py (100%) rename {docs/src => docs_src}/path_params_numeric_validations/tutorial006.py (100%) rename {docs/src => docs_src}/python_types/tutorial001.py (100%) rename {docs/src => docs_src}/python_types/tutorial002.py (100%) rename {docs/src => docs_src}/python_types/tutorial003.py (100%) rename {docs/src => docs_src}/python_types/tutorial004.py (100%) rename {docs/src => docs_src}/python_types/tutorial005.py (100%) rename {docs/src => docs_src}/python_types/tutorial006.py (100%) rename {docs/src => docs_src}/python_types/tutorial007.py (100%) rename {docs/src => docs_src}/python_types/tutorial008.py (100%) rename {docs/src => docs_src}/python_types/tutorial009.py (100%) rename {docs/src => docs_src}/python_types/tutorial010.py (100%) rename {docs/src => docs_src}/query_params/tutorial001.py (100%) rename {docs/src => docs_src}/query_params/tutorial002.py (100%) rename {docs/src => docs_src}/query_params/tutorial003.py (100%) rename {docs/src => docs_src}/query_params/tutorial004.py (100%) rename {docs/src => docs_src}/query_params/tutorial005.py (100%) rename {docs/src => docs_src}/query_params/tutorial006.py (100%) rename {docs/src => docs_src}/query_params/tutorial007.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial001.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial002.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial003.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial004.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial005.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial006.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial007.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial008.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial009.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial010.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial011.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial012.py (100%) rename {docs/src => docs_src}/query_params_str_validations/tutorial013.py (100%) rename {docs/src => docs_src}/request_files/tutorial001.py (100%) rename {docs/src => docs_src}/request_files/tutorial002.py (100%) rename {docs/src => docs_src}/request_forms/tutorial001.py (100%) rename {docs/src => docs_src}/request_forms_and_files/tutorial001.py (100%) rename {docs/src => docs_src}/response_change_status_code/tutorial001.py (100%) rename {docs/src => docs_src}/response_cookies/tutorial001.py (100%) rename {docs/src => docs_src}/response_cookies/tutorial002.py (100%) rename {docs/src => docs_src}/response_directly/tutorial001.py (100%) rename {docs/src => docs_src}/response_directly/tutorial002.py (100%) rename {docs/src => docs_src}/response_headers/tutorial001.py (100%) rename {docs/src => docs_src}/response_headers/tutorial002.py (100%) rename {docs/src => docs_src}/response_model/tutorial001.py (100%) rename {docs/src => docs_src}/response_model/tutorial002.py (100%) rename {docs/src => docs_src}/response_model/tutorial003.py (100%) rename {docs/src => docs_src}/response_model/tutorial004.py (100%) rename {docs/src => docs_src}/response_model/tutorial005.py (100%) rename {docs/src => docs_src}/response_model/tutorial006.py (100%) rename {docs/src => docs_src}/response_status_code/tutorial001.py (100%) rename {docs/src => docs_src}/response_status_code/tutorial002.py (100%) rename {docs/src => docs_src}/security/tutorial001.py (100%) rename {docs/src => docs_src}/security/tutorial002.py (100%) rename {docs/src => docs_src}/security/tutorial003.py (100%) rename {docs/src => docs_src}/security/tutorial004.py (100%) rename {docs/src => docs_src}/security/tutorial005.py (100%) rename {docs/src => docs_src}/security/tutorial006.py (100%) rename {docs/src => docs_src}/security/tutorial007.py (100%) rename {docs/src => docs_src}/sql_databases/__init__.py (100%) rename {docs/src => docs_src}/sql_databases/sql_app/__init__.py (100%) rename {docs/src => docs_src}/sql_databases/sql_app/alt_main.py (100%) rename {docs/src => docs_src}/sql_databases/sql_app/crud.py (100%) rename {docs/src => docs_src}/sql_databases/sql_app/database.py (100%) rename {docs/src => docs_src}/sql_databases/sql_app/main.py (100%) rename {docs/src => docs_src}/sql_databases/sql_app/models.py (100%) rename {docs/src => docs_src}/sql_databases/sql_app/schemas.py (100%) rename {docs/src => docs_src}/sql_databases_peewee/__init__.py (100%) rename {docs/src => docs_src}/sql_databases_peewee/sql_app/__init__.py (100%) rename {docs/src => docs_src}/sql_databases_peewee/sql_app/crud.py (100%) rename {docs/src => docs_src}/sql_databases_peewee/sql_app/database.py (100%) rename {docs/src => docs_src}/sql_databases_peewee/sql_app/main.py (100%) rename {docs/src => docs_src}/sql_databases_peewee/sql_app/models.py (100%) rename {docs/src => docs_src}/sql_databases_peewee/sql_app/schemas.py (100%) rename {docs/src => docs_src}/static_files/tutorial001.py (100%) rename {docs/src => docs_src}/sub_applications/tutorial001.py (100%) rename {docs/src => docs_src}/templates/static/styles.css (100%) rename {docs/src => docs_src}/templates/templates/item.html (100%) rename {docs/src => docs_src}/templates/tutorial001.py (100%) rename {docs/src => docs_src}/using_request_directly/tutorial001.py (100%) rename {docs/src => docs_src}/websockets/__init__.py (100%) rename {docs/src => docs_src}/websockets/tutorial001.py (100%) rename {docs/src => docs_src}/websockets/tutorial002.py (100%) rename {docs/src => docs_src}/wsgi/tutorial001.py (100%) delete mode 100644 mkdocs.yml create mode 100644 scripts/docs.py diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 77f1c274c1adc..60f8e4b4ae4ef 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -16,8 +16,8 @@ jobs: run: python3.7 -m pip install flit - name: Install docs extras run: python3.7 -m flit install --extras doc - - name: Build MkDocs - run: python3.7 -m mkdocs build + - name: Build Docs + run: python3.7 ./scripts/docs.py build-all - name: Deploy to Netlify uses: nwtgck/actions-netlify@v1.0.3 with: diff --git a/.gitignore b/.gitignore index f110dc59725eb..3dbfdd44d16b5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ test.db log.txt Pipfile.lock env3.* +docs_build diff --git a/docs/contributing.md b/docs/contributing.md deleted file mode 100644 index 4ca50db9e831a..0000000000000 --- a/docs/contributing.md +++ /dev/null @@ -1,176 +0,0 @@ -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: - -```console -$ python -m venv env -``` - -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: - -```console -$ source ./env/bin/activate -``` - -Or in Windows' PowerShell: - -```console -$ .\env\Scripts\Activate.ps1 -``` - -Or if you use Bash for Windows (e.g. Git Bash): - -```console -$ source ./env/Scripts/activate -``` - -To check it worked, use: - -```console -$ which pip - -some/directory/fastapi/env/bin/pip -``` - -If it shows the `pip` binary at `env/bin/pip` then it worked. 🎉 - -Or in Windows PowerShell: - -```console -$ Get-Command pip - -some/directory/fastapi/env/bin/pip -``` - -!!! 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 Flit to build, package and publish the project. - -After activating the environment as described above, install `flit`: - -```console -$ pip install flit -``` - -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: - -```console -$ flit install --deps develop --symlink -``` - -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`, 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: - -```console -$ bash scripts/format.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: - -```console -$ flit install --symlink -``` - -### Format imports - -There is another script that formats all the imports and makes sure you don't have unused imports: - -```console -$ bash scripts/format-imports.sh -``` - -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 - -The documentation uses MkDocs. - -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 test coverage. - -During local development, there is a script that builds the site and checks for any changes, live-reloading: - -```console -$ 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 - -If you run the examples with, e.g.: - -```console -$ 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: - -```console -$ 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. - -### Tests in your editor - -If you want to use the integrated tests in your editor add `./docs/src` to your `PYTHONPATH` variable. - -For example, in VS Code you can create a file `.env` with: - -```env -PYTHONPATH=./docs/src -``` diff --git a/docs/advanced/additional-responses.md b/docs/en/docs/advanced/additional-responses.md similarity index 97% rename from docs/advanced/additional-responses.md rename to docs/en/docs/advanced/additional-responses.md index 71c4cf61d279a..4b2abaada3109 100644 --- a/docs/advanced/additional-responses.md +++ b/docs/en/docs/advanced/additional-responses.md @@ -1,3 +1,5 @@ +# Additional Responses in OpenAPI + !!! warning This is a rather advanced topic. @@ -22,7 +24,7 @@ Each of those response `dict`s can have a key `model`, containing a Pydantic mod For example, to declare another response with a status code `404` and a Pydantic model `Message`, you can write: ```Python hl_lines="18 23" -{!./src/additional_responses/tutorial001.py!} +{!../../../docs_src/additional_responses/tutorial001.py!} ``` !!! note @@ -167,7 +169,7 @@ You can use this same `responses` parameter to add different media types for the For example, you can add an additional media type of `image/png`, declaring that your *path operation* can return a JSON object (with media type `application/json`) or a PNG image: ```Python hl_lines="17 18 19 20 21 22 23 24 28" -{!./src/additional_responses/tutorial002.py!} +{!../../../docs_src/additional_responses/tutorial002.py!} ``` !!! note @@ -191,7 +193,7 @@ For example, you can declare a response with a status code `404` that uses a Pyd And a response with a status code `200` that uses your `response_model`, but includes a custom `example`: ```Python hl_lines="20 21 22 23 24 25 26 27 28 29 30 31" -{!./src/additional_responses/tutorial003.py!} +{!../../../docs_src/additional_responses/tutorial003.py!} ``` It will all be combined and included in your OpenAPI, and shown in the API docs: @@ -227,7 +229,7 @@ You can use that technique to re-use some predefined responses in your *path ope For example: ```Python hl_lines="11 12 13 14 15 24" -{!./src/additional_responses/tutorial004.py!} +{!../../../docs_src/additional_responses/tutorial004.py!} ``` ## More information about OpenAPI responses diff --git a/docs/advanced/additional-status-codes.md b/docs/en/docs/advanced/additional-status-codes.md similarity index 95% rename from docs/advanced/additional-status-codes.md rename to docs/en/docs/advanced/additional-status-codes.md index 14a867c989c88..b9a32cfa3b661 100644 --- a/docs/advanced/additional-status-codes.md +++ b/docs/en/docs/advanced/additional-status-codes.md @@ -1,3 +1,5 @@ +# Additional Status Codes + By default, **FastAPI** will return the responses using a `JSONResponse`, putting the content you return from your *path operation* inside of that `JSONResponse`. It will use the default status code or the one you set in your *path operation*. @@ -13,7 +15,7 @@ But you also want it to accept new items. And when the items didn't exist before To achieve that, import `JSONResponse`, and return your content there directly, setting the `status_code` that you want: ```Python hl_lines="2 19" -{!./src/additional_status_codes/tutorial001.py!} +{!../../../docs_src/additional_status_codes/tutorial001.py!} ``` !!! warning diff --git a/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md similarity index 91% rename from docs/advanced/advanced-dependencies.md rename to docs/en/docs/advanced/advanced-dependencies.md index ec796362a847d..5e79776cacddd 100644 --- a/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -1,3 +1,5 @@ +# Advanced Dependencies + ## Parameterized dependencies All the dependencies we have seen are a fixed function or class. @@ -17,7 +19,7 @@ Not the class itself (which is already a callable), but an instance of that clas To do that, we declare a method `__call__`: ```Python hl_lines="10" -{!./src/dependencies/tutorial011.py!} +{!../../../docs_src/dependencies/tutorial011.py!} ``` In this case, this `__call__` is what **FastAPI** will use to check for additional parameters and sub-dependencies, and this is what will be called to pass a value to the parameter in your *path operation function* later. @@ -27,7 +29,7 @@ In this case, this `__call__` is what **FastAPI** will use to check for addition And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency: ```Python hl_lines="7" -{!./src/dependencies/tutorial011.py!} +{!../../../docs_src/dependencies/tutorial011.py!} ``` In this case, **FastAPI** won't ever touch or care about `__init__`, we will use it directly in our code. @@ -37,7 +39,7 @@ In this case, **FastAPI** won't ever touch or care about `__init__`, we will use We could create an instance of this class with: ```Python hl_lines="16" -{!./src/dependencies/tutorial011.py!} +{!../../../docs_src/dependencies/tutorial011.py!} ``` And that way we are able to "parameterize" our dependency, that now has `"bar"` inside of it, as the attribute `checker.fixed_content`. @@ -55,7 +57,7 @@ checker(q="somequery") ...and pass whatever that returns as the value of the dependency in our *path operation function* as the parameter `fixed_content_included`: ```Python hl_lines="20" -{!./src/dependencies/tutorial011.py!} +{!../../../docs_src/dependencies/tutorial011.py!} ``` !!! tip diff --git a/docs/advanced/async-sql-databases.md b/docs/en/docs/advanced/async-sql-databases.md similarity index 90% rename from docs/advanced/async-sql-databases.md rename to docs/en/docs/advanced/async-sql-databases.md index c7d0b86dff3ed..523bc91bf4726 100644 --- a/docs/advanced/async-sql-databases.md +++ b/docs/en/docs/advanced/async-sql-databases.md @@ -1,3 +1,5 @@ +# Async SQL (Relational) Databases + You can also use `encode/databases` with **FastAPI** to connect to databases using `async` and `await`. It is compatible with: @@ -22,7 +24,7 @@ Later, for your production application, you might want to use a database server * Create a table `notes` using the `metadata` object. ```Python hl_lines="4 14 16 17 18 19 20 21 22" -{!./src/async_sql_databases/tutorial001.py!} +{!../../../docs_src/async_sql_databases/tutorial001.py!} ``` !!! tip @@ -37,7 +39,7 @@ Later, for your production application, you might want to use a database server * Create a `database` object. ```Python hl_lines="3 9 12" -{!./src/async_sql_databases/tutorial001.py!} +{!../../../docs_src/async_sql_databases/tutorial001.py!} ``` !!! tip @@ -53,7 +55,7 @@ Here, this section would run directly, right before starting your **FastAPI** ap * Create all the tables from the `metadata` object. ```Python hl_lines="25 26 27 28" -{!./src/async_sql_databases/tutorial001.py!} +{!../../../docs_src/async_sql_databases/tutorial001.py!} ``` ## Create models @@ -64,7 +66,7 @@ Create Pydantic models for: * Notes to be returned (`Note`). ```Python hl_lines="31 32 33 36 37 38 39" -{!./src/async_sql_databases/tutorial001.py!} +{!../../../docs_src/async_sql_databases/tutorial001.py!} ``` By creating these Pydantic models, the input data will be validated, serialized (converted), and annotated (documented). @@ -77,7 +79,7 @@ So, you will be able to see it all in the interactive API docs. * Create event handlers to connect and disconnect from the database. ```Python hl_lines="42 45 46 47 50 51 52" -{!./src/async_sql_databases/tutorial001.py!} +{!../../../docs_src/async_sql_databases/tutorial001.py!} ``` ## Read notes @@ -85,7 +87,7 @@ So, you will be able to see it all in the interactive API docs. Create the *path operation function* to read notes: ```Python hl_lines="55 56 57 58" -{!./src/async_sql_databases/tutorial001.py!} +{!../../../docs_src/async_sql_databases/tutorial001.py!} ``` !!! Note @@ -102,7 +104,7 @@ That documents (and validates, serializes, filters) the output data, as a `list` Create the *path operation function* to create notes: ```Python hl_lines="61 62 63 64 65" -{!./src/async_sql_databases/tutorial001.py!} +{!../../../docs_src/async_sql_databases/tutorial001.py!} ``` !!! Note diff --git a/docs/advanced/custom-request-and-route.md b/docs/en/docs/advanced/custom-request-and-route.md similarity index 91% rename from docs/advanced/custom-request-and-route.md rename to docs/en/docs/advanced/custom-request-and-route.md index 2e823143a74d9..6c5e97a18b003 100644 --- a/docs/advanced/custom-request-and-route.md +++ b/docs/en/docs/advanced/custom-request-and-route.md @@ -1,3 +1,5 @@ +# 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. @@ -35,7 +37,7 @@ 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 9 10 11 12 13 14 15" -{!./src/custom_request_and_route/tutorial001.py!} +{!../../../docs_src/custom_request_and_route/tutorial001.py!} ``` ### Create a custom `GzipRoute` class @@ -49,7 +51,7 @@ This method returns a function. And that function is what will receive a request Here we use it to create a `GzipRequest` from the original request. ```Python hl_lines="18 19 20 21 22 23 24 25 26" -{!./src/custom_request_and_route/tutorial001.py!} +{!../../../docs_src/custom_request_and_route/tutorial001.py!} ``` !!! note "Technical Details" @@ -83,13 +85,13 @@ We can also use this same approach to access the request body in an exception ha All we need to do is handle the request inside a `try`/`except` block: ```Python hl_lines="13 15" -{!./src/custom_request_and_route/tutorial002.py!} +{!../../../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 17 18" -{!./src/custom_request_and_route/tutorial002.py!} +{!../../../docs_src/custom_request_and_route/tutorial002.py!} ``` ## Custom `APIRoute` class in a router @@ -97,11 +99,11 @@ If an exception occurs, the`Request` instance will still be in scope, so we can You can also set the `route_class` parameter of an `APIRouter`: ```Python hl_lines="26" -{!./src/custom_request_and_route/tutorial003.py!} +{!../../../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 14 15 16 17 18 19 20" -{!./src/custom_request_and_route/tutorial003.py!} +{!../../../docs_src/custom_request_and_route/tutorial003.py!} ``` diff --git a/docs/advanced/custom-response.md b/docs/en/docs/advanced/custom-response.md similarity index 92% rename from docs/advanced/custom-response.md rename to docs/en/docs/advanced/custom-response.md index 21e7abee4bcb1..545a84436015c 100644 --- a/docs/advanced/custom-response.md +++ b/docs/en/docs/advanced/custom-response.md @@ -1,3 +1,5 @@ +# 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}. @@ -20,7 +22,7 @@ For example, if you are squeezing performance, you can install and use NoSQL. Here we'll see an example using **Couchbase**, a document based NoSQL database. @@ -18,7 +20,7 @@ You can adapt it to any other NoSQL database like: For now, don't pay attention to the rest, only the imports: ```Python hl_lines="6 7 8" -{!./src/nosql_databases/tutorial001.py!} +{!../../../docs_src/nosql_databases/tutorial001.py!} ``` ## Define a constant to use as a "document type" @@ -28,7 +30,7 @@ We will use it later as a fixed field `type` in our documents. This is not required by Couchbase, but is a good practice that will help you afterwards. ```Python hl_lines="10" -{!./src/nosql_databases/tutorial001.py!} +{!../../../docs_src/nosql_databases/tutorial001.py!} ``` ## Add a function to get a `Bucket` @@ -53,7 +55,7 @@ This utility function will: * Return it. ```Python hl_lines="13 14 15 16 17 18 19 20 21 22" -{!./src/nosql_databases/tutorial001.py!} +{!../../../docs_src/nosql_databases/tutorial001.py!} ``` ## Create Pydantic models @@ -65,7 +67,7 @@ As **Couchbase** "documents" are actually just "JSON objects", we can model them First, let's create a `User` model: ```Python hl_lines="25 26 27 28 29" -{!./src/nosql_databases/tutorial001.py!} +{!../../../docs_src/nosql_databases/tutorial001.py!} ``` We will use this model in our *path operation function*, so, we don't include in it the `hashed_password`. @@ -79,7 +81,7 @@ This will have the data that is actually stored in the database. We don't create it as a subclass of Pydantic's `BaseModel` but as a subclass of our own `User`, because it will have all the attributes in `User` plus a couple more: ```Python hl_lines="32 33 34" -{!./src/nosql_databases/tutorial001.py!} +{!../../../docs_src/nosql_databases/tutorial001.py!} ``` !!! note @@ -99,7 +101,7 @@ Now create a function that will: By creating a function that is only dedicated to getting your user from a `username` (or any other parameter) independent of your *path operation function*, you can more easily re-use it in multiple parts and also add unit tests for it: ```Python hl_lines="37 38 39 40 41 42 43" -{!./src/nosql_databases/tutorial001.py!} +{!../../../docs_src/nosql_databases/tutorial001.py!} ``` ### f-strings @@ -134,7 +136,7 @@ UserInDB(username="johndoe", hashed_password="some_hash") ### Create the `FastAPI` app ```Python hl_lines="47" -{!./src/nosql_databases/tutorial001.py!} +{!../../../docs_src/nosql_databases/tutorial001.py!} ``` ### Create the *path operation function* @@ -144,7 +146,7 @@ As our code is calling Couchbase and we are not using the threads", so, we can get just get the bucket directly and pass it to our utility functions: ```Python hl_lines="50 51 52 53 54" -{!./src/nosql_databases/tutorial001.py!} +{!../../../docs_src/nosql_databases/tutorial001.py!} ``` ## Recap diff --git a/docs/advanced/openapi-callbacks.md b/docs/en/docs/advanced/openapi-callbacks.md similarity index 97% rename from docs/advanced/openapi-callbacks.md rename to docs/en/docs/advanced/openapi-callbacks.md index 8d3b0ac9e87d8..15c0ba2a1d45d 100644 --- a/docs/advanced/openapi-callbacks.md +++ b/docs/en/docs/advanced/openapi-callbacks.md @@ -1,3 +1,5 @@ +# 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). @@ -30,7 +32,7 @@ It will have a *path operation* that will receive an `Invoice` body, and a query This part is pretty normal, most of the code is probably already familiar to you: ```Python hl_lines="8 9 10 11 12 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53" -{!./src/openapi_callbacks/tutorial001.py!} +{!../../../docs_src/openapi_callbacks/tutorial001.py!} ``` !!! tip @@ -91,7 +93,7 @@ Because of that, you need to declare what will be the `default_response_class`, But as we are never calling `app.include_router(some_router)`, we need to set the `default_response_class` during creation of the `APIRouter`. ```Python hl_lines="3 24" -{!./src/openapi_callbacks/tutorial001.py!} +{!../../../docs_src/openapi_callbacks/tutorial001.py!} ``` ### Create the callback *path operation* @@ -104,7 +106,7 @@ It should look just like a normal FastAPI *path operation*: * And it could also have a declaration of the response it should return, e.g. `response_model=InvoiceEventReceived`. ```Python hl_lines="15 16 17 20 21 27 28 29 30 31" -{!./src/openapi_callbacks/tutorial001.py!} +{!../../../docs_src/openapi_callbacks/tutorial001.py!} ``` There are 2 main differences from a normal *path operation*: @@ -171,7 +173,7 @@ At this point you have the *callback path operation(s)* needed (the one(s) that 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="34" -{!./src/openapi_callbacks/tutorial001.py!} +{!../../../docs_src/openapi_callbacks/tutorial001.py!} ``` !!! tip diff --git a/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md similarity index 82% rename from docs/advanced/path-operation-advanced-configuration.md rename to docs/en/docs/advanced/path-operation-advanced-configuration.md index eb5cc6a120075..7a3247a64ab4d 100644 --- a/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -1,3 +1,5 @@ +# Path Operation Advanced Configuration + ## OpenAPI operationId !!! warning @@ -8,7 +10,7 @@ You can set the OpenAPI `operationId` to be used in your *path operation* with t You would have to make sure that it is unique for each operation. ```Python hl_lines="6" -{!./src/path_operation_advanced_configuration/tutorial001.py!} +{!../../../docs_src/path_operation_advanced_configuration/tutorial001.py!} ``` ### Using the *path operation function* name as the operationId @@ -18,7 +20,7 @@ If you want to use your APIs' function names as `operationId`s, you can iterate You should do it after adding all your *path operations*. ```Python hl_lines="2 12 13 14 15 16 17 18 19 20 21 24" -{!./src/path_operation_advanced_configuration/tutorial002.py!} +{!../../../docs_src/path_operation_advanced_configuration/tutorial002.py!} ``` !!! tip @@ -34,7 +36,7 @@ You should do it after adding all your *path operations*. 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" -{!./src/path_operation_advanced_configuration/tutorial003.py!} +{!../../../docs_src/path_operation_advanced_configuration/tutorial003.py!} ``` ## Advanced description from docstring @@ -46,5 +48,5 @@ Adding an `\f` (an escaped "form feed" character) causes **FastAPI** to truncate 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 20 21 22 23 24 25 26 27 28 29" -{!./src/path_operation_advanced_configuration/tutorial004.py!} +{!../../../docs_src/path_operation_advanced_configuration/tutorial004.py!} ``` diff --git a/docs/advanced/response-change-status-code.md b/docs/en/docs/advanced/response-change-status-code.md similarity index 93% rename from docs/advanced/response-change-status-code.md rename to docs/en/docs/advanced/response-change-status-code.md index 1f10e12da9133..979cef3f05367 100644 --- a/docs/advanced/response-change-status-code.md +++ b/docs/en/docs/advanced/response-change-status-code.md @@ -1,3 +1,5 @@ +# Response - Change Status Code + You probably read before that you can set a default [Response Status Code](../tutorial/response-status-code.md){.internal-link target=_blank}. But in some cases you need to return a different status code than the default. @@ -19,7 +21,7 @@ You can declare a parameter of type `Response` in your *path operation function* And then you can set the `status_code` in that *temporal* response object. ```Python hl_lines="1 9 12" -{!./src/response_change_status_code/tutorial001.py!} +{!../../../docs_src/response_change_status_code/tutorial001.py!} ``` And then you can return any object you need, as you normally would (a `dict`, a database model, etc). diff --git a/docs/advanced/response-cookies.md b/docs/en/docs/advanced/response-cookies.md similarity index 94% rename from docs/advanced/response-cookies.md rename to docs/en/docs/advanced/response-cookies.md index 8ce07bbf35c92..e46ae755f79d9 100644 --- a/docs/advanced/response-cookies.md +++ b/docs/en/docs/advanced/response-cookies.md @@ -1,3 +1,5 @@ +# Response Cookies + ## Use a `Response` parameter You can declare a parameter of type `Response` in your *path operation function*. @@ -5,7 +7,7 @@ You can declare a parameter of type `Response` in your *path operation function* And then you can set cookies in that *temporal* response object. ```Python hl_lines="1 8 9" -{!./src/response_cookies/tutorial002.py!} +{!../../../docs_src/response_cookies/tutorial002.py!} ``` And then you can return any object you need, as you normally would (a `dict`, a database model, etc). @@ -25,7 +27,7 @@ To do that, you can create a response as described in [Return a Response Directl Then set Cookies in it, and then return it: ```Python hl_lines="10 11 12" -{!./src/response_cookies/tutorial001.py!} +{!../../../docs_src/response_cookies/tutorial001.py!} ``` !!! tip diff --git a/docs/advanced/response-directly.md b/docs/en/docs/advanced/response-directly.md similarity index 95% rename from docs/advanced/response-directly.md rename to docs/en/docs/advanced/response-directly.md index 1b123280878ef..f1dca1812b6d5 100644 --- a/docs/advanced/response-directly.md +++ b/docs/en/docs/advanced/response-directly.md @@ -1,3 +1,5 @@ +# Return a Response Directly + When you create a **FastAPI** *path operation* you can normally return any data from it: a `dict`, a `list`, a Pydantic model, a database model, etc. By default, **FastAPI** would automatically convert that return value to JSON using the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}. @@ -30,7 +32,7 @@ For example, you cannot put a Pydantic model in a `JSONResponse` without first c For those cases, you can use the `jsonable_encoder` to convert your data before passing it to a response: ```Python hl_lines="4 6 20 21" -{!./src/response_directly/tutorial001.py!} +{!../../../docs_src/response_directly/tutorial001.py!} ``` !!! note "Technical Details" @@ -49,7 +51,7 @@ Let's say that you want to return an `secrets` to check the username and password: ```Python hl_lines="1 11 12 13" -{!./src/security/tutorial007.py!} +{!../../../docs_src/security/tutorial007.py!} ``` This will ensure that `credentials.username` is `"stanleyjobson"`, and that `credentials.password` is `"swordfish"`. This would be similar to: @@ -101,5 +103,5 @@ That way, using `secrets.compare_digest()` in your application code, it will be After detecting that the credentials are incorrect, return an `HTTPException` with a status code 401 (the same returned when no credentials are provided) and add the header `WWW-Authenticate` to make the browser show the login prompt again: ```Python hl_lines="15 16 17 18 19" -{!./src/security/tutorial007.py!} +{!../../../docs_src/security/tutorial007.py!} ``` diff --git a/docs/advanced/security/index.md b/docs/en/docs/advanced/security/index.md similarity index 95% rename from docs/advanced/security/index.md rename to docs/en/docs/advanced/security/index.md index 560af5a7cc58f..0c94986b5728e 100644 --- a/docs/advanced/security/index.md +++ b/docs/en/docs/advanced/security/index.md @@ -1,3 +1,5 @@ +# 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}. diff --git a/docs/advanced/security/oauth2-scopes.md b/docs/en/docs/advanced/security/oauth2-scopes.md similarity index 97% rename from docs/advanced/security/oauth2-scopes.md rename to docs/en/docs/advanced/security/oauth2-scopes.md index 84c806bf888e4..a7842322137c9 100644 --- a/docs/advanced/security/oauth2-scopes.md +++ b/docs/en/docs/advanced/security/oauth2-scopes.md @@ -1,3 +1,5 @@ +# OAuth2 scopes + You can use OAuth2 scopes directly with **FastAPI**, they are integrated to work seamlessly. This would allow you to have a more fine-grained permission system, following the OAuth2 standard, integrated into your OpenAPI application (and the API docs). @@ -55,7 +57,7 @@ They are normally used to declare specific security permissions, for example: First, let's quickly see the parts that change from the examples in the main **Tutorial - User Guide** for [OAuth2 with Password (and hashing), Bearer with JWT tokens](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. Now using OAuth2 scopes: ```Python hl_lines="2 5 9 13 47 65 106 108 109 110 111 112 113 114 115 116 122 123 124 125 129 130 131 132 133 134 135 140 154" -{!./src/security/tutorial005.py!} +{!../../../docs_src/security/tutorial005.py!} ``` Now let's review those changes step by step. @@ -67,7 +69,7 @@ The first change is that now we are declaring the OAuth2 security scheme with tw The `scopes` parameter receives a `dict` with each scope as a key and the description as the value: ```Python hl_lines="63 64 65 66" -{!./src/security/tutorial005.py!} +{!../../../docs_src/security/tutorial005.py!} ``` Because we are now declaring those scopes, they will show up in the API docs when you log-in/authorize. @@ -92,7 +94,7 @@ And we return the scopes as part of the JWT token. But in your application, for security, you should make sure you only add the scopes that the user is actually able to have, or the ones you have predefined. ```Python hl_lines="155" -{!./src/security/tutorial005.py!} +{!../../../docs_src/security/tutorial005.py!} ``` ## Declare scopes in *path operations* and dependencies @@ -117,7 +119,7 @@ In this case, it requires the scope `me` (it could require more than one scope). We are doing it here to demonstrate how **FastAPI** handles scopes declared at different levels. ```Python hl_lines="5 140 167" -{!./src/security/tutorial005.py!} +{!../../../docs_src/security/tutorial005.py!} ``` !!! info "Technical Details" @@ -142,7 +144,7 @@ We also declare a special parameter of type `SecurityScopes`, imported from `fas This `SecurityScopes` class is similar to `Request` (`Request` was used to get the request object directly). ```Python hl_lines="9 106" -{!./src/security/tutorial005.py!} +{!../../../docs_src/security/tutorial005.py!} ``` ## Use the `scopes` @@ -158,7 +160,7 @@ We create an `HTTPException` that we can re-use (`raise`) later at several point In this exception, we include the scopes required (if any) as a string separated by spaces (using `scope_str`). We put that string containing the scopes in in the `WWW-Authenticate` header (this is part of the spec). ```Python hl_lines="106 108 109 110 111 112 113 114 115 116" -{!./src/security/tutorial005.py!} +{!../../../docs_src/security/tutorial005.py!} ``` ## Verify the `username` and data shape @@ -176,7 +178,7 @@ Instead of, for example, a `dict`, or something else, as it could break the appl We also verify that we have a user with that username, and if not, we raise that same exception we created before. ```Python hl_lines="47 117 118 119 120 121 122 123 124 125 126 127 128" -{!./src/security/tutorial005.py!} +{!../../../docs_src/security/tutorial005.py!} ``` ## Verify the `scopes` @@ -186,7 +188,7 @@ We now verify that all the scopes required, by this dependency and all the depen For this, we use `security_scopes.scopes`, that contains a `list` with all these scopes as `str`. ```Python hl_lines="129 130 131 132 133 134 135" -{!./src/security/tutorial005.py!} +{!../../../docs_src/security/tutorial005.py!} ``` ## Dependency tree and scopes diff --git a/docs/advanced/sql-databases-peewee.md b/docs/en/docs/advanced/sql-databases-peewee.md similarity index 95% rename from docs/advanced/sql-databases-peewee.md rename to docs/en/docs/advanced/sql-databases-peewee.md index ae957bec640c3..7610290066b01 100644 --- a/docs/advanced/sql-databases-peewee.md +++ b/docs/en/docs/advanced/sql-databases-peewee.md @@ -1,3 +1,5 @@ +# 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. @@ -60,7 +62,7 @@ Let's refer to the file `sql_app/database.py`. Let's first check all the normal Peewee code, create a Peewee database: ```Python hl_lines="3 5 22" -{!./src/sql_databases_peewee/sql_app/database.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/database.py!} ``` !!! tip @@ -112,7 +114,7 @@ This might seem a bit complex (and it actually is), you don't really need to com We will create a `PeeweeConnectionState`: ```Python hl_lines="10 11 12 13 14 15 16 17 18 19" -{!./src/sql_databases_peewee/sql_app/database.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/database.py!} ``` This class inherits from a special internal class used by Peewee. @@ -133,7 +135,7 @@ So, we need to do some extra tricks to make it work as if it was just using `thr Now, overwrite the `._state` internal attribute in the Peewee database `db` object using the new `PeeweeConnectionState`: ```Python hl_lines="24" -{!./src/sql_databases_peewee/sql_app/database.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/database.py!} ``` !!! tip @@ -160,7 +162,7 @@ This is the same you would do if you followed the Peewee tutorial and updated th Import `db` from `database` (the file `database.py` from above) and use it here. ```Python hl_lines="3 6 7 8 9 10 11 12 15 16 17 18 19 20 21" -{!./src/sql_databases_peewee/sql_app/models.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/models.py!} ``` !!! tip @@ -188,7 +190,7 @@ Now let's check the file `sql_app/schemas.py`. Create all the same Pydantic models as in the SQLAlchemy tutorial: ```Python hl_lines="16 17 18 21 22 25 26 27 28 29 30 34 35 38 39 42 43 44 45 46 47 48" -{!./src/sql_databases_peewee/sql_app/schemas.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!} ``` !!! tip @@ -213,7 +215,7 @@ But recent versions of Pydantic allow providing a custom class that inherits fro 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 9 10 11 12 13 31 49" -{!./src/sql_databases_peewee/sql_app/schemas.py!} +{!../../../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`. @@ -234,7 +236,7 @@ Now let's see the file `sql_app/crud.py`. 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 17 18 19 20 23 24 27 28 29 30" -{!./src/sql_databases_peewee/sql_app/crud.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/crud.py!} ``` There are some differences with the code for the SQLAlchemy tutorial. @@ -258,7 +260,7 @@ And now in the file `sql_app/main.py` let's integrate and use all the other part In a very simplistic way create the database tables: ```Python hl_lines="9 10 11" -{!./src/sql_databases_peewee/sql_app/main.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} ``` ### Create a dependency @@ -266,7 +268,7 @@ In a very simplistic way create the database tables: 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 24 25 26 27 28 29" -{!./src/sql_databases_peewee/sql_app/main.py!} +{!../../../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. @@ -280,7 +282,7 @@ And then, in each *path operation function* that needs to access the database we 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" -{!./src/sql_databases_peewee/sql_app/main.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} ``` ### Context variable sub-dependency @@ -290,7 +292,7 @@ For all the `contextvars` parts to work, we need to make sure we have an indepen 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 19 20" -{!./src/sql_databases_peewee/sql_app/main.py!} +{!../../../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). @@ -319,7 +321,7 @@ async def reset_db_state(): Now, finally, here's the standard **FastAPI** *path operations* code. ```Python hl_lines="32 33 34 35 36 37 40 41 42 43 46 47 48 49 50 51 52 53 56 57 58 59 60 61 62 65 66 67 68 71 72 73 74 75 76 77 78 79" -{!./src/sql_databases_peewee/sql_app/main.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} ``` ### About `def` vs `async def` @@ -436,31 +438,31 @@ Repeat the same process with the 10 tabs. This time all of them will wait and yo * `sql_app/database.py`: ```Python -{!./src/sql_databases_peewee/sql_app/database.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/database.py!} ``` * `sql_app/models.py`: ```Python -{!./src/sql_databases_peewee/sql_app/models.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/models.py!} ``` * `sql_app/schemas.py`: ```Python -{!./src/sql_databases_peewee/sql_app/schemas.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!} ``` * `sql_app/crud.py`: ```Python -{!./src/sql_databases_peewee/sql_app/crud.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/crud.py!} ``` * `sql_app/main.py`: ```Python -{!./src/sql_databases_peewee/sql_app/main.py!} +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} ``` ## Technical Details diff --git a/docs/advanced/sub-applications-proxy.md b/docs/en/docs/advanced/sub-applications-proxy.md similarity index 94% rename from docs/advanced/sub-applications-proxy.md rename to docs/en/docs/advanced/sub-applications-proxy.md index 333ef9ae2de76..03a7f94460088 100644 --- a/docs/advanced/sub-applications-proxy.md +++ b/docs/en/docs/advanced/sub-applications-proxy.md @@ -1,3 +1,5 @@ +# Sub Applications - Behind a Proxy, Mounts + There are at least two situations where you could need to create your **FastAPI** application using some specific paths. But then you need to set them up to be served with a path prefix. @@ -44,7 +46,7 @@ You could want to do this if you have several "independent" applications that yo First, create the main, top-level, **FastAPI** application, and its *path operations*: ```Python hl_lines="3 6 7 8" -{!./src/sub_applications/tutorial001.py!} +{!../../../docs_src/sub_applications/tutorial001.py!} ``` ### Sub-application @@ -56,7 +58,7 @@ This sub-application is just another standard FastAPI application, but this is t When creating the sub-application, use the parameter `openapi_prefix`. In this case, with a prefix of `/subapi`: ```Python hl_lines="11 14 15 16" -{!./src/sub_applications/tutorial001.py!} +{!../../../docs_src/sub_applications/tutorial001.py!} ``` ### Mount the sub-application @@ -66,7 +68,7 @@ In your top-level application, `app`, mount the sub-application, `subapi`. Here you need to make sure you use the same path that you used for the `openapi_prefix`, in this case, `/subapi`: ```Python hl_lines="11 19" -{!./src/sub_applications/tutorial001.py!} +{!../../../docs_src/sub_applications/tutorial001.py!} ``` ## Check the automatic API docs diff --git a/docs/advanced/templates.md b/docs/en/docs/advanced/templates.md similarity index 91% rename from docs/advanced/templates.md rename to docs/en/docs/advanced/templates.md index da4752e4a598d..871f3163f0622 100644 --- a/docs/advanced/templates.md +++ b/docs/en/docs/advanced/templates.md @@ -1,3 +1,5 @@ +# Templates + You can use any template engine you want with **FastAPI**. A common election is Jinja2, the same one used by Flask and other tools. @@ -38,7 +40,7 @@ $ pip install aiofiles * Use the `templates` you created to render and return a `TemplateResponse`, passing the `request` as one of the key-value pairs in the Jinja2 "context". ```Python hl_lines="3 10 14 15" -{!./src/templates/tutorial001.py!} +{!../../../docs_src/templates/tutorial001.py!} ``` !!! note @@ -54,7 +56,7 @@ $ pip install aiofiles Then you can write a template at `templates/item.html` with: ```jinja hl_lines="7" -{!./src/templates/templates/item.html!} +{!../../../docs_src/templates/templates/item.html!} ``` It will show the `id` taken from the "context" `dict` you passed: @@ -68,13 +70,13 @@ It will show the `id` taken from the "context" `dict` you passed: And you can also use `url_for()` inside of the template, and use it, for example, with the `StaticFiles` you mounted. ```jinja hl_lines="4" -{!./src/templates/templates/item.html!} +{!../../../docs_src/templates/templates/item.html!} ``` In this example, it would link to a CSS file at `static/styles.css` with: ```CSS hl_lines="4" -{!./src/templates/static/styles.css!} +{!../../../docs_src/templates/static/styles.css!} ``` And because you are using `StaticFiles`, that CSS file would be served automatically by your **FastAPI** application at the URL `/static/styles.css`. diff --git a/docs/advanced/testing-dependencies.md b/docs/en/docs/advanced/testing-dependencies.md similarity index 96% rename from docs/advanced/testing-dependencies.md rename to docs/en/docs/advanced/testing-dependencies.md index 50d50618cf154..5ba7e219b9b76 100644 --- a/docs/advanced/testing-dependencies.md +++ b/docs/en/docs/advanced/testing-dependencies.md @@ -1,3 +1,5 @@ +# Testing Dependencies with Overrides + ## Overriding dependencies during testing There are some scenarios where you might want to override a dependency during testing. @@ -39,7 +41,7 @@ To override a dependency for testing, you put as a key the original dependency ( And then **FastAPI** will call that override instead of the original dependency. ```Python hl_lines="24 25 28" -{!./src/dependency_testing/tutorial001.py!} +{!../../../docs_src/dependency_testing/tutorial001.py!} ``` !!! tip diff --git a/docs/advanced/testing-events.md b/docs/en/docs/advanced/testing-events.md similarity index 67% rename from docs/advanced/testing-events.md rename to docs/en/docs/advanced/testing-events.md index a52aabe9870f5..45d8d868654fb 100644 --- a/docs/advanced/testing-events.md +++ b/docs/en/docs/advanced/testing-events.md @@ -1,7 +1,7 @@ -## Testing Events, `startup` and `shutdown` +# 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 10 11 12 20 21 22 23 24" -{!./src/app_testing/tutorial003.py!} -``` \ No newline at end of file +{!../../../docs_src/app_testing/tutorial003.py!} +``` diff --git a/docs/advanced/testing-websockets.md b/docs/en/docs/advanced/testing-websockets.md similarity index 72% rename from docs/advanced/testing-websockets.md rename to docs/en/docs/advanced/testing-websockets.md index 8e9ecf34f8e7e..a9d6821becfd4 100644 --- a/docs/advanced/testing-websockets.md +++ b/docs/en/docs/advanced/testing-websockets.md @@ -1,9 +1,9 @@ -## Testing WebSockets +# 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 28 29 30 31" -{!./src/app_testing/tutorial002.py!} +{!../../../docs_src/app_testing/tutorial002.py!} ``` diff --git a/docs/advanced/using-request-directly.md b/docs/en/docs/advanced/using-request-directly.md similarity index 96% rename from docs/advanced/using-request-directly.md rename to docs/en/docs/advanced/using-request-directly.md index 4d9e84d2c2a42..ac452c25ee343 100644 --- a/docs/advanced/using-request-directly.md +++ b/docs/en/docs/advanced/using-request-directly.md @@ -1,3 +1,5 @@ +# Using the Request Directly + Up to now, you have been declaring the parts of the request that you need with their types. Taking data from: @@ -28,7 +30,7 @@ Let's imagine you want to get the client's IP address/host inside of your *path For that you need to access the request directly. ```Python hl_lines="1 7 8" -{!./src/using_request_directly/tutorial001.py!} +{!../../../docs_src/using_request_directly/tutorial001.py!} ``` By declaring a *path operation function* parameter with the type being the `Request` **FastAPI** will know to pass the `Request` in that parameter. diff --git a/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md similarity index 94% rename from docs/advanced/websockets.md rename to docs/en/docs/advanced/websockets.md index a76eab59c815e..d473cef0705c5 100644 --- a/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -1,3 +1,4 @@ +# WebSockets You can use WebSockets with **FastAPI**. @@ -24,7 +25,7 @@ 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 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 41 42 43" -{!./src/websockets/tutorial001.py!} +{!../../../docs_src/websockets/tutorial001.py!} ``` ## Create a `websocket` @@ -32,7 +33,7 @@ But it's the simplest way to focus on the server-side of WebSockets and have a w In your **FastAPI** application, create a `websocket`: ```Python hl_lines="1 46 47" -{!./src/websockets/tutorial001.py!} +{!../../../docs_src/websockets/tutorial001.py!} ``` !!! note "Technical Details" @@ -45,7 +46,7 @@ In your **FastAPI** application, create a `websocket`: In your WebSocket route you can `await` for messages and send messages. ```Python hl_lines="48 49 50 51 52" -{!./src/websockets/tutorial001.py!} +{!../../../docs_src/websockets/tutorial001.py!} ``` You can receive and send binary, text, and JSON data. @@ -64,7 +65,7 @@ In WebSocket endpoints you can import from `fastapi` and use: They work the same way as for other FastAPI endpoints/*path operations*: ```Python hl_lines="53 54 55 56 57 58 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76" -{!./src/websockets/tutorial002.py!} +{!../../../docs_src/websockets/tutorial002.py!} ``` !!! info diff --git a/docs/advanced/wsgi.md b/docs/en/docs/advanced/wsgi.md similarity index 92% rename from docs/advanced/wsgi.md rename to docs/en/docs/advanced/wsgi.md index 7d2edfc55a6c5..4b81aff2e77fa 100644 --- a/docs/advanced/wsgi.md +++ b/docs/en/docs/advanced/wsgi.md @@ -1,3 +1,5 @@ +# Including WSGI - Flask, Django, others + You can mount WSGI applications as you saw with [Sub Applications - Behind a Proxy, Mounts](./sub-applications-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. @@ -11,7 +13,7 @@ Then wrap the WSGI (e.g. Flask) app with the middleware. And then mount that under a path. ```Python hl_lines="1 3 22" -{!./src/wsgi/tutorial001.py!} +{!../../../docs_src/wsgi/tutorial001.py!} ``` ## Check it diff --git a/docs/alternatives.md b/docs/en/docs/alternatives.md similarity index 99% rename from docs/alternatives.md rename to docs/en/docs/alternatives.md index 730c1079450f0..3d9e3a55af90f 100644 --- a/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -1,3 +1,5 @@ +# Alternatives, Inspiration and Comparisons + What inspired **FastAPI**, how it compares to other alternatives and what it learned from them. ## Intro diff --git a/docs/async.md b/docs/en/docs/async.md similarity index 99% rename from docs/async.md rename to docs/en/docs/async.md index 24c2f61d2ef9a..dc17e5172c1cf 100644 --- a/docs/async.md +++ b/docs/en/docs/async.md @@ -1,3 +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. ## In a hurry? diff --git a/docs/benchmarks.md b/docs/en/docs/benchmarks.md similarity index 99% rename from docs/benchmarks.md rename to docs/en/docs/benchmarks.md index 95efcab531766..5223df81d6c97 100644 --- a/docs/benchmarks.md +++ b/docs/en/docs/benchmarks.md @@ -1,3 +1,5 @@ +# Benchmarks + Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) But when checking benchmarks and comparisons you should have the following in mind. diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md new file mode 100644 index 0000000000000..a08991a1579a1 --- /dev/null +++ b/docs/en/docs/contributing.md @@ -0,0 +1,441 @@ +# 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: + +
+ +```console +$ python -m venv env +``` + +
+ +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: + +
+ +```console +$ source ./env/bin/activate +``` + +
+ +Or in Windows' PowerShell: + +
+ +```console +$ .\env\Scripts\Activate.ps1 +``` + +
+ +Or if you use Bash for Windows (e.g. Git Bash): + +
+ +```console +$ source ./env/Scripts/activate +``` + +
+ +To check it worked, use: + +
+ +```console +$ which pip + +some/directory/fastapi/env/bin/pip +``` + +
+ +If it shows the `pip` binary at `env/bin/pip` then it worked. 🎉 + +Or in Windows PowerShell: + +
+ +```console +$ Get-Command pip + +some/directory/fastapi/env/bin/pip +``` + +
+ +!!! 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 Flit to build, package and publish the project. + +After activating the environment as described above, install `flit`: + +
+ +```console +$ pip install flit + +---> 100% +``` + +
+ +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: + +
+ +```console +$ flit install --deps develop --symlink + +---> 100% +``` + +
+ +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`, 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: + +
+ +```console +$ bash scripts/format.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: + +
+ +```console +$ flit install --symlink + +---> 100% +``` + +
+ +### Format imports + +There is another script that formats all the imports and makes sure you don't have unused imports: + +
+ +```console +$ bash scripts/format-imports.sh +``` + +
+ +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 MkDocs. + +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: + +
+ +```console +$ python ./scripts/docs.py live + +[INFO] Serving on http://0.0.0.0:8008 +[INFO] Start watching changes +[INFO] Start detecting changes +``` + +
+ +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. + +#### 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 Typer CLI, and you will get autocompletion in your terminal for the commands after installing completion. + +If you install Typer CLI, you can install completion with: + +
+ +```console +$ typer --install-completion + +zsh completion installed in /home/user/.bashrc. +Completion will take effect once you restart the terminal. +``` + +
+ +### Apps and docs at the same time + +If you run the examples with, e.g.: + +
+ +```console +$ uvicorn tutorial001:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +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 + +✨ 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. + +✨ To check the 2-letter code for the language you want to translate you can use the table List of ISO 639-1 codes. + +#### 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: + +
+ +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +[INFO] Serving on http://0.0.0.0:8008 +[INFO] Start watching changes +[INFO] Start detecting changes +``` + +
+ +Now you can go to http://0.0.0.0:8008 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 are not translated and have a notification about the the translation is missing. + +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 [Advanced User Guide: Extending OpenAPI](advanced/extending-openapi.md){.internal-link target=_blank}. + +* Copy the file at: + +``` +docs/en/docs/advanced/extending-openapi.md +``` + +* Paste it in exactly the same location but for the language you want to translate, e.g.: + +``` +docs/es/docs/advanced/extending-openapi.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 at: + +``` +docs/en/docs/mkdocs.yml +``` + +* Find the place where that `advanced/extending-openapi.md` is located in the config file. Somewhere like: + +```YAML hl_lines="9" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +# More stuff +- Advanced User Guide: + # More stuff + - advanced/testing-dependencies.md + - advanced/extending-openapi.md + - advanced/openapi-callbacks.md +``` + +* Open the MkDocs config file for the language you are editing, e.g.: + +``` +docs/es/docs/mkdocs.yml +``` + +* Add the equivalent structure code and add it at the exact same location it would be, e.g.: + +```YAML hl_lines="6 9" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +# More stuff +- Guía de Usuario Avanzada: + # More stuff + - advanced/testing-dependencies.md + - advanced/extending-openapi.md + - advanced/openapi-callbacks.md +``` + +Notice that the `Advanced User Guide` is translated to `Guía de Usuario Avanzada`. + +Also, make sure that if there are other entries, the new entry with your translation is in 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: + +
+ +```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 +``` + +
+ +Now you can check in your code editor the newly created directory `docs/ht/`. + +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. + +## Tests + +There is a script that you can run locally to test all the code and generate coverage reports in HTML: + +
+ +```console +$ 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. + +### Tests in your editor + +If you want to use the integrated tests in your editor add `./docs/src` to your `PYTHONPATH` variable. + +For example, in VS Code you can create a file `.env` with: + +```env +PYTHONPATH=./docs/src +``` diff --git a/docs/css/custom.css b/docs/en/docs/css/custom.css similarity index 100% rename from docs/css/custom.css rename to docs/en/docs/css/custom.css diff --git a/docs/css/termynal.css b/docs/en/docs/css/termynal.css similarity index 100% rename from docs/css/termynal.css rename to docs/en/docs/css/termynal.css diff --git a/docs/deployment.md b/docs/en/docs/deployment.md similarity index 99% rename from docs/deployment.md rename to docs/en/docs/deployment.md index 8d4c6fcdba464..8ca7c433ff6af 100644 --- a/docs/deployment.md +++ b/docs/en/docs/deployment.md @@ -1,3 +1,5 @@ +# Deployment + 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. diff --git a/docs/external-links.md b/docs/en/docs/external-links.md similarity index 99% rename from docs/external-links.md rename to docs/en/docs/external-links.md index c222e1247fcbc..4546befd042c5 100644 --- a/docs/external-links.md +++ b/docs/en/docs/external-links.md @@ -1,3 +1,5 @@ +# External Links and Articles + **FastAPI** has a great community constantly growing. There are many posts, articles, tools, and projects, related to **FastAPI**. diff --git a/docs/features.md b/docs/en/docs/features.md similarity index 96% rename from docs/features.md rename to docs/en/docs/features.md index 69c62cbced1f6..8b5bb51f171ea 100644 --- a/docs/features.md +++ b/docs/en/docs/features.md @@ -1,3 +1,4 @@ +# Features ## FastAPI features @@ -16,11 +17,11 @@ Interactive API documentation and exploration web user interfaces. As the framew * Swagger UI, with interactive exploration, call and test your API directly from the browser. -![Swagger UI interaction](img/index/index-03-swagger-02.png) +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) * Alternative API documentation with ReDoc. -![ReDoc](img/index/index-06-redoc-02.png) +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) ### Just Modern Python @@ -82,11 +83,11 @@ Here's how your editor might help you: * in Visual Studio Code: -![editor support](img/vscode-completion.png) +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) * in PyCharm: -![editor support](img/pycharm-completion.png) +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) You will get completion in code you might even consider impossible before. As for example, the `price` key inside a JSON body (that could have been nested) that comes from a request. diff --git a/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md similarity index 99% rename from docs/help-fastapi.md rename to docs/en/docs/help-fastapi.md index 1370a5770eb9f..f6965658302bd 100644 --- a/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -1,3 +1,5 @@ +# Help FastAPI - Get Help + Do you like **FastAPI**? Would you like to help FastAPI, other users, and the author? diff --git a/docs/history-design-future.md b/docs/en/docs/history-design-future.md similarity index 99% rename from docs/history-design-future.md rename to docs/en/docs/history-design-future.md index 08067e3c201bc..9db1027c26c2a 100644 --- a/docs/history-design-future.md +++ b/docs/en/docs/history-design-future.md @@ -1,3 +1,5 @@ +# History, Design and Future + Some time ago, a **FastAPI** user asked: > What’s the history of this project? It seems to have come from nowhere to awesome in a few weeks [...] diff --git a/docs/img/favicon.png b/docs/en/docs/img/favicon.png similarity index 100% rename from docs/img/favicon.png rename to docs/en/docs/img/favicon.png diff --git a/docs/img/github-social-preview.png b/docs/en/docs/img/github-social-preview.png similarity index 100% rename from docs/img/github-social-preview.png rename to docs/en/docs/img/github-social-preview.png diff --git a/docs/img/github-social-preview.svg b/docs/en/docs/img/github-social-preview.svg similarity index 100% rename from docs/img/github-social-preview.svg rename to docs/en/docs/img/github-social-preview.svg diff --git a/docs/img/icon-transparent-bg.png b/docs/en/docs/img/icon-transparent-bg.png similarity index 100% rename from docs/img/icon-transparent-bg.png rename to docs/en/docs/img/icon-transparent-bg.png diff --git a/docs/img/icon-white-bg.png b/docs/en/docs/img/icon-white-bg.png similarity index 100% rename from docs/img/icon-white-bg.png rename to docs/en/docs/img/icon-white-bg.png diff --git a/docs/img/icon-white.svg b/docs/en/docs/img/icon-white.svg similarity index 100% rename from docs/img/icon-white.svg rename to docs/en/docs/img/icon-white.svg diff --git a/docs/img/index/index-01-swagger-ui-simple.png b/docs/en/docs/img/index/index-01-swagger-ui-simple.png similarity index 100% rename from docs/img/index/index-01-swagger-ui-simple.png rename to docs/en/docs/img/index/index-01-swagger-ui-simple.png diff --git a/docs/img/index/index-02-redoc-simple.png b/docs/en/docs/img/index/index-02-redoc-simple.png similarity index 100% rename from docs/img/index/index-02-redoc-simple.png rename to docs/en/docs/img/index/index-02-redoc-simple.png diff --git a/docs/img/index/index-03-swagger-02.png b/docs/en/docs/img/index/index-03-swagger-02.png similarity index 100% rename from docs/img/index/index-03-swagger-02.png rename to docs/en/docs/img/index/index-03-swagger-02.png diff --git a/docs/img/index/index-04-swagger-03.png b/docs/en/docs/img/index/index-04-swagger-03.png similarity index 100% rename from docs/img/index/index-04-swagger-03.png rename to docs/en/docs/img/index/index-04-swagger-03.png diff --git a/docs/img/index/index-05-swagger-04.png b/docs/en/docs/img/index/index-05-swagger-04.png similarity index 100% rename from docs/img/index/index-05-swagger-04.png rename to docs/en/docs/img/index/index-05-swagger-04.png diff --git a/docs/img/index/index-06-redoc-02.png b/docs/en/docs/img/index/index-06-redoc-02.png similarity index 100% rename from docs/img/index/index-06-redoc-02.png rename to docs/en/docs/img/index/index-06-redoc-02.png diff --git a/docs/img/logo-margin/logo-teal-vector.svg b/docs/en/docs/img/logo-margin/logo-teal-vector.svg similarity index 100% rename from docs/img/logo-margin/logo-teal-vector.svg rename to docs/en/docs/img/logo-margin/logo-teal-vector.svg diff --git a/docs/img/logo-margin/logo-teal.png b/docs/en/docs/img/logo-margin/logo-teal.png similarity index 100% rename from docs/img/logo-margin/logo-teal.png rename to docs/en/docs/img/logo-margin/logo-teal.png diff --git a/docs/img/logo-margin/logo-teal.svg b/docs/en/docs/img/logo-margin/logo-teal.svg similarity index 100% rename from docs/img/logo-margin/logo-teal.svg rename to docs/en/docs/img/logo-margin/logo-teal.svg diff --git a/docs/img/logo-margin/logo-white-bg.png b/docs/en/docs/img/logo-margin/logo-white-bg.png similarity index 100% rename from docs/img/logo-margin/logo-white-bg.png rename to docs/en/docs/img/logo-margin/logo-white-bg.png diff --git a/docs/img/logo-teal-vector.svg b/docs/en/docs/img/logo-teal-vector.svg similarity index 100% rename from docs/img/logo-teal-vector.svg rename to docs/en/docs/img/logo-teal-vector.svg diff --git a/docs/img/logo-teal.svg b/docs/en/docs/img/logo-teal.svg similarity index 100% rename from docs/img/logo-teal.svg rename to docs/en/docs/img/logo-teal.svg diff --git a/docs/img/pycharm-completion.png b/docs/en/docs/img/pycharm-completion.png similarity index 100% rename from docs/img/pycharm-completion.png rename to docs/en/docs/img/pycharm-completion.png diff --git a/docs/img/python-types/image01.png b/docs/en/docs/img/python-types/image01.png similarity index 100% rename from docs/img/python-types/image01.png rename to docs/en/docs/img/python-types/image01.png diff --git a/docs/img/python-types/image02.png b/docs/en/docs/img/python-types/image02.png similarity index 100% rename from docs/img/python-types/image02.png rename to docs/en/docs/img/python-types/image02.png diff --git a/docs/img/python-types/image03.png b/docs/en/docs/img/python-types/image03.png similarity index 100% rename from docs/img/python-types/image03.png rename to docs/en/docs/img/python-types/image03.png diff --git a/docs/img/python-types/image04.png b/docs/en/docs/img/python-types/image04.png similarity index 100% rename from docs/img/python-types/image04.png rename to docs/en/docs/img/python-types/image04.png diff --git a/docs/img/python-types/image05.png b/docs/en/docs/img/python-types/image05.png similarity index 100% rename from docs/img/python-types/image05.png rename to docs/en/docs/img/python-types/image05.png diff --git a/docs/img/python-types/image06.png b/docs/en/docs/img/python-types/image06.png similarity index 100% rename from docs/img/python-types/image06.png rename to docs/en/docs/img/python-types/image06.png diff --git a/docs/img/tutorial/additional-responses/image01.png b/docs/en/docs/img/tutorial/additional-responses/image01.png similarity index 100% rename from docs/img/tutorial/additional-responses/image01.png rename to docs/en/docs/img/tutorial/additional-responses/image01.png diff --git a/docs/img/tutorial/application-configuration/image01.png b/docs/en/docs/img/tutorial/application-configuration/image01.png similarity index 100% rename from docs/img/tutorial/application-configuration/image01.png rename to docs/en/docs/img/tutorial/application-configuration/image01.png diff --git a/docs/img/tutorial/async-sql-databases/image01.png b/docs/en/docs/img/tutorial/async-sql-databases/image01.png similarity index 100% rename from docs/img/tutorial/async-sql-databases/image01.png rename to docs/en/docs/img/tutorial/async-sql-databases/image01.png diff --git a/docs/img/tutorial/bigger-applications/image01.png b/docs/en/docs/img/tutorial/bigger-applications/image01.png similarity index 100% rename from docs/img/tutorial/bigger-applications/image01.png rename to docs/en/docs/img/tutorial/bigger-applications/image01.png diff --git a/docs/img/tutorial/body-fields/image01.png b/docs/en/docs/img/tutorial/body-fields/image01.png similarity index 100% rename from docs/img/tutorial/body-fields/image01.png rename to docs/en/docs/img/tutorial/body-fields/image01.png diff --git a/docs/img/tutorial/body-nested-models/image01.png b/docs/en/docs/img/tutorial/body-nested-models/image01.png similarity index 100% rename from docs/img/tutorial/body-nested-models/image01.png rename to docs/en/docs/img/tutorial/body-nested-models/image01.png diff --git a/docs/img/tutorial/body/image01.png b/docs/en/docs/img/tutorial/body/image01.png similarity index 100% rename from docs/img/tutorial/body/image01.png rename to docs/en/docs/img/tutorial/body/image01.png diff --git a/docs/img/tutorial/body/image02.png b/docs/en/docs/img/tutorial/body/image02.png similarity index 100% rename from docs/img/tutorial/body/image02.png rename to docs/en/docs/img/tutorial/body/image02.png diff --git a/docs/img/tutorial/body/image03.png b/docs/en/docs/img/tutorial/body/image03.png similarity index 100% rename from docs/img/tutorial/body/image03.png rename to docs/en/docs/img/tutorial/body/image03.png diff --git a/docs/img/tutorial/body/image04.png b/docs/en/docs/img/tutorial/body/image04.png similarity index 100% rename from docs/img/tutorial/body/image04.png rename to docs/en/docs/img/tutorial/body/image04.png diff --git a/docs/img/tutorial/body/image05.png b/docs/en/docs/img/tutorial/body/image05.png similarity index 100% rename from docs/img/tutorial/body/image05.png rename to docs/en/docs/img/tutorial/body/image05.png diff --git a/docs/img/tutorial/custom-response/image01.png b/docs/en/docs/img/tutorial/custom-response/image01.png similarity index 100% rename from docs/img/tutorial/custom-response/image01.png rename to docs/en/docs/img/tutorial/custom-response/image01.png diff --git a/docs/img/tutorial/debugging/image01.png b/docs/en/docs/img/tutorial/debugging/image01.png similarity index 100% rename from docs/img/tutorial/debugging/image01.png rename to docs/en/docs/img/tutorial/debugging/image01.png diff --git a/docs/img/tutorial/dependencies/image01.png b/docs/en/docs/img/tutorial/dependencies/image01.png similarity index 100% rename from docs/img/tutorial/dependencies/image01.png rename to docs/en/docs/img/tutorial/dependencies/image01.png diff --git a/docs/img/tutorial/dependencies/image02.png b/docs/en/docs/img/tutorial/dependencies/image02.png similarity index 100% rename from docs/img/tutorial/dependencies/image02.png rename to docs/en/docs/img/tutorial/dependencies/image02.png diff --git a/docs/img/tutorial/extending-openapi/image01.png b/docs/en/docs/img/tutorial/extending-openapi/image01.png similarity index 100% rename from docs/img/tutorial/extending-openapi/image01.png rename to docs/en/docs/img/tutorial/extending-openapi/image01.png diff --git a/docs/img/tutorial/graphql/image01.png b/docs/en/docs/img/tutorial/graphql/image01.png similarity index 100% rename from docs/img/tutorial/graphql/image01.png rename to docs/en/docs/img/tutorial/graphql/image01.png diff --git a/docs/img/tutorial/openapi-callbacks/image01.png b/docs/en/docs/img/tutorial/openapi-callbacks/image01.png similarity index 100% rename from docs/img/tutorial/openapi-callbacks/image01.png rename to docs/en/docs/img/tutorial/openapi-callbacks/image01.png diff --git a/docs/img/tutorial/path-operation-configuration/image01.png b/docs/en/docs/img/tutorial/path-operation-configuration/image01.png similarity index 100% rename from docs/img/tutorial/path-operation-configuration/image01.png rename to docs/en/docs/img/tutorial/path-operation-configuration/image01.png diff --git a/docs/img/tutorial/path-operation-configuration/image02.png b/docs/en/docs/img/tutorial/path-operation-configuration/image02.png similarity index 100% rename from docs/img/tutorial/path-operation-configuration/image02.png rename to docs/en/docs/img/tutorial/path-operation-configuration/image02.png diff --git a/docs/img/tutorial/path-operation-configuration/image03.png b/docs/en/docs/img/tutorial/path-operation-configuration/image03.png similarity index 100% rename from docs/img/tutorial/path-operation-configuration/image03.png rename to docs/en/docs/img/tutorial/path-operation-configuration/image03.png diff --git a/docs/img/tutorial/path-operation-configuration/image04.png b/docs/en/docs/img/tutorial/path-operation-configuration/image04.png similarity index 100% rename from docs/img/tutorial/path-operation-configuration/image04.png rename to docs/en/docs/img/tutorial/path-operation-configuration/image04.png diff --git a/docs/img/tutorial/path-operation-configuration/image05.png b/docs/en/docs/img/tutorial/path-operation-configuration/image05.png similarity index 100% rename from docs/img/tutorial/path-operation-configuration/image05.png rename to docs/en/docs/img/tutorial/path-operation-configuration/image05.png diff --git a/docs/img/tutorial/path-params/image01.png b/docs/en/docs/img/tutorial/path-params/image01.png similarity index 100% rename from docs/img/tutorial/path-params/image01.png rename to docs/en/docs/img/tutorial/path-params/image01.png diff --git a/docs/img/tutorial/path-params/image02.png b/docs/en/docs/img/tutorial/path-params/image02.png similarity index 100% rename from docs/img/tutorial/path-params/image02.png rename to docs/en/docs/img/tutorial/path-params/image02.png diff --git a/docs/img/tutorial/path-params/image03.png b/docs/en/docs/img/tutorial/path-params/image03.png similarity index 100% rename from docs/img/tutorial/path-params/image03.png rename to docs/en/docs/img/tutorial/path-params/image03.png diff --git a/docs/img/tutorial/query-params-str-validations/image01.png b/docs/en/docs/img/tutorial/query-params-str-validations/image01.png similarity index 100% rename from docs/img/tutorial/query-params-str-validations/image01.png rename to docs/en/docs/img/tutorial/query-params-str-validations/image01.png diff --git a/docs/img/tutorial/query-params-str-validations/image02.png b/docs/en/docs/img/tutorial/query-params-str-validations/image02.png similarity index 100% rename from docs/img/tutorial/query-params-str-validations/image02.png rename to docs/en/docs/img/tutorial/query-params-str-validations/image02.png diff --git a/docs/img/tutorial/response-model/image01.png b/docs/en/docs/img/tutorial/response-model/image01.png similarity index 100% rename from docs/img/tutorial/response-model/image01.png rename to docs/en/docs/img/tutorial/response-model/image01.png diff --git a/docs/img/tutorial/response-model/image02.png b/docs/en/docs/img/tutorial/response-model/image02.png similarity index 100% rename from docs/img/tutorial/response-model/image02.png rename to docs/en/docs/img/tutorial/response-model/image02.png diff --git a/docs/img/tutorial/response-status-code/image01.png b/docs/en/docs/img/tutorial/response-status-code/image01.png similarity index 100% rename from docs/img/tutorial/response-status-code/image01.png rename to docs/en/docs/img/tutorial/response-status-code/image01.png diff --git a/docs/img/tutorial/response-status-code/image02.png b/docs/en/docs/img/tutorial/response-status-code/image02.png similarity index 100% rename from docs/img/tutorial/response-status-code/image02.png rename to docs/en/docs/img/tutorial/response-status-code/image02.png diff --git a/docs/img/tutorial/security/image01.png b/docs/en/docs/img/tutorial/security/image01.png similarity index 100% rename from docs/img/tutorial/security/image01.png rename to docs/en/docs/img/tutorial/security/image01.png diff --git a/docs/img/tutorial/security/image02.png b/docs/en/docs/img/tutorial/security/image02.png similarity index 100% rename from docs/img/tutorial/security/image02.png rename to docs/en/docs/img/tutorial/security/image02.png diff --git a/docs/img/tutorial/security/image03.png b/docs/en/docs/img/tutorial/security/image03.png similarity index 100% rename from docs/img/tutorial/security/image03.png rename to docs/en/docs/img/tutorial/security/image03.png diff --git a/docs/img/tutorial/security/image04.png b/docs/en/docs/img/tutorial/security/image04.png similarity index 100% rename from docs/img/tutorial/security/image04.png rename to docs/en/docs/img/tutorial/security/image04.png diff --git a/docs/img/tutorial/security/image05.png b/docs/en/docs/img/tutorial/security/image05.png similarity index 100% rename from docs/img/tutorial/security/image05.png rename to docs/en/docs/img/tutorial/security/image05.png diff --git a/docs/img/tutorial/security/image06.png b/docs/en/docs/img/tutorial/security/image06.png similarity index 100% rename from docs/img/tutorial/security/image06.png rename to docs/en/docs/img/tutorial/security/image06.png diff --git a/docs/img/tutorial/security/image07.png b/docs/en/docs/img/tutorial/security/image07.png similarity index 100% rename from docs/img/tutorial/security/image07.png rename to docs/en/docs/img/tutorial/security/image07.png diff --git a/docs/img/tutorial/security/image08.png b/docs/en/docs/img/tutorial/security/image08.png similarity index 100% rename from docs/img/tutorial/security/image08.png rename to docs/en/docs/img/tutorial/security/image08.png diff --git a/docs/img/tutorial/security/image09.png b/docs/en/docs/img/tutorial/security/image09.png similarity index 100% rename from docs/img/tutorial/security/image09.png rename to docs/en/docs/img/tutorial/security/image09.png diff --git a/docs/img/tutorial/security/image10.png b/docs/en/docs/img/tutorial/security/image10.png similarity index 100% rename from docs/img/tutorial/security/image10.png rename to docs/en/docs/img/tutorial/security/image10.png diff --git a/docs/img/tutorial/security/image11.png b/docs/en/docs/img/tutorial/security/image11.png similarity index 100% rename from docs/img/tutorial/security/image11.png rename to docs/en/docs/img/tutorial/security/image11.png diff --git a/docs/img/tutorial/security/image12.png b/docs/en/docs/img/tutorial/security/image12.png similarity index 100% rename from docs/img/tutorial/security/image12.png rename to docs/en/docs/img/tutorial/security/image12.png diff --git a/docs/img/tutorial/sql-databases/image01.png b/docs/en/docs/img/tutorial/sql-databases/image01.png similarity index 100% rename from docs/img/tutorial/sql-databases/image01.png rename to docs/en/docs/img/tutorial/sql-databases/image01.png diff --git a/docs/img/tutorial/sql-databases/image02.png b/docs/en/docs/img/tutorial/sql-databases/image02.png similarity index 100% rename from docs/img/tutorial/sql-databases/image02.png rename to docs/en/docs/img/tutorial/sql-databases/image02.png diff --git a/docs/img/tutorial/sub-applications/image01.png b/docs/en/docs/img/tutorial/sub-applications/image01.png similarity index 100% rename from docs/img/tutorial/sub-applications/image01.png rename to docs/en/docs/img/tutorial/sub-applications/image01.png diff --git a/docs/img/tutorial/sub-applications/image02.png b/docs/en/docs/img/tutorial/sub-applications/image02.png similarity index 100% rename from docs/img/tutorial/sub-applications/image02.png rename to docs/en/docs/img/tutorial/sub-applications/image02.png diff --git a/docs/img/tutorial/websockets/image01.png b/docs/en/docs/img/tutorial/websockets/image01.png similarity index 100% rename from docs/img/tutorial/websockets/image01.png rename to docs/en/docs/img/tutorial/websockets/image01.png diff --git a/docs/img/tutorial/websockets/image02.png b/docs/en/docs/img/tutorial/websockets/image02.png similarity index 100% rename from docs/img/tutorial/websockets/image02.png rename to docs/en/docs/img/tutorial/websockets/image02.png diff --git a/docs/img/tutorial/websockets/image03.png b/docs/en/docs/img/tutorial/websockets/image03.png similarity index 100% rename from docs/img/tutorial/websockets/image03.png rename to docs/en/docs/img/tutorial/websockets/image03.png diff --git a/docs/img/tutorial/websockets/image04.png b/docs/en/docs/img/tutorial/websockets/image04.png similarity index 100% rename from docs/img/tutorial/websockets/image04.png rename to docs/en/docs/img/tutorial/websockets/image04.png diff --git a/docs/img/vscode-completion.png b/docs/en/docs/img/vscode-completion.png similarity index 100% rename from docs/img/vscode-completion.png rename to docs/en/docs/img/vscode-completion.png diff --git a/docs/index.md b/docs/en/docs/index.md similarity index 100% rename from docs/index.md rename to docs/en/docs/index.md diff --git a/docs/js/custom.js b/docs/en/docs/js/custom.js similarity index 100% rename from docs/js/custom.js rename to docs/en/docs/js/custom.js diff --git a/docs/js/termynal.js b/docs/en/docs/js/termynal.js similarity index 100% rename from docs/js/termynal.js rename to docs/en/docs/js/termynal.js diff --git a/docs/project-generation.md b/docs/en/docs/project-generation.md similarity index 99% rename from docs/project-generation.md rename to docs/en/docs/project-generation.md index 4562d6204a873..fd7a10fb7d109 100644 --- a/docs/project-generation.md +++ b/docs/en/docs/project-generation.md @@ -1,3 +1,5 @@ +# Project Generation - Template + There is a project generator that you can use to get started, with a lot of the initial set up, security, database and first API endpoints already done for you. ## Full-Stack-FastAPI-PostgreSQL diff --git a/docs/python-types.md b/docs/en/docs/python-types.md similarity index 92% rename from docs/python-types.md rename to docs/en/docs/python-types.md index 5d25e18165978..ce24281ba3ebf 100644 --- a/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -1,3 +1,5 @@ +# Python Types Intro + **Python 3.6+** has support for optional "type hints". These **"type hints"** are a new syntax (since Python 3.6+) that allow declaring the type of a variable. @@ -18,7 +20,7 @@ But even if you never use **FastAPI**, you would benefit from learning a bit abo Let's start with a simple example: ```Python -{!./src/python_types/tutorial001.py!} +{!../../../docs_src/python_types/tutorial001.py!} ``` Calling this program outputs: @@ -34,7 +36,7 @@ The function does the following: * Concatenates them with a space in the middle. ```Python hl_lines="2" -{!./src/python_types/tutorial001.py!} +{!../../../docs_src/python_types/tutorial001.py!} ``` ### Edit it @@ -78,7 +80,7 @@ That's it. Those are the "type hints": ```Python hl_lines="1" -{!./src/python_types/tutorial002.py!} +{!../../../docs_src/python_types/tutorial002.py!} ``` That is not the same as declaring default values like would be with: @@ -108,7 +110,7 @@ With that, you can scroll, seeing the options, until you find the one that "ring Check this function, it already has type hints: ```Python hl_lines="1" -{!./src/python_types/tutorial003.py!} +{!../../../docs_src/python_types/tutorial003.py!} ``` Because the editor knows the types of the variables, you don't only get completion, you also get error checks: @@ -118,7 +120,7 @@ Because the editor knows the types of the variables, you don't only get completi Now you know that you have to fix it, convert `age` to a string with `str(age)`: ```Python hl_lines="2" -{!./src/python_types/tutorial004.py!} +{!../../../docs_src/python_types/tutorial004.py!} ``` ## Declaring types @@ -139,7 +141,7 @@ You can use, for example: * `bytes` ```Python hl_lines="1" -{!./src/python_types/tutorial005.py!} +{!../../../docs_src/python_types/tutorial005.py!} ``` ### Types with subtypes @@ -157,7 +159,7 @@ For example, let's define a variable to be a `list` of `str`. From `typing`, import `List` (with a capital `L`): ```Python hl_lines="1" -{!./src/python_types/tutorial006.py!} +{!../../../docs_src/python_types/tutorial006.py!} ``` Declare the variable, with the same colon (`:`) syntax. @@ -167,7 +169,7 @@ As the type, put the `List`. As the list is a type that takes a "subtype", you put the subtype in square brackets: ```Python hl_lines="4" -{!./src/python_types/tutorial006.py!} +{!../../../docs_src/python_types/tutorial006.py!} ``` That means: "the variable `items` is a `list`, and each of the items in this list is a `str`". @@ -187,7 +189,7 @@ And still, the editor knows it is a `str`, and provides support for that. You would do the same to declare `tuple`s and `set`s: ```Python hl_lines="1 4" -{!./src/python_types/tutorial007.py!} +{!../../../docs_src/python_types/tutorial007.py!} ``` This means: @@ -204,7 +206,7 @@ The first subtype is for the keys of the `dict`. The second subtype is for the values of the `dict`: ```Python hl_lines="1 4" -{!./src/python_types/tutorial008.py!} +{!../../../docs_src/python_types/tutorial008.py!} ``` This means: @@ -220,13 +222,13 @@ You can also declare a class as the type of a variable. Let's say you have a class `Person`, with a name: ```Python hl_lines="1 2 3" -{!./src/python_types/tutorial009.py!} +{!../../../docs_src/python_types/tutorial009.py!} ``` Then you can declare a variable to be of type `Person`: ```Python hl_lines="6" -{!./src/python_types/tutorial009.py!} +{!../../../docs_src/python_types/tutorial009.py!} ``` And then, again, you get all the editor support: @@ -248,7 +250,7 @@ And you get all the editor support with that resulting object. Taken from the official Pydantic docs: ```Python -{!./src/python_types/tutorial010.py!} +{!../../../docs_src/python_types/tutorial010.py!} ``` !!! info diff --git a/docs/release-notes.md b/docs/en/docs/release-notes.md similarity index 99% rename from docs/release-notes.md rename to docs/en/docs/release-notes.md index 08d8806505371..d5218e063fb8a 100644 --- a/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -1,3 +1,5 @@ +# Release Notes + ## Latest changes * Update terminal styles in docs and add note about [**Typer**, the FastAPI of CLIs](https://typer.tiangolo.com/). PR [#1139](https://github.com/tiangolo/fastapi/pull/1139). diff --git a/docs/tutorial/application-configuration.md b/docs/en/docs/tutorial/application-configuration.md similarity index 87% rename from docs/tutorial/application-configuration.md rename to docs/en/docs/tutorial/application-configuration.md index 706ecd3483b46..978273cc1a2ea 100644 --- a/docs/tutorial/application-configuration.md +++ b/docs/en/docs/tutorial/application-configuration.md @@ -1,3 +1,5 @@ +# Application Configuration + There are several things that you can configure in your FastAPI application. ## Title, description, and version @@ -12,7 +14,7 @@ You can set the: To set them, use the parameters `title`, `description`, and `version`: ```Python hl_lines="4 5 6" -{!./src/application_configuration/tutorial001.py!} +{!../../../docs_src/application_configuration/tutorial001.py!} ``` With this configuration, the automatic API docs would look like: @@ -28,7 +30,7 @@ But you can configure it with the parameter `openapi_url`. For example, to set it to be served at `/api/v1/openapi.json`: ```Python hl_lines="3" -{!./src/application_configuration/tutorial002.py!} +{!../../../docs_src/application_configuration/tutorial002.py!} ``` If you want to disable the OpenAPI schema completely you can set `openapi_url=None`. @@ -47,5 +49,5 @@ You can configure the two documentation user interfaces included: For example, to set Swagger UI to be served at `/documentation` and disable ReDoc: ```Python hl_lines="3" -{!./src/application_configuration/tutorial003.py!} +{!../../../docs_src/application_configuration/tutorial003.py!} ``` diff --git a/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md similarity index 95% rename from docs/tutorial/background-tasks.md rename to docs/en/docs/tutorial/background-tasks.md index 15b92e8f5ae0f..3771eff7129a2 100644 --- a/docs/tutorial/background-tasks.md +++ b/docs/en/docs/tutorial/background-tasks.md @@ -1,3 +1,5 @@ +# Background Tasks + You can define background tasks to be run *after* returning a response. This is useful for operations that need to happen after a request, but that the client doesn't really have to be waiting for the operation to complete before receiving his response. @@ -14,7 +16,7 @@ This includes, for example: First, import `BackgroundTasks` and define a parameter in your *path operation function* with a type declaration of `BackgroundTasks`: ```Python hl_lines="1 13" -{!./src/background_tasks/tutorial001.py!} +{!../../../docs_src/background_tasks/tutorial001.py!} ``` **FastAPI** will create the object of type `BackgroundTasks` for you and pass it as that parameter. @@ -32,7 +34,7 @@ In this case, the task function will write to a file (simulating sending an emai And as the write operation doesn't use `async` and `await`, we define the function with normal `def`: ```Python hl_lines="6 7 8 9" -{!./src/background_tasks/tutorial001.py!} +{!../../../docs_src/background_tasks/tutorial001.py!} ``` ## Add the background task @@ -40,7 +42,7 @@ And as the write operation doesn't use `async` and `await`, we define the functi Inside of your *path operation function*, pass your task function to the *background tasks* object with the method `.add_task()`: ```Python hl_lines="14" -{!./src/background_tasks/tutorial001.py!} +{!../../../docs_src/background_tasks/tutorial001.py!} ``` `.add_task()` receives as arguments: @@ -56,7 +58,7 @@ Using `BackgroundTasks` also works with the dependency injection system, you can **FastAPI** knows what to do in each case and how to re-use the same object, so that all the background tasks are merged together and are run in the background afterwards: ```Python hl_lines="11 14 20 23" -{!./src/background_tasks/tutorial002.py!} +{!../../../docs_src/background_tasks/tutorial002.py!} ``` In this example, the messages will be written to the `log.txt` file *after* the response is sent. diff --git a/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md similarity index 95% rename from docs/tutorial/bigger-applications.md rename to docs/en/docs/tutorial/bigger-applications.md index f6c623b840b3a..0d3be0f16538c 100644 --- a/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -1,3 +1,5 @@ +# Bigger Applications - Multiple Files + If you are building an application or a web API, it's rarely the case that you can put everything on a single file. **FastAPI** provides a convenience tool to structure your application while keeping all the flexibility. @@ -59,7 +61,7 @@ You can create the *path operations* for that module using `APIRouter`. You import it and create an "instance" the same way you would with the class `FastAPI`: ```Python hl_lines="1 3" -{!./src/bigger_applications/app/routers/users.py!} +{!../../../docs_src/bigger_applications/app/routers/users.py!} ``` ### *Path operations* with `APIRouter` @@ -69,7 +71,7 @@ And then you use it to declare your *path operations*. Use it the same way you would use the `FastAPI` class: ```Python hl_lines="6 11 16" -{!./src/bigger_applications/app/routers/users.py!} +{!../../../docs_src/bigger_applications/app/routers/users.py!} ``` You can think of `APIRouter` as a "mini `FastAPI`" class. @@ -99,7 +101,7 @@ But let's say that this time we are more lazy. And we don't want to have to explicitly type `/items/` and `tags=["items"]` in every *path operation* (we will be able to do it later): ```Python hl_lines="6 11" -{!./src/bigger_applications/app/routers/items.py!} +{!../../../docs_src/bigger_applications/app/routers/items.py!} ``` ### Add some custom `tags`, `responses`, and `dependencies` @@ -109,7 +111,7 @@ We are not adding the prefix `/items/` nor the `tags=["items"]` to add them late But we can add custom `tags` and `responses` that will be applied to a specific *path operation*: ```Python hl_lines="18 19" -{!./src/bigger_applications/app/routers/items.py!} +{!../../../docs_src/bigger_applications/app/routers/items.py!} ``` ## The main `FastAPI` @@ -125,7 +127,7 @@ This will be the main file in your application that ties everything together. You import and create a `FastAPI` class as normally: ```Python hl_lines="1 5" -{!./src/bigger_applications/app/main.py!} +{!../../../docs_src/bigger_applications/app/main.py!} ``` ### Import the `APIRouter` @@ -135,7 +137,7 @@ But this time we are not adding *path operations* directly with the `FastAPI` `a We import the other submodules that have `APIRouter`s: ```Python hl_lines="3" -{!./src/bigger_applications/app/main.py!} +{!../../../docs_src/bigger_applications/app/main.py!} ``` As the file `app/routers/items.py` is part of the same Python package, we can import it using "dot notation". @@ -187,7 +189,7 @@ The `router` from `users` would overwrite the one from `items` and we wouldn't b So, to be able to use both of them in the same file, we import the submodules directly: ```Python hl_lines="3" -{!./src/bigger_applications/app/main.py!} +{!../../../docs_src/bigger_applications/app/main.py!} ``` ### Include an `APIRouter` @@ -195,7 +197,7 @@ So, to be able to use both of them in the same file, we import the submodules di Now, let's include the `router` from the submodule `users`: ```Python hl_lines="13" -{!./src/bigger_applications/app/main.py!} +{!../../../docs_src/bigger_applications/app/main.py!} ``` !!! info @@ -244,7 +246,7 @@ And we can add predefined `responses` that will be included in all the *path ope And we can add a list of `dependencies` that will be added to all the *path operations* in the router and will be executed/solved for each request made to them. Note that, much like dependencies in *path operation decorators*, no value will be passed to your *path operation function*. ```Python hl_lines="8 9 10 14 15 16 17 18 19 20" -{!./src/bigger_applications/app/main.py!} +{!../../../docs_src/bigger_applications/app/main.py!} ``` The end result is that the item paths are now: diff --git a/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md similarity index 93% rename from docs/tutorial/body-fields.md rename to docs/en/docs/tutorial/body-fields.md index f54ed4eb83f38..daba2eaffa820 100644 --- a/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -1,3 +1,5 @@ +# Body - Fields + The same way you can declare additional validation and metadata in *path operation function* parameters with `Query`, `Path` and `Body`, you can declare validation and metadata inside of Pydantic models using Pydantic's `Field`. ## Import `Field` @@ -5,7 +7,7 @@ The same way you can declare additional validation and metadata in *path operati First, you have to import it: ```Python hl_lines="2" -{!./src/body_fields/tutorial001.py!} +{!../../../docs_src/body_fields/tutorial001.py!} ``` !!! warning @@ -16,7 +18,7 @@ First, you have to import it: You can then use `Field` with model attributes: ```Python hl_lines="9 10" -{!./src/body_fields/tutorial001.py!} +{!../../../docs_src/body_fields/tutorial001.py!} ``` `Field` works the same way as `Query`, `Path` and `Body`, it has all the same parameters, etc. @@ -47,7 +49,7 @@ If you know JSON Schema and want to add extra information apart from what we hav For example, you can use that functionality to pass a JSON Schema example field to a body request JSON Schema: ```Python hl_lines="20 21 22 23 24 25" -{!./src/body_fields/tutorial002.py!} +{!../../../docs_src/body_fields/tutorial002.py!} ``` And it would look in the `/docs` like this: diff --git a/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md similarity index 93% rename from docs/tutorial/body-multiple-params.md rename to docs/en/docs/tutorial/body-multiple-params.md index 5009c21bfdb22..6d5e6e39deed3 100644 --- a/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -1,3 +1,5 @@ +# Body - Multiple Parameters + Now that we have seen how to use `Path` and `Query`, let's see more advanced uses of request body declarations. ## Mix `Path`, `Query` and body parameters @@ -7,7 +9,7 @@ First, of course, you can mix `Path`, `Query` and request body parameter declara And you can also declare body parameters as optional, by setting the default to `None`: ```Python hl_lines="17 18 19" -{!./src/body_multiple_params/tutorial001.py!} +{!../../../docs_src/body_multiple_params/tutorial001.py!} ``` !!! note @@ -29,7 +31,7 @@ In the previous example, the *path operations* would expect a JSON body with the But you can also declare multiple body parameters, e.g. `item` and `user`: ```Python hl_lines="20" -{!./src/body_multiple_params/tutorial002.py!} +{!../../../docs_src/body_multiple_params/tutorial002.py!} ``` In this case, **FastAPI** will notice that there are more than one body parameters in the function (two parameters that are Pydantic models). @@ -71,7 +73,7 @@ But you can instruct **FastAPI** to treat it as another body key using `Body`: ```Python hl_lines="21" -{!./src/body_multiple_params/tutorial003.py!} +{!../../../docs_src/body_multiple_params/tutorial003.py!} ``` In this case, **FastAPI** will expect a body like: @@ -108,7 +110,7 @@ q: str = None as in: ```Python hl_lines="25" -{!./src/body_multiple_params/tutorial004.py!} +{!../../../docs_src/body_multiple_params/tutorial004.py!} ``` !!! info @@ -130,7 +132,7 @@ item: Item = Body(..., embed=True) as in: ```Python hl_lines="15" -{!./src/body_multiple_params/tutorial005.py!} +{!../../../docs_src/body_multiple_params/tutorial005.py!} ``` In this case **FastAPI** will expect a body like: diff --git a/docs/tutorial/body-nested-models.md b/docs/en/docs/tutorial/body-nested-models.md similarity index 90% rename from docs/tutorial/body-nested-models.md rename to docs/en/docs/tutorial/body-nested-models.md index d5c5c95756e55..993389c903c96 100644 --- a/docs/tutorial/body-nested-models.md +++ b/docs/en/docs/tutorial/body-nested-models.md @@ -1,3 +1,5 @@ +# Body - Nested Models + With **FastAPI**, you can define, validate, document, and use arbitrarily deeply nested models (thanks to Pydantic). ## List fields @@ -5,7 +7,7 @@ With **FastAPI**, you can define, validate, document, and use arbitrarily deeply You can define an attribute to be a subtype. For example, a Python `list`: ```Python hl_lines="12" -{!./src/body_nested_models/tutorial001.py!} +{!../../../docs_src/body_nested_models/tutorial001.py!} ``` This will make `tags` be a list of items. Although it doesn't declare the type of each of the items. @@ -19,7 +21,7 @@ But Python has a specific way to declare lists with subtypes: First, import `List` from standard Python's `typing` module: ```Python hl_lines="1" -{!./src/body_nested_models/tutorial002.py!} +{!../../../docs_src/body_nested_models/tutorial002.py!} ``` ### Declare a `List` with a subtype @@ -42,7 +44,7 @@ Use that same standard syntax for model attributes with subtypes. So, in our example, we can make `tags` be specifically a "list of strings": ```Python hl_lines="14" -{!./src/body_nested_models/tutorial002.py!} +{!../../../docs_src/body_nested_models/tutorial002.py!} ``` ## Set types @@ -54,7 +56,7 @@ And Python has a special data type for sets of unique items, the `set`. Then we can import `Set` and declare `tags` as a `set` of `str`: ```Python hl_lines="1 14" -{!./src/body_nested_models/tutorial003.py!} +{!../../../docs_src/body_nested_models/tutorial003.py!} ``` With this, even if you receive a request with duplicate data, it will be converted to a set of unique items. @@ -78,7 +80,7 @@ All that, arbitrarily nested. For example, we can define an `Image` model: ```Python hl_lines="9 10 11" -{!./src/body_nested_models/tutorial004.py!} +{!../../../docs_src/body_nested_models/tutorial004.py!} ``` ### Use the submodel as a type @@ -86,7 +88,7 @@ For example, we can define an `Image` model: And then we can use it as the type of an attribute: ```Python hl_lines="20" -{!./src/body_nested_models/tutorial004.py!} +{!../../../docs_src/body_nested_models/tutorial004.py!} ``` This would mean that **FastAPI** would expect a body similar to: @@ -121,7 +123,7 @@ To see all the options you have, checkout the docs for HTTP `PUT` operation. @@ -5,7 +7,7 @@ To update an item you can use the CORS or "Cross-Origin Resource Sharing" refers to the situations when a frontend running in a browser has JavaScript code that communicates with a backend, and the backend is in a different "origin" than the frontend. ## Origin @@ -45,7 +47,7 @@ You can also specify if your backend allows: * Specific HTTP headers or all of them with the wildcard `"*"`. ```Python hl_lines="2 6 7 8 9 10 11 13 14 15 16 17 18 19" -{!./src/cors/tutorial001.py!} +{!../../../docs_src/cors/tutorial001.py!} ``` The default parameters used by the `CORSMiddleware` implementation are restrictive by default, so you'll need to explicitly enable particular origins, methods, or headers, in order for browsers to be permitted to use them in a Cross-Domain context. diff --git a/docs/tutorial/debugging.md b/docs/en/docs/tutorial/debugging.md similarity index 97% rename from docs/tutorial/debugging.md rename to docs/en/docs/tutorial/debugging.md index e527bf9712363..733ae32a17eac 100644 --- a/docs/tutorial/debugging.md +++ b/docs/en/docs/tutorial/debugging.md @@ -1,3 +1,5 @@ +# Debugging + You can connect the debugger in your editor, for example with Visual Studio Code or PyCharm. ## Call `uvicorn` @@ -5,7 +7,7 @@ You can connect the debugger in your editor, for example with Visual Studio Code In your FastAPI application, import and run `uvicorn` directly: ```Python hl_lines="1 15" -{!./src/debugging/tutorial001.py!} +{!../../../docs_src/debugging/tutorial001.py!} ``` ### About `__name__ == "__main__"` diff --git a/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md similarity index 93% rename from docs/tutorial/dependencies/classes-as-dependencies.md rename to docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index 674615410832a..b4d78af84e4b4 100644 --- a/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -1,3 +1,5 @@ +# Classes as Dependencies + Before diving deeper into the **Dependency Injection** system, let's upgrade the previous example. ## A `dict` from the previous example @@ -5,7 +7,7 @@ Before diving deeper into the **Dependency Injection** system, let's upgrade the In the previous example, we were returning a `dict` from our dependency ("dependable"): ```Python hl_lines="7" -{!./src/dependencies/tutorial001.py!} +{!../../../docs_src/dependencies/tutorial001.py!} ``` But then we get a `dict` in the parameter `commons` of the *path operation function*. @@ -70,19 +72,19 @@ That also applies to callables with no parameters at all. The same as it would b Then, we can change the dependency "dependable" `common_parameters` from above to the class `CommonQueryParameters`: ```Python hl_lines="9 10 11 12 13" -{!./src/dependencies/tutorial002.py!} +{!../../../docs_src/dependencies/tutorial002.py!} ``` Pay attention to the `__init__` method used to create the instance of the class: ```Python hl_lines="10" -{!./src/dependencies/tutorial002.py!} +{!../../../docs_src/dependencies/tutorial002.py!} ``` ...it has the same parameters as our previous `common_parameters`: ```Python hl_lines="6" -{!./src/dependencies/tutorial001.py!} +{!../../../docs_src/dependencies/tutorial001.py!} ``` Those parameters are what **FastAPI** will use to "solve" the dependency. @@ -102,7 +104,7 @@ Now you can declare your dependency using this class. And as when **FastAPI** calls that class the value that will be passed as `commons` to your function will be an "instance" of the class, you can declare that parameter `commons` to be of type of the class, `CommonQueryParams`. ```Python hl_lines="17" -{!./src/dependencies/tutorial002.py!} +{!../../../docs_src/dependencies/tutorial002.py!} ``` ## Type annotation vs `Depends` @@ -142,7 +144,7 @@ commons = Depends(CommonQueryParams) ..as in: ```Python hl_lines="17" -{!./src/dependencies/tutorial003.py!} +{!../../../docs_src/dependencies/tutorial003.py!} ``` But declaring the type is encouraged as that way your editor will know what will be passed as the parameter `commons`, and then it can help you with code completion, type checks, etc: @@ -178,7 +180,7 @@ So, you can declare the dependency as the type of the variable, and use `Depends So, the same example would look like: ```Python hl_lines="17" -{!./src/dependencies/tutorial004.py!} +{!../../../docs_src/dependencies/tutorial004.py!} ``` ...and **FastAPI** will know what to do. diff --git a/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md similarity index 89% rename from docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md rename to docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index 7e774d5209ce1..64e03139c96ad 100644 --- a/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -1,3 +1,5 @@ +# Dependencies in path operation decorators + In some cases you don't really need the return value of a dependency inside your *path operation function*. Or the dependency doesn't return a value. @@ -13,7 +15,7 @@ The *path operation decorator* receives an optional argument `dependencies`. It should be a `list` of `Depends()`: ```Python hl_lines="17" -{!./src/dependencies/tutorial006.py!} +{!../../../docs_src/dependencies/tutorial006.py!} ``` These dependencies will be executed/solved the same way normal dependencies. But their value (if they return any) won't be passed to your *path operation function*. @@ -34,7 +36,7 @@ You can use the same dependency *functions* you use normally. They can declare request requirements (like headers) or other sub-dependencies: ```Python hl_lines="6 11" -{!./src/dependencies/tutorial006.py!} +{!../../../docs_src/dependencies/tutorial006.py!} ``` ### Raise exceptions @@ -42,7 +44,7 @@ They can declare request requirements (like headers) or other sub-dependencies: These dependencies can `raise` exceptions, the same as normal dependencies: ```Python hl_lines="8 13" -{!./src/dependencies/tutorial006.py!} +{!../../../docs_src/dependencies/tutorial006.py!} ``` ### Return values @@ -52,7 +54,7 @@ And they can return values or not, the values won't be used. So, you can re-use a normal dependency (that returns a value) you already use somewhere else, and even though the value won't be used, the dependency will be executed: ```Python hl_lines="9 14" -{!./src/dependencies/tutorial006.py!} +{!../../../docs_src/dependencies/tutorial006.py!} ``` ## Dependencies for a group of *path operations* diff --git a/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md similarity index 96% rename from docs/tutorial/dependencies/dependencies-with-yield.md rename to docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index 08d600a5aee4d..1c8cdba65a2a1 100644 --- a/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -1,4 +1,4 @@ -# Dependencies with `yield` +# Dependencies with yield FastAPI supports dependencies that do some extra steps after finishing. @@ -33,19 +33,19 @@ For example, you could use this to create a database session and close it after Only the code prior to and including the `yield` statement is executed before sending a response: ```Python hl_lines="2 3 4" -{!./src/dependencies/tutorial007.py!} +{!../../../docs_src/dependencies/tutorial007.py!} ``` The yielded value is what is injected into *path operations* and other dependencies: ```Python hl_lines="4" -{!./src/dependencies/tutorial007.py!} +{!../../../docs_src/dependencies/tutorial007.py!} ``` The code following the `yield` statement is executed after the response has been delivered: ```Python hl_lines="5 6" -{!./src/dependencies/tutorial007.py!} +{!../../../docs_src/dependencies/tutorial007.py!} ``` !!! tip @@ -64,7 +64,7 @@ So, you can look for that specific exception inside the dependency with `except In the same way, you can use `finally` to make sure the exit steps are executed, no matter if there was an exception or not. ```Python hl_lines="3 5" -{!./src/dependencies/tutorial007.py!} +{!../../../docs_src/dependencies/tutorial007.py!} ``` ## Sub-dependencies with `yield` @@ -76,7 +76,7 @@ You can have sub-dependencies and "trees" of sub-dependencies of any size and sh For example, `dependency_c` can have a dependency on `dependency_b`, and `dependency_b` on `dependency_a`: ```Python hl_lines="4 12 20" -{!./src/dependencies/tutorial008.py!} +{!../../../docs_src/dependencies/tutorial008.py!} ``` And all of them can use `yield`. @@ -86,7 +86,7 @@ In this case `dependency_c`, to execute its exit code, needs the value from `dep And, in turn, `dependency_b` needs the value from `dependency_a` (here named `dep_a`) to be available for its exit code. ```Python hl_lines="16 17 24 25" -{!./src/dependencies/tutorial008.py!} +{!../../../docs_src/dependencies/tutorial008.py!} ``` The same way, you could have dependencies with `yield` and `return` mixed. @@ -208,7 +208,7 @@ You can also use them inside of **FastAPI** dependencies with `yield` by using `with` or `async with` statements inside of the dependency function: ```Python hl_lines="1 2 3 4 5 6 7 8 9 13" -{!./src/dependencies/tutorial010.py!} +{!../../../docs_src/dependencies/tutorial010.py!} ``` !!! tip diff --git a/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md similarity index 97% rename from docs/tutorial/dependencies/index.md rename to docs/en/docs/tutorial/dependencies/index.md index c92dcebf6d7b2..5b8d11ea765ae 100644 --- a/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -1,3 +1,5 @@ +# Dependencies - First Steps + **FastAPI** has a very powerful but intuitive **Dependency Injection** system. It is designed to be very simple to use, and to make it very easy for any developer to integrate other components with **FastAPI**. @@ -30,7 +32,7 @@ Let's first focus on the dependency. It is just a function that can take all the same parameters that a *path operation function* can take: ```Python hl_lines="6 7" -{!./src/dependencies/tutorial001.py!} +{!../../../docs_src/dependencies/tutorial001.py!} ``` That's it. @@ -54,7 +56,7 @@ And then it just returns a `dict` containing those values. ### Import `Depends` ```Python hl_lines="1" -{!./src/dependencies/tutorial001.py!} +{!../../../docs_src/dependencies/tutorial001.py!} ``` ### Declare the dependency, in the "dependant" @@ -62,7 +64,7 @@ And then it just returns a `dict` containing those values. The same way you use `Body`, `Query`, etc. with your *path operation function* parameters, use `Depends` with a new parameter: ```Python hl_lines="11 16" -{!./src/dependencies/tutorial001.py!} +{!../../../docs_src/dependencies/tutorial001.py!} ``` Although you use `Depends` in the parameters of your function the same way you use `Body`, `Query`, etc, `Depends` works a bit differently. diff --git a/docs/tutorial/dependencies/sub-dependencies.md b/docs/en/docs/tutorial/dependencies/sub-dependencies.md similarity index 95% rename from docs/tutorial/dependencies/sub-dependencies.md rename to docs/en/docs/tutorial/dependencies/sub-dependencies.md index 3407733808cce..71b1404d6f860 100644 --- a/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/sub-dependencies.md @@ -1,3 +1,5 @@ +# Sub-dependencies + You can create dependencies that have **sub-dependencies**. They can be as **deep** as you need them to be. @@ -9,7 +11,7 @@ They can be as **deep** as you need them to be. You could create a first dependency ("dependable") like: ```Python hl_lines="6 7" -{!./src/dependencies/tutorial005.py!} +{!../../../docs_src/dependencies/tutorial005.py!} ``` It declares an optional query parameter `q` as a `str`, and then it just returns it. @@ -21,7 +23,7 @@ This is quite simple (not very useful), but will help us focus on how the sub-de Then you can create another dependency function (a "dependable") that at the same time declares a dependency of its own (so it is a "dependant" too): ```Python hl_lines="11" -{!./src/dependencies/tutorial005.py!} +{!../../../docs_src/dependencies/tutorial005.py!} ``` Let's focus on the parameters declared: @@ -36,7 +38,7 @@ Let's focus on the parameters declared: Then we can use the dependency with: ```Python hl_lines="19" -{!./src/dependencies/tutorial005.py!} +{!../../../docs_src/dependencies/tutorial005.py!} ``` !!! info diff --git a/docs/tutorial/encoder.md b/docs/en/docs/tutorial/encoder.md similarity index 95% rename from docs/tutorial/encoder.md rename to docs/en/docs/tutorial/encoder.md index 4ce940eca1227..3879f976af6e1 100644 --- a/docs/tutorial/encoder.md +++ b/docs/en/docs/tutorial/encoder.md @@ -1,3 +1,5 @@ +# JSON Compatible Encoder + There are some cases where you might need to convert a data type (like a Pydantic model) to something compatible with JSON (like a `dict`, `list`, etc). For example, if you need to store it in a database. @@ -19,7 +21,7 @@ You can use `jsonable_encoder` for that. It receives an object, like a Pydantic model, and returns a JSON compatible version: ```Python hl_lines="4 21" -{!./src/encoder/tutorial001.py!} +{!../../../docs_src/encoder/tutorial001.py!} ``` In this example, it would convert the Pydantic model to a `dict`, and the `datetime` to a `str`. diff --git a/docs/tutorial/extra-data-types.md b/docs/en/docs/tutorial/extra-data-types.md similarity index 94% rename from docs/tutorial/extra-data-types.md rename to docs/en/docs/tutorial/extra-data-types.md index 7534be86da81b..07c1ed06f792d 100644 --- a/docs/tutorial/extra-data-types.md +++ b/docs/en/docs/tutorial/extra-data-types.md @@ -1,3 +1,5 @@ +# Extra Data Types + Up to now, you have been using common data types, like: * `int` @@ -53,11 +55,11 @@ Here are some of the additional data types you can use: Here's an example *path operation* with parameters using some of the above types. ```Python hl_lines="1 2 11 12 13 14 15" -{!./src/extra_data_types/tutorial001.py!} +{!../../../docs_src/extra_data_types/tutorial001.py!} ``` Note that the parameters inside the function have their natural data type, and you can, for example, perform normal date manipulations, like: ```Python hl_lines="17 18" -{!./src/extra_data_types/tutorial001.py!} +{!../../../docs_src/extra_data_types/tutorial001.py!} ``` diff --git a/docs/tutorial/extra-models.md b/docs/en/docs/tutorial/extra-models.md similarity index 95% rename from docs/tutorial/extra-models.md rename to docs/en/docs/tutorial/extra-models.md index 731a73f3d18e7..5b8bf5a2b84ba 100644 --- a/docs/tutorial/extra-models.md +++ b/docs/en/docs/tutorial/extra-models.md @@ -1,3 +1,5 @@ +# Extra Models + Continuing with the previous example, it will be common to have more than one related model. This is especially the case for user models, because: @@ -16,7 +18,7 @@ This is especially the case for user models, because: Here's a general idea of how the models could look like with their password fields and the places where they are used: ```Python hl_lines="7 9 14 20 22 27 28 31 32 33 38 39" -{!./src/extra_models/tutorial001.py!} +{!../../../docs_src/extra_models/tutorial001.py!} ``` ### About `**user_in.dict()` @@ -149,7 +151,7 @@ All the data conversion, validation, documentation, etc. will still work as norm That way, we can declare just the differences between the models (with plaintext `password`, with `hashed_password` and without password): ```Python hl_lines="7 13 14 17 18 21 22" -{!./src/extra_models/tutorial002.py!} +{!../../../docs_src/extra_models/tutorial002.py!} ``` ## `Union` or `anyOf` @@ -161,7 +163,7 @@ It will be defined in OpenAPI with `anyOf`. To do that, use the standard Python type hint `typing.Union`: ```Python hl_lines="1 14 15 18 19 20 33" -{!./src/extra_models/tutorial003.py!} +{!../../../docs_src/extra_models/tutorial003.py!} ``` ## List of models @@ -171,7 +173,7 @@ The same way, you can declare responses of lists of objects. For that, use the standard Python `typing.List`: ```Python hl_lines="1 20" -{!./src/extra_models/tutorial004.py!} +{!../../../docs_src/extra_models/tutorial004.py!} ``` ## Response with arbitrary `dict` @@ -183,7 +185,7 @@ This is useful if you don't know the valid field/attribute names (that would be In this case, you can use `typing.Dict`: ```Python hl_lines="1 8" -{!./src/extra_models/tutorial005.py!} +{!../../../docs_src/extra_models/tutorial005.py!} ``` ## Recap diff --git a/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md similarity index 95% rename from docs/tutorial/first-steps.md rename to docs/en/docs/tutorial/first-steps.md index 639cfed395658..2d212762ed8db 100644 --- a/docs/tutorial/first-steps.md +++ b/docs/en/docs/tutorial/first-steps.md @@ -1,7 +1,9 @@ +# First Steps + The simplest FastAPI file could look like this: ```Python -{!./src/first_steps/tutorial001.py!} +{!../../../docs_src/first_steps/tutorial001.py!} ``` Copy that to a file `main.py`. @@ -129,7 +131,7 @@ You could also use it to generate code automatically, for clients that communica ### Step 1: import `FastAPI` ```Python hl_lines="1" -{!./src/first_steps/tutorial001.py!} +{!../../../docs_src/first_steps/tutorial001.py!} ``` `FastAPI` is a Python class that provides all the functionality for your API. @@ -142,7 +144,7 @@ You could also use it to generate code automatically, for clients that communica ### Step 2: create a `FastAPI` "instance" ```Python hl_lines="3" -{!./src/first_steps/tutorial001.py!} +{!../../../docs_src/first_steps/tutorial001.py!} ``` Here the `app` variable will be an "instance" of the class `FastAPI`. @@ -164,7 +166,7 @@ $ uvicorn main:app --reload If you create your app like: ```Python hl_lines="3" -{!./src/first_steps/tutorial002.py!} +{!../../../docs_src/first_steps/tutorial002.py!} ``` And put it in a file `main.py`, then you would call `uvicorn` like: @@ -240,7 +242,7 @@ We are going to call them "**operations**" too. #### Define a *path operation function* ```Python hl_lines="6" -{!./src/first_steps/tutorial001.py!} +{!../../../docs_src/first_steps/tutorial001.py!} ``` The `@app.get("/")` tells **FastAPI** that the function right below is in charge of handling requests that go to: @@ -290,7 +292,7 @@ This is our "**path operation function**": * **function**: is the function below the "decorator" (below `@app.get("/")`). ```Python hl_lines="7" -{!./src/first_steps/tutorial001.py!} +{!../../../docs_src/first_steps/tutorial001.py!} ``` This is a Python function. @@ -304,7 +306,7 @@ In this case, it is an `async` function. You could also define it as a normal function instead of `async def`: ```Python hl_lines="7" -{!./src/first_steps/tutorial003.py!} +{!../../../docs_src/first_steps/tutorial003.py!} ``` !!! note @@ -313,7 +315,7 @@ You could also define it as a normal function instead of `async def`: ### Step 5: return the content ```Python hl_lines="8" -{!./src/first_steps/tutorial001.py!} +{!../../../docs_src/first_steps/tutorial001.py!} ``` You can return a `dict`, `list`, singular values as `str`, `int`, etc. diff --git a/docs/tutorial/handling-errors.md b/docs/en/docs/tutorial/handling-errors.md similarity index 95% rename from docs/tutorial/handling-errors.md rename to docs/en/docs/tutorial/handling-errors.md index 8ae9ff47ea97b..99ecc2dc82990 100644 --- a/docs/tutorial/handling-errors.md +++ b/docs/en/docs/tutorial/handling-errors.md @@ -1,3 +1,5 @@ +# Handling Errors + There are many situations in where you need to notify an error to a client that is using your API. This client could be a browser with a frontend, a code from someone else, an IoT device, etc. @@ -24,7 +26,7 @@ To return HTTP responses with errors to the client you use `HTTPException`. ### Import `HTTPException` ```Python hl_lines="1" -{!./src/handling_errors/tutorial001.py!} +{!../../../docs_src/handling_errors/tutorial001.py!} ``` ### Raise an `HTTPException` in your code @@ -40,7 +42,7 @@ The benefit of raising an exception over `return`ing a value will be more eviden In this example, when the client request an item by an ID that doesn't exist, raise an exception with a status code of `404`: ```Python hl_lines="11" -{!./src/handling_errors/tutorial001.py!} +{!../../../docs_src/handling_errors/tutorial001.py!} ``` ### The resulting response @@ -77,7 +79,7 @@ You probably won't need to use it directly in your code. But in case you needed it for an advanced scenario, you can add custom headers: ```Python hl_lines="14" -{!./src/handling_errors/tutorial002.py!} +{!../../../docs_src/handling_errors/tutorial002.py!} ``` ## Install custom exception handlers @@ -91,7 +93,7 @@ And you want to handle this exception globally with FastAPI. You could add a custom exception handler with `@app.exception_handler()`: ```Python hl_lines="5 6 7 13 14 15 16 17 18 24" -{!./src/handling_errors/tutorial003.py!} +{!../../../docs_src/handling_errors/tutorial003.py!} ``` Here, if you request `/unicorns/yolo`, the *path operation* will `raise` a `UnicornException`. @@ -128,7 +130,7 @@ To override it, import the `RequestValidationError` and use it with `@app.except The exception handler will receive a `Request` and the exception. ```Python hl_lines="2 14 15 16" -{!./src/handling_errors/tutorial004.py!} +{!../../../docs_src/handling_errors/tutorial004.py!} ``` Now, if you go to `/items/foo`, instead of getting the default JSON error with: @@ -178,7 +180,7 @@ The same way, you can override the `HTTPException` handler. For example, you could want to return a plain text response instead of JSON for these errors: ```Python hl_lines="3 4 9 10 11 22" -{!./src/handling_errors/tutorial004.py!} +{!../../../docs_src/handling_errors/tutorial004.py!} ``` !!! note "Technical Details" @@ -193,7 +195,7 @@ The `RequestValidationError` contains the `body` it received with invalid data. You could use it while developing your app to log the body and debug it, return it to the user, etc. ```Python hl_lines="14" -{!./src/handling_errors/tutorial005.py!} +{!../../../docs_src/handling_errors/tutorial005.py!} ``` Now try sending an invalid item like: @@ -256,7 +258,7 @@ You could also just want to use the exception somehow, but then use the same def You can import and re-use the default exception handlers from `fastapi.exception_handlers`: ```Python hl_lines="2 3 4 5 15 21" -{!./src/handling_errors/tutorial006.py!} +{!../../../docs_src/handling_errors/tutorial006.py!} ``` In this example, you are just `print`ing the error with a very expressive message. diff --git a/docs/tutorial/header-params.md b/docs/en/docs/tutorial/header-params.md similarity index 92% rename from docs/tutorial/header-params.md rename to docs/en/docs/tutorial/header-params.md index 46c349bd11ff0..738d2a559ea90 100644 --- a/docs/tutorial/header-params.md +++ b/docs/en/docs/tutorial/header-params.md @@ -1,3 +1,5 @@ +# Header Parameters + You can define Header parameters the same way you define `Query`, `Path` and `Cookie` parameters. ## Import `Header` @@ -5,7 +7,7 @@ You can define Header parameters the same way you define `Query`, `Path` and `Co First import `Header`: ```Python hl_lines="1" -{!./src/header_params/tutorial001.py!} +{!../../../docs_src/header_params/tutorial001.py!} ``` ## Declare `Header` parameters @@ -15,7 +17,7 @@ Then declare the header parameters using the same structure as with `Path`, `Que The first value is the default value, you can pass all the extra validation or annotation parameters: ```Python hl_lines="7" -{!./src/header_params/tutorial001.py!} +{!../../../docs_src/header_params/tutorial001.py!} ``` !!! note "Technical Details" @@ -43,7 +45,7 @@ So, you can use `user_agent` as you normally would in Python code, instead of ne If for some reason you need to disable automatic conversion of underscores to hyphens, set the parameter `convert_underscores` of `Header` to `False`: ```Python hl_lines="7" -{!./src/header_params/tutorial002.py!} +{!../../../docs_src/header_params/tutorial002.py!} ``` !!! warning @@ -61,7 +63,7 @@ You will receive all the values from the duplicate header as a Python `list`. For example, to declare a header of `X-Token` that can appear more than once, you can write: ```Python hl_lines="9" -{!./src/header_params/tutorial003.py!} +{!../../../docs_src/header_params/tutorial003.py!} ``` If you communicate with that *path operation* sending two HTTP headers like: diff --git a/docs/tutorial/index.md b/docs/en/docs/tutorial/index.md similarity index 98% rename from docs/tutorial/index.md rename to docs/en/docs/tutorial/index.md index ae073518ae486..8fb93ab2b67c8 100644 --- a/docs/tutorial/index.md +++ b/docs/en/docs/tutorial/index.md @@ -1,3 +1,5 @@ +# Tutorial - User Guide - Intro + This tutorial shows you how to use **FastAPI** with most of its features, step by step. Each section gradually builds on the previous ones, but it's structured to separate topics, so that you can go directly to any specific one to solve your specific API needs. diff --git a/docs/tutorial/middleware.md b/docs/en/docs/tutorial/middleware.md similarity index 96% rename from docs/tutorial/middleware.md rename to docs/en/docs/tutorial/middleware.md index 5dd779189abc5..6f7ca000addf9 100644 --- a/docs/tutorial/middleware.md +++ b/docs/en/docs/tutorial/middleware.md @@ -1,3 +1,5 @@ +# Middleware + You can add middleware to **FastAPI** applications. A "middleware" is a function that works with every **request** before it is processed by any specific *path operation*. And also with every **response** before returning it. @@ -27,7 +29,7 @@ The middleware function receives: * You can then modify further the `response` before returning it. ```Python hl_lines="8 9 11 14" -{!./src/middleware/tutorial001.py!} +{!../../../docs_src/middleware/tutorial001.py!} ``` !!! tip @@ -49,7 +51,7 @@ And also after the `response` is generated, before returning it. For example, you could add a custom header `X-Process-Time` containing the time in seconds that it took to process the request and generate a response: ```Python hl_lines="10 12 13" -{!./src/middleware/tutorial001.py!} +{!../../../docs_src/middleware/tutorial001.py!} ``` ## Other middlewares diff --git a/docs/tutorial/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md similarity index 88% rename from docs/tutorial/path-operation-configuration.md rename to docs/en/docs/tutorial/path-operation-configuration.md index cfc970b7a9005..c2c3008944157 100644 --- a/docs/tutorial/path-operation-configuration.md +++ b/docs/en/docs/tutorial/path-operation-configuration.md @@ -1,3 +1,5 @@ +# Path Operation Configuration + There are several parameters that you can pass to your *path operation decorator* to configure it. !!! warning @@ -12,7 +14,7 @@ You can pass directly the `int` code, like `404`. But if you don't remember what each number code is for, you can use the shortcut constants in `status`: ```Python hl_lines="3 17" -{!./src/path_operation_configuration/tutorial001.py!} +{!../../../docs_src/path_operation_configuration/tutorial001.py!} ``` That status code will be used in the response and will be added to the OpenAPI schema. @@ -27,7 +29,7 @@ That status code will be used in the response and will be added to the OpenAPI s You can add tags to your *path operation*, pass the parameter `tags` with a `list` of `str` (commonly just one `str`): ```Python hl_lines="17 22 27" -{!./src/path_operation_configuration/tutorial002.py!} +{!../../../docs_src/path_operation_configuration/tutorial002.py!} ``` They will be added to the OpenAPI schema and used by the automatic documentation interfaces: @@ -39,7 +41,7 @@ They will be added to the OpenAPI schema and used by the automatic documentation You can add a `summary` and `description`: ```Python hl_lines="20 21" -{!./src/path_operation_configuration/tutorial003.py!} +{!../../../docs_src/path_operation_configuration/tutorial003.py!} ``` ## Description from docstring @@ -49,7 +51,7 @@ As descriptions tend to be long and cover multiple lines, you can declare the *p You can write Markdown in the docstring, it will be interpreted and displayed correctly (taking into account docstring indentation). ```Python hl_lines="19 20 21 22 23 24 25 26 27" -{!./src/path_operation_configuration/tutorial004.py!} +{!../../../docs_src/path_operation_configuration/tutorial004.py!} ``` It will be used in the interactive docs: @@ -61,7 +63,7 @@ It will be used in the interactive docs: You can specify the response description with the parameter `response_description`: ```Python hl_lines="21" -{!./src/path_operation_configuration/tutorial005.py!} +{!../../../docs_src/path_operation_configuration/tutorial005.py!} ``` !!! info @@ -79,7 +81,7 @@ You can specify the response description with the parameter `response_descriptio If you need to mark a *path operation* as deprecated, but without removing it, pass the parameter `deprecated`: ```Python hl_lines="16" -{!./src/path_operation_configuration/tutorial006.py!} +{!../../../docs_src/path_operation_configuration/tutorial006.py!} ``` It will be clearly marked as deprecated in the interactive docs: diff --git a/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md similarity index 89% rename from docs/tutorial/path-params-numeric-validations.md rename to docs/en/docs/tutorial/path-params-numeric-validations.md index 5fe638f5f3351..9ae4c8b1a480b 100644 --- a/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -1,3 +1,5 @@ +# Path Parameters and Numeric Validations + The same way you can declare more validations and metadata for query parameters with `Query`, you can declare the same type of validations and metadata for path parameters with `Path`. ## Import Path @@ -5,7 +7,7 @@ The same way you can declare more validations and metadata for query parameters First, import `Path` from `fastapi`: ```Python hl_lines="1" -{!./src/path_params_numeric_validations/tutorial001.py!} +{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} ``` ## Declare metadata @@ -15,7 +17,7 @@ You can declare all the same parameters as for `Query`. For example, to declare a `title` metadata value for the path parameter `item_id` you can type: ```Python hl_lines="8" -{!./src/path_params_numeric_validations/tutorial001.py!} +{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} ``` !!! note @@ -42,7 +44,7 @@ It doesn't matter for **FastAPI**. It will detect the parameters by their names, So, you can declare your function as: ```Python hl_lines="8" -{!./src/path_params_numeric_validations/tutorial002.py!} +{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} ``` ## Order the parameters as you need, tricks @@ -54,7 +56,7 @@ Pass `*`, as the first parameter of the function. Python won't do anything with that `*`, but it will know that all the following parameters should be called as keyword arguments (key-value pairs), also known as kwargs. Even if they don't have a default value. ```Python hl_lines="8" -{!./src/path_params_numeric_validations/tutorial003.py!} +{!../../../docs_src/path_params_numeric_validations/tutorial003.py!} ``` ## Number validations: greater than or equal @@ -64,7 +66,7 @@ With `Query` and `Path` (and other's you'll see later) you can declare string co Here, with `ge=1`, `item_id` will need to be an integer number "`g`reater than or `e`qual" to `1`. ```Python hl_lines="8" -{!./src/path_params_numeric_validations/tutorial004.py!} +{!../../../docs_src/path_params_numeric_validations/tutorial004.py!} ``` ## Number validations: greater than and less than or equal @@ -75,7 +77,7 @@ The same applies for: * `le`: `l`ess than or `e`qual ```Python hl_lines="9" -{!./src/path_params_numeric_validations/tutorial005.py!} +{!../../../docs_src/path_params_numeric_validations/tutorial005.py!} ``` ## Number validations: floats, greater than and less than @@ -89,7 +91,7 @@ So, `0.5` would be a valid value. But `0.0` or `0` would not. And the same for lt. ```Python hl_lines="11" -{!./src/path_params_numeric_validations/tutorial006.py!} +{!../../../docs_src/path_params_numeric_validations/tutorial006.py!} ``` ## Recap diff --git a/docs/tutorial/path-params.md b/docs/en/docs/tutorial/path-params.md similarity index 94% rename from docs/tutorial/path-params.md rename to docs/en/docs/tutorial/path-params.md index b4c4054551a3b..1c32108bb6a3e 100644 --- a/docs/tutorial/path-params.md +++ b/docs/en/docs/tutorial/path-params.md @@ -1,7 +1,9 @@ +# Path Parameters + You can declare path "parameters" or "variables" with the same syntax used by Python format strings: ```Python hl_lines="6 7" -{!./src/path_params/tutorial001.py!} +{!../../../docs_src/path_params/tutorial001.py!} ``` The value of the path parameter `item_id` will be passed to your function as the argument `item_id`. @@ -17,7 +19,7 @@ So, if you run this example and go to regular expression that the parameter should match: ```Python hl_lines="8" -{!./src/query_params_str_validations/tutorial004.py!} +{!../../../docs_src/query_params_str_validations/tutorial004.py!} ``` This specific regular expression checks that the received parameter value: @@ -85,7 +87,7 @@ The same way that you can pass `None` as the first argument to be used as the de Let's say that you want to declare the `q` query parameter to have a `min_length` of `3`, and to have a default value of `"fixedquery"`: ```Python hl_lines="7" -{!./src/query_params_str_validations/tutorial005.py!} +{!../../../docs_src/query_params_str_validations/tutorial005.py!} ``` !!! note @@ -114,7 +116,7 @@ q: str = Query(None, min_length=3) So, when you need to declare a value as required while using `Query`, you can use `...` as the first argument: ```Python hl_lines="7" -{!./src/query_params_str_validations/tutorial006.py!} +{!../../../docs_src/query_params_str_validations/tutorial006.py!} ``` !!! info @@ -129,7 +131,7 @@ When you define a query parameter explicitly with `Query` you can also declare i For example, to declare a query parameter `q` that can appear multiple times in the URL, you can write: ```Python hl_lines="9" -{!./src/query_params_str_validations/tutorial011.py!} +{!../../../docs_src/query_params_str_validations/tutorial011.py!} ``` Then, with a URL like: @@ -163,7 +165,7 @@ The interactive API docs will update accordingly, to allow multiple values: And you can also define a default `list` of values if none are provided: ```Python hl_lines="9" -{!./src/query_params_str_validations/tutorial012.py!} +{!../../../docs_src/query_params_str_validations/tutorial012.py!} ``` If you go to: @@ -188,7 +190,7 @@ the default of `q` will be: `["foo", "bar"]` and your response will be: You can also use `list` directly instead of `List[str]`: ```Python hl_lines="7" -{!./src/query_params_str_validations/tutorial013.py!} +{!../../../docs_src/query_params_str_validations/tutorial013.py!} ``` !!! note @@ -210,13 +212,13 @@ That information will be included in the generated OpenAPI and used by the docum You can add a `title`: ```Python hl_lines="7" -{!./src/query_params_str_validations/tutorial007.py!} +{!../../../docs_src/query_params_str_validations/tutorial007.py!} ``` And a `description`: ```Python hl_lines="11" -{!./src/query_params_str_validations/tutorial008.py!} +{!../../../docs_src/query_params_str_validations/tutorial008.py!} ``` ## Alias parameters @@ -238,7 +240,7 @@ But you still need it to be exactly `item-query`... Then you can declare an `alias`, and that alias is what will be used to find the parameter value: ```Python hl_lines="7" -{!./src/query_params_str_validations/tutorial009.py!} +{!../../../docs_src/query_params_str_validations/tutorial009.py!} ``` ## Deprecating parameters @@ -250,7 +252,7 @@ You have to leave it there a while because there are clients using it, but you w Then pass the parameter `deprecated=True` to `Query`: ```Python hl_lines="16" -{!./src/query_params_str_validations/tutorial010.py!} +{!../../../docs_src/query_params_str_validations/tutorial010.py!} ``` The docs will show it like this: diff --git a/docs/tutorial/query-params.md b/docs/en/docs/tutorial/query-params.md similarity index 92% rename from docs/tutorial/query-params.md rename to docs/en/docs/tutorial/query-params.md index 5e72301105ad3..0efc57e1b87e1 100644 --- a/docs/tutorial/query-params.md +++ b/docs/en/docs/tutorial/query-params.md @@ -1,7 +1,9 @@ +# Query Parameters + When you declare other function parameters that are not part of the path parameters, they are automatically interpreted as "query" parameters. ```Python hl_lines="9" -{!./src/query_params/tutorial001.py!} +{!../../../docs_src/query_params/tutorial001.py!} ``` The query is the set of key-value pairs that go after the `?` in a URL, separated by `&` characters. @@ -62,7 +64,7 @@ The parameter values in your function will be: The same way, you can declare optional query parameters, by setting their default to `None`: ```Python hl_lines="7" -{!./src/query_params/tutorial002.py!} +{!../../../docs_src/query_params/tutorial002.py!} ``` In this case, the function parameter `q` will be optional, and will be `None` by default. @@ -75,7 +77,7 @@ In this case, the function parameter `q` will be optional, and will be `None` by You can also declare `bool` types, and they will be converted: ```Python hl_lines="7" -{!./src/query_params/tutorial003.py!} +{!../../../docs_src/query_params/tutorial003.py!} ``` In this case, if you go to: @@ -120,7 +122,7 @@ And you don't have to declare them in any specific order. They will be detected by name: ```Python hl_lines="6 8" -{!./src/query_params/tutorial004.py!} +{!../../../docs_src/query_params/tutorial004.py!} ``` ## Required query parameters @@ -132,7 +134,7 @@ If you don't want to add a specific value but just make it optional, set the def But when you want to make a query parameter required, you can just not declare any default value: ```Python hl_lines="6 7" -{!./src/query_params/tutorial005.py!} +{!../../../docs_src/query_params/tutorial005.py!} ``` Here the query parameter `needy` is a required query parameter of type `str`. @@ -178,7 +180,7 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy And of course, you can define some parameters as required, some as having a default value, and some entirely optional: ```Python hl_lines="7" -{!./src/query_params/tutorial006.py!} +{!../../../docs_src/query_params/tutorial006.py!} ``` In this case, there are 3 query parameters: @@ -220,5 +222,5 @@ limit: Optional[int] = None In a *path operation* that could look like: ```Python hl_lines="9" -{!./src/query_params/tutorial007.py!} +{!../../../docs_src/query_params/tutorial007.py!} ``` diff --git a/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md similarity index 96% rename from docs/tutorial/request-files.md rename to docs/en/docs/tutorial/request-files.md index b14f45c702f22..260a5e1905aeb 100644 --- a/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -1,3 +1,5 @@ +# Request Files + You can define files to be uploaded by the client using `File`. !!! info @@ -12,7 +14,7 @@ You can define files to be uploaded by the client using `File`. Import `File` and `UploadFile` from `fastapi`: ```Python hl_lines="1" -{!./src/request_files/tutorial001.py!} +{!../../../docs_src/request_files/tutorial001.py!} ``` ## Define `File` parameters @@ -20,7 +22,7 @@ Import `File` and `UploadFile` from `fastapi`: Create file parameters the same way you would for `Body` or `Form`: ```Python hl_lines="7" -{!./src/request_files/tutorial001.py!} +{!../../../docs_src/request_files/tutorial001.py!} ``` !!! info @@ -44,7 +46,7 @@ But there are several cases in where you might benefit from using `UploadFile`. Define a `File` parameter with a type of `UploadFile`: ```Python hl_lines="12" -{!./src/request_files/tutorial001.py!} +{!../../../docs_src/request_files/tutorial001.py!} ``` Using `UploadFile` has several advantages over `bytes`: @@ -120,7 +122,7 @@ They would be associated to the same "form field" sent using "form data". To use that, declare a `List` of `bytes` or `UploadFile`: ```Python hl_lines="10 15" -{!./src/request_files/tutorial002.py!} +{!../../../docs_src/request_files/tutorial002.py!} ``` You will receive, as declared, a `list` of `bytes` or `UploadFile`s. diff --git a/docs/tutorial/request-forms-and-files.md b/docs/en/docs/tutorial/request-forms-and-files.md similarity index 88% rename from docs/tutorial/request-forms-and-files.md rename to docs/en/docs/tutorial/request-forms-and-files.md index 51ba786386c0c..6844f8c658f2b 100644 --- a/docs/tutorial/request-forms-and-files.md +++ b/docs/en/docs/tutorial/request-forms-and-files.md @@ -1,3 +1,5 @@ +# Request Forms and Files + You can define files and form fields at the same time using `File` and `Form`. !!! info @@ -8,7 +10,7 @@ You can define files and form fields at the same time using `File` and `Form`. ## Import `File` and `Form` ```Python hl_lines="1" -{!./src/request_forms_and_files/tutorial001.py!} +{!../../../docs_src/request_forms_and_files/tutorial001.py!} ``` ## Define `File` and `Form` parameters @@ -16,7 +18,7 @@ You can define files and form fields at the same time using `File` and `Form`. Create file and form parameters the same way you would for `Body` or `Query`: ```Python hl_lines="8" -{!./src/request_forms_and_files/tutorial001.py!} +{!../../../docs_src/request_forms_and_files/tutorial001.py!} ``` The files and form fields will be uploaded as form data and you will receive the files and form fields. diff --git a/docs/tutorial/request-forms.md b/docs/en/docs/tutorial/request-forms.md similarity index 95% rename from docs/tutorial/request-forms.md rename to docs/en/docs/tutorial/request-forms.md index edb9af64174ef..b5495a4005610 100644 --- a/docs/tutorial/request-forms.md +++ b/docs/en/docs/tutorial/request-forms.md @@ -1,3 +1,5 @@ +# Form Data + When you need to receive form fields instead of JSON, you can use `Form`. !!! info @@ -10,7 +12,7 @@ When you need to receive form fields instead of JSON, you can use `Form`. Import `Form` from `fastapi`: ```Python hl_lines="1" -{!./src/request_forms/tutorial001.py!} +{!../../../docs_src/request_forms/tutorial001.py!} ``` ## Define `Form` parameters @@ -18,7 +20,7 @@ Import `Form` from `fastapi`: Create form parameters the same way you would for `Body` or `Query`: ```Python hl_lines="7" -{!./src/request_forms/tutorial001.py!} +{!../../../docs_src/request_forms/tutorial001.py!} ``` For example, in one of the ways the OAuth2 specification can be used (called "password flow") it is required to send a `username` and `password` as form fields. diff --git a/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md similarity index 92% rename from docs/tutorial/response-model.md rename to docs/en/docs/tutorial/response-model.md index 1f1288ca7c4d1..3f06cd4d573b5 100644 --- a/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -1,3 +1,5 @@ +# Response Model + You can declare the model used for the response with the parameter `response_model` in any of the *path operations*: * `@app.get()` @@ -7,7 +9,7 @@ You can declare the model used for the response with the parameter `response_mod * etc. ```Python hl_lines="17" -{!./src/response_model/tutorial001.py!} +{!../../../docs_src/response_model/tutorial001.py!} ``` !!! note @@ -34,13 +36,13 @@ But most importantly: Here we are declaring a `UserIn` model, it will contain a plaintext password: ```Python hl_lines="7 9" -{!./src/response_model/tutorial002.py!} +{!../../../docs_src/response_model/tutorial002.py!} ``` And we are using this model to declare our input and the same model to declare our output: ```Python hl_lines="15 16" -{!./src/response_model/tutorial002.py!} +{!../../../docs_src/response_model/tutorial002.py!} ``` Now, whenever a browser is creating a user with a password, the API will return the same password in the response. @@ -57,19 +59,19 @@ But if we use the same model for another *path operation*, we could be sending o We can instead create an input model with the plaintext password and an output model without it: ```Python hl_lines="7 9 14" -{!./src/response_model/tutorial003.py!} +{!../../../docs_src/response_model/tutorial003.py!} ``` Here, even though our *path operation function* is returning the same input user that contains the password: ```Python hl_lines="22" -{!./src/response_model/tutorial003.py!} +{!../../../docs_src/response_model/tutorial003.py!} ``` ...we declared the `response_model` to be our model `UserOut`, that doesn't include the password: ```Python hl_lines="20" -{!./src/response_model/tutorial003.py!} +{!../../../docs_src/response_model/tutorial003.py!} ``` So, **FastAPI** will take care of filtering out all the data that is not declared in the output model (using Pydantic). @@ -89,7 +91,7 @@ And both models will be used for the interactive API documentation: Your response model could have default values, like: ```Python hl_lines="11 13 14" -{!./src/response_model/tutorial004.py!} +{!../../../docs_src/response_model/tutorial004.py!} ``` * `description: str = None` has a default of `None`. @@ -105,7 +107,7 @@ For example, if you have models with many optional attributes in a NoSQL databas You can set the *path operation decorator* parameter `response_model_exclude_unset=True`: ```Python hl_lines="24" -{!./src/response_model/tutorial004.py!} +{!../../../docs_src/response_model/tutorial004.py!} ``` and those default values won't be included in the response, only the values actually set. @@ -174,7 +176,7 @@ This can be used as a quick shortcut if you have only one Pydantic model and wan This is because the JSON Schema generated in your app's OpenAPI (and the docs) will still be the one for the complete model, even if you use `response_model_include` or `response_model_exclude` to omit some attributes. ```Python hl_lines="29 35" -{!./src/response_model/tutorial005.py!} +{!../../../docs_src/response_model/tutorial005.py!} ``` !!! tip @@ -187,7 +189,7 @@ This can be used as a quick shortcut if you have only one Pydantic model and wan If you forget to use a `set` and use a `list` or `tuple` instead, FastAPI will still convert it to a `set` and it will work correctly: ```Python hl_lines="29 35" -{!./src/response_model/tutorial006.py!} +{!../../../docs_src/response_model/tutorial006.py!} ``` ## Recap diff --git a/docs/tutorial/response-status-code.md b/docs/en/docs/tutorial/response-status-code.md similarity index 94% rename from docs/tutorial/response-status-code.md rename to docs/en/docs/tutorial/response-status-code.md index 3f77272de3f58..29b8521fc737f 100644 --- a/docs/tutorial/response-status-code.md +++ b/docs/en/docs/tutorial/response-status-code.md @@ -1,3 +1,5 @@ +# Response Status Code + The same way you can specify a response model, you can also declare the HTTP status code used for the response with the parameter `status_code` in any of the *path operations*: * `@app.get()` @@ -7,7 +9,7 @@ The same way you can specify a response model, you can also declare the HTTP sta * etc. ```Python hl_lines="6" -{!./src/response_status_code/tutorial001.py!} +{!../../../docs_src/response_status_code/tutorial001.py!} ``` !!! note @@ -57,7 +59,7 @@ In short: Let's see the previous example again: ```Python hl_lines="6" -{!./src/response_status_code/tutorial001.py!} +{!../../../docs_src/response_status_code/tutorial001.py!} ``` `201` is the status code for "Created". @@ -67,7 +69,7 @@ But you don't have to memorize what each of these codes mean. You can use the convenience variables from `fastapi.status`. ```Python hl_lines="1 6" -{!./src/response_status_code/tutorial002.py!} +{!../../../docs_src/response_status_code/tutorial002.py!} ``` They are just a convenience, they hold the same number, but that way you can use the editor's autocomplete to find them: diff --git a/docs/tutorial/security/first-steps.md b/docs/en/docs/tutorial/security/first-steps.md similarity index 97% rename from docs/tutorial/security/first-steps.md rename to docs/en/docs/tutorial/security/first-steps.md index 6fef16235585c..c14ec2ad984ff 100644 --- a/docs/tutorial/security/first-steps.md +++ b/docs/en/docs/tutorial/security/first-steps.md @@ -1,3 +1,5 @@ +# Security - First Steps + Let's imagine that you have your **backend** API in some domain. And you have a **frontend** in another domain or in a different path of the same domain (or in a mobile application). @@ -19,7 +21,7 @@ Let's first just use the code and see how it works, and then we'll come back to Copy the example in a file `main.py`: ```Python -{!./src/security/tutorial001.py!} +{!../../../docs_src/security/tutorial001.py!} ``` ## Run it @@ -115,7 +117,7 @@ In this example we are going to use **OAuth2**, with the **Password** flow, usin `OAuth2PasswordBearer` is a class that we create passing a parameter of the URL in where the client (the frontend running in the user's browser) can use to send the `username` and `password` and get a token. ```Python hl_lines="6" -{!./src/security/tutorial001.py!} +{!../../../docs_src/security/tutorial001.py!} ``` It doesn't create that endpoint / *path operation*, but declares that that URL is the one that the client should use to get the token. That information is used in OpenAPI, and then in the interactive API documentation systems. @@ -140,7 +142,7 @@ So, it can be used with `Depends`. Now you can pass that `oauth2_scheme` in a dependency with `Depends`. ```Python hl_lines="10" -{!./src/security/tutorial001.py!} +{!../../../docs_src/security/tutorial001.py!} ``` This dependency will provide a `str` that is assigned to the parameter `token` of the *path operation function*. diff --git a/docs/tutorial/security/get-current-user.md b/docs/en/docs/tutorial/security/get-current-user.md similarity index 92% rename from docs/tutorial/security/get-current-user.md rename to docs/en/docs/tutorial/security/get-current-user.md index e2d6b7924cacd..3fc0164c3263f 100644 --- a/docs/tutorial/security/get-current-user.md +++ b/docs/en/docs/tutorial/security/get-current-user.md @@ -1,7 +1,9 @@ +# Get Current User + In the previous chapter the security system (which is based on the dependency injection system) was giving the *path operation function* a `token` as a `str`: ```Python hl_lines="10" -{!./src/security/tutorial001.py!} +{!../../../docs_src/security/tutorial001.py!} ``` But that is still not that useful. @@ -15,7 +17,7 @@ First, let's create a Pydantic user model. The same way we use Pydantic to declare bodies, we can use it anywhere else: ```Python hl_lines="5 12 13 14 15 16" -{!./src/security/tutorial002.py!} +{!../../../docs_src/security/tutorial002.py!} ``` ## Create a `get_current_user` dependency @@ -29,7 +31,7 @@ Remember that dependencies can have sub-dependencies? The same as we were doing before in the *path operation* directly, our new dependency `get_current_user` will receive a `token` as a `str` from the sub-dependency `oauth2_scheme`: ```Python hl_lines="25" -{!./src/security/tutorial002.py!} +{!../../../docs_src/security/tutorial002.py!} ``` ## Get the user @@ -37,7 +39,7 @@ The same as we were doing before in the *path operation* directly, our new depen `get_current_user` will use a (fake) utility function we created, that takes a token as a `str` and returns our Pydantic `User` model: ```Python hl_lines="19 20 21 22 26 27" -{!./src/security/tutorial002.py!} +{!../../../docs_src/security/tutorial002.py!} ``` ## Inject the current user @@ -45,7 +47,7 @@ The same as we were doing before in the *path operation* directly, our new depen So now we can use the same `Depends` with our `get_current_user` in the *path operation*: ```Python hl_lines="31" -{!./src/security/tutorial002.py!} +{!../../../docs_src/security/tutorial002.py!} ``` Notice that we declare the type of `current_user` as the Pydantic model `User`. @@ -97,7 +99,7 @@ And all of them (or any portion of them that you want) can take the advantage of And all these thousands of *path operations* can be as small as 3 lines: ```Python hl_lines="30 31 32" -{!./src/security/tutorial002.py!} +{!../../../docs_src/security/tutorial002.py!} ``` ## Recap diff --git a/docs/tutorial/security/index.md b/docs/en/docs/tutorial/security/index.md similarity index 99% rename from docs/tutorial/security/index.md rename to docs/en/docs/tutorial/security/index.md index 96c941dd58297..9aed2adb5d3a5 100644 --- a/docs/tutorial/security/index.md +++ b/docs/en/docs/tutorial/security/index.md @@ -1,3 +1,5 @@ +# Security Intro + There are many ways to handle security, authentication and authorization. And it normally is a complex and "difficult" topic. diff --git a/docs/tutorial/security/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md similarity index 97% rename from docs/tutorial/security/oauth2-jwt.md rename to docs/en/docs/tutorial/security/oauth2-jwt.md index cd15cb9d293fc..18b4e0e300c1a 100644 --- a/docs/tutorial/security/oauth2-jwt.md +++ b/docs/en/docs/tutorial/security/oauth2-jwt.md @@ -1,3 +1,5 @@ +# OAuth2 with Password (and hashing), Bearer with JWT tokens + Now that we have all the security flow, let's make the application actually secure, using JWT tokens and secure password hashing. This code is something you can actually use in your application, save the password hashes in your database, etc. @@ -99,7 +101,7 @@ And another utility to verify if a received password matches the hash stored. And another one to authenticate and return a user. ```Python hl_lines="7 48 55 56 59 60 69 70 71 72 73 74 75" -{!./src/security/tutorial004.py!} +{!../../../docs_src/security/tutorial004.py!} ``` !!! note @@ -134,7 +136,7 @@ Define a Pydantic Model that will be used in the token endpoint for the response Create a utility function to generate a new access token. ```Python hl_lines="3 6 12 13 14 28 29 30 78 79 80 81 82 83 84 85 86" -{!./src/security/tutorial004.py!} +{!../../../docs_src/security/tutorial004.py!} ``` ## Update the dependencies @@ -146,7 +148,7 @@ Decode the received token, verify it, and return the current user. If the token is invalid, return an HTTP error right away. ```Python hl_lines="89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106" -{!./src/security/tutorial004.py!} +{!../../../docs_src/security/tutorial004.py!} ``` ## Update the `/token` *path operation* @@ -156,7 +158,7 @@ Create a `timedelta` with the expiration time of the token. Create a real JWT access token and return it. ```Python hl_lines="115 116 117 118 119 120 121 122 123 124 125 126 127 128" -{!./src/security/tutorial004.py!} +{!../../../docs_src/security/tutorial004.py!} ``` ### Technical details about the JWT "subject" `sub` diff --git a/docs/tutorial/security/simple-oauth2.md b/docs/en/docs/tutorial/security/simple-oauth2.md similarity index 97% rename from docs/tutorial/security/simple-oauth2.md rename to docs/en/docs/tutorial/security/simple-oauth2.md index d7507a1ec8183..c95c6aa6b62c6 100644 --- a/docs/tutorial/security/simple-oauth2.md +++ b/docs/en/docs/tutorial/security/simple-oauth2.md @@ -1,3 +1,5 @@ +# Simple OAuth2 with Password and Bearer + Now let's build from the previous chapter and add the missing parts to have a complete security flow. ## Get the `username` and `password` @@ -48,7 +50,7 @@ Now let's use the utilities provided by **FastAPI** to handle this. First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depends` for the path `/token`: ```Python hl_lines="2 74" -{!./src/security/tutorial003.py!} +{!../../../docs_src/security/tutorial003.py!} ``` `OAuth2PasswordRequestForm` is a class dependency that declares a form body with: @@ -89,7 +91,7 @@ If there is no such user, we return an error saying "incorrect username or passw For the error, we use the exception `HTTPException`: ```Python hl_lines="1 75 76 77" -{!./src/security/tutorial003.py!} +{!../../../docs_src/security/tutorial003.py!} ``` ### Check the password @@ -117,7 +119,7 @@ If your database is stolen, the thief won't have your users' plaintext passwords So, the thief won't be able to try to use those same passwords in another system (as many users use the same password everywhere, this would be dangerous). ```Python hl_lines="78 79 80 81" -{!./src/security/tutorial003.py!} +{!../../../docs_src/security/tutorial003.py!} ``` #### About `**user_dict` @@ -155,7 +157,7 @@ For this simple example, we are going to just be completely insecure and return But for now, let's focus on the specific details we need. ```Python hl_lines="83" -{!./src/security/tutorial003.py!} +{!../../../docs_src/security/tutorial003.py!} ``` !!! tip @@ -180,7 +182,7 @@ Both of these dependencies will just return an HTTP error if the user doesn't ex So, in our endpoint, we will only get a user if the user exists, was correctly authenticated, and is active: ```Python hl_lines="56 57 58 59 60 61 62 63 64 65 67 68 69 70 88" -{!./src/security/tutorial003.py!} +{!../../../docs_src/security/tutorial003.py!} ``` !!! info diff --git a/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md similarity index 95% rename from docs/tutorial/sql-databases.md rename to docs/en/docs/tutorial/sql-databases.md index ba4b836483140..179e75f0a9ec6 100644 --- a/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -1,3 +1,5 @@ +# SQL (Relational) Databases + **FastAPI** doesn't require you to use a SQL (relational) database. But you can use any relational database that you want. @@ -85,13 +87,13 @@ Let's refer to the file `sql_app/database.py`. ### Import the SQLAlchemy parts ```Python hl_lines="1 2 3" -{!./src/sql_databases/sql_app/database.py!} +{!../../../docs_src/sql_databases/sql_app/database.py!} ``` ### Create a database URL for SQLAlchemy ```Python hl_lines="5 6" -{!./src/sql_databases/sql_app/database.py!} +{!../../../docs_src/sql_databases/sql_app/database.py!} ``` In this example, we are "connecting" to a SQLite database (opening a file with the SQLite database). @@ -119,7 +121,7 @@ The first step is to create a SQLAlchemy "engine". We will later use this `engine` in other places. ```Python hl_lines="8 9 10" -{!./src/sql_databases/sql_app/database.py!} +{!../../../docs_src/sql_databases/sql_app/database.py!} ``` #### Note @@ -155,7 +157,7 @@ We will use `Session` (the one imported from SQLAlchemy) later. To create the `SessionLocal` class, use the function `sessionmaker`: ```Python hl_lines="11" -{!./src/sql_databases/sql_app/database.py!} +{!../../../docs_src/sql_databases/sql_app/database.py!} ``` ### Create a `Base` class @@ -165,7 +167,7 @@ Now we will use the function `declarative_base()` that returns a class. Later we will inherit from this class to create each of the database models or classes (the ORM models): ```Python hl_lines="13" -{!./src/sql_databases/sql_app/database.py!} +{!../../../docs_src/sql_databases/sql_app/database.py!} ``` ## Create the database models @@ -188,7 +190,7 @@ Create classes that inherit from it. These classes are the SQLAlchemy models. ```Python hl_lines="4 7 8 18 19" -{!./src/sql_databases/sql_app/models.py!} +{!../../../docs_src/sql_databases/sql_app/models.py!} ``` The `__tablename__` attribute tells SQLAlchemy the name of the table to use in the database for each of these models. @@ -204,7 +206,7 @@ We use `Column` from SQLAlchemy as the default value. And we pass a SQLAlchemy class "type", as `Integer`, `String`, and `Boolean`, that defines the type in the database, as an argument. ```Python hl_lines="1 10 11 12 13 21 22 23 24" -{!./src/sql_databases/sql_app/models.py!} +{!../../../docs_src/sql_databases/sql_app/models.py!} ``` ### Create the relationships @@ -216,7 +218,7 @@ For this, we use `relationship` provided by SQLAlchemy ORM. This will become, more or less, a "magic" attribute that will contain the values from other tables related to this one. ```Python hl_lines="2 15 26" -{!./src/sql_databases/sql_app/models.py!} +{!../../../docs_src/sql_databases/sql_app/models.py!} ``` When accessing the attribute `items` in a `User`, as in `my_user.items`, it will have a list of `Item` SQLAlchemy models (from the `items` table) that have a foreign key pointing to this record in the `users` table. @@ -247,7 +249,7 @@ So, the user will also have a `password` when creating it. But for security, the `password` won't be in other Pydantic *models*, for example, it won't be sent from the API when reading a user. ```Python hl_lines="3 6 7 8 11 12 23 24 27 28" -{!./src/sql_databases/sql_app/schemas.py!} +{!../../../docs_src/sql_databases/sql_app/schemas.py!} ``` #### SQLAlchemy style and Pydantic style @@ -277,7 +279,7 @@ The same way, when reading a user, we can now declare that `items` will contain Not only the IDs of those items, but all the data that we defined in the Pydantic *model* for reading items: `Item`. ```Python hl_lines="15 16 17 31 32 33 34" -{!./src/sql_databases/sql_app/schemas.py!} +{!../../../docs_src/sql_databases/sql_app/schemas.py!} ``` !!! tip @@ -292,7 +294,7 @@ This Starlette, testing **FastAPI** applications is easy and enjoyable. It is based on Requests, so it's very familiar and intuitive. @@ -17,7 +19,7 @@ Use the `TestClient` object the same way as you do with `requests`. Write simple `assert` statements with the standard Python expressions that you need to check (again, standard `pytest`). ```Python hl_lines="2 12 15 16 17 18" -{!./src/app_testing/tutorial001.py!} +{!../../../docs_src/app_testing/tutorial001.py!} ``` !!! tip @@ -43,7 +45,7 @@ And your **FastAPI** application might also be composed of several files/modules Let's say you have a file `main.py` with your **FastAPI** app: ```Python -{!./src/app_testing/main.py!} +{!../../../docs_src/app_testing/main.py!} ``` ### Testing file @@ -51,7 +53,7 @@ Let's say you have a file `main.py` with your **FastAPI** app: Then you could have a file `test_main.py` with your tests, and import your `app` from the `main` module (`main.py`): ```Python -{!./src/app_testing/test_main.py!} +{!../../../docs_src/app_testing/test_main.py!} ``` ## Testing: extended example @@ -69,7 +71,7 @@ It has a `POST` operation that could return several errors. Both *path operations* require an `X-Token` header. ```Python -{!./src/app_testing/main_b.py!} +{!../../../docs_src/app_testing/main_b.py!} ``` ### Extended testing file @@ -77,7 +79,7 @@ Both *path operations* require an `X-Token` header. You could then have a `test_main_b.py`, the same as before, with the extended tests: ```Python -{!./src/app_testing/test_main_b.py!} +{!../../../docs_src/app_testing/test_main_b.py!} ``` Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it in `requests`. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml new file mode 100644 index 0000000000000..eb3b54e9481cd --- /dev/null +++ b/docs/en/mkdocs.yml @@ -0,0 +1,147 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/ +theme: + name: material + palette: + primary: teal + accent: amber + logo: img/icon-white.svg + favicon: img/favicon.png + language: en +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +google_analytics: +- UA-133183413-1 +- auto +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +- python-types.md +- Tutorial - User Guide: + - tutorial/index.md + - tutorial/first-steps.md + - tutorial/path-params.md + - tutorial/query-params.md + - tutorial/body.md + - tutorial/query-params-str-validations.md + - tutorial/path-params-numeric-validations.md + - tutorial/body-multiple-params.md + - tutorial/body-fields.md + - tutorial/body-nested-models.md + - tutorial/extra-data-types.md + - tutorial/cookie-params.md + - tutorial/header-params.md + - tutorial/response-model.md + - tutorial/extra-models.md + - tutorial/response-status-code.md + - tutorial/request-forms.md + - tutorial/request-files.md + - tutorial/request-forms-and-files.md + - tutorial/handling-errors.md + - tutorial/path-operation-configuration.md + - tutorial/encoder.md + - tutorial/body-updates.md + - Dependencies: + - tutorial/dependencies/index.md + - tutorial/dependencies/classes-as-dependencies.md + - tutorial/dependencies/sub-dependencies.md + - tutorial/dependencies/dependencies-in-path-operation-decorators.md + - tutorial/dependencies/dependencies-with-yield.md + - Security: + - tutorial/security/index.md + - tutorial/security/first-steps.md + - tutorial/security/get-current-user.md + - tutorial/security/simple-oauth2.md + - tutorial/security/oauth2-jwt.md + - tutorial/middleware.md + - tutorial/cors.md + - tutorial/sql-databases.md + - tutorial/bigger-applications.md + - tutorial/background-tasks.md + - tutorial/application-configuration.md + - tutorial/static-files.md + - tutorial/testing.md + - tutorial/debugging.md +- Advanced User Guide: + - advanced/index.md + - advanced/path-operation-advanced-configuration.md + - advanced/additional-status-codes.md + - advanced/response-directly.md + - advanced/custom-response.md + - advanced/additional-responses.md + - advanced/response-cookies.md + - advanced/response-headers.md + - advanced/response-change-status-code.md + - advanced/advanced-dependencies.md + - Advanced Security: + - advanced/security/index.md + - advanced/security/oauth2-scopes.md + - advanced/security/http-basic-auth.md + - advanced/using-request-directly.md + - advanced/middleware.md + - advanced/sql-databases-peewee.md + - advanced/async-sql-databases.md + - advanced/nosql-databases.md + - advanced/sub-applications-proxy.md + - advanced/templates.md + - advanced/graphql.md + - advanced/websockets.md + - advanced/events.md + - advanced/custom-request-and-route.md + - advanced/testing-websockets.md + - advanced/testing-events.md + - advanced/testing-dependencies.md + - advanced/extending-openapi.md + - advanced/openapi-callbacks.md + - advanced/wsgi.md +- async.md +- deployment.md +- project-generation.md +- alternatives.md +- history-design-future.md +- external-links.md +- benchmarks.md +- help-fastapi.md +- contributing.md +- release-notes.md +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- markdown_include.include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_div_format '' +extra: + social: + - type: github + link: https://github.com/tiangolo/typer + - type: twitter + link: https://twitter.com/tiangolo + - type: linkedin + link: https://www.linkedin.com/in/tiangolo + - type: rss + link: https://dev.to/tiangolo + - type: medium + link: https://medium.com/@tiangolo + - type: globe + link: https://tiangolo.com +extra_css: +- css/termynal.css +- css/custom.css +extra_javascript: +- https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js +- js/termynal.js +- js/custom.js diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md new file mode 100644 index 0000000000000..11c9453720dc7 --- /dev/null +++ b/docs/es/docs/index.md @@ -0,0 +1,437 @@ +

+ FastAPI +

+

+ FastAPI framework, high performance, easy to learn, fast to code, ready for production +

+

+ + Build Status + + + Coverage + + + Package version + + + Join the chat at https://gitter.im/tiangolo/fastapi + +

+ +--- + +**Documentation**: https://fastapi.tiangolo.com + +**Source Code**: https://github.com/tiangolo/fastapi + +--- + +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. + +The key features are: + +* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). + +* **Fast to code**: Increase the speed to develop features by about 200% to 300% *. +* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * +* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. +* **Easy**: Designed to be easy to use and learn. Less time reading docs. +* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. +* **Robust**: Get production-ready code. With automatic interactive documentation. +* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. + +* estimation based on tests on an internal development team, building production applications. + +## Opinions + +"*[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products.*" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"*I’m over the moon excited about **FastAPI**. It’s so fun!*" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"*Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that.*" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*" + +"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+ +--- + +"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*" + +
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+ +--- + +## **Typer**, the FastAPI of CLIs + + + +If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. + +**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 + +## Requirements + +Python 3.6+ + +FastAPI stands on the shoulders of giants: + +* Starlette for the web parts. +* Pydantic for the data parts. + +## Installation + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +You will also need an ASGI server, for production such as Uvicorn or Hypercorn. + +
+ +```console +$ pip install uvicorn + +---> 100% +``` + +
+ +## Example + +### Create it + +* Create a file `main.py` 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} +``` + +
+Or use async def... + +If your code uses `async` / `await`, use `async def`: + +```Python hl_lines="7 12" +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: str = None): + return {"item_id": item_id, "q": q} +``` + +**Note**: + +If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. + +
+ +### Run it + +Run the server with: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+About the command uvicorn main:app --reload... + +The command `uvicorn main:app` refers to: + +* `main`: the file `main.py` (the Python "module"). +* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +* `--reload`: make the server restart after code changes. Only do this for development. + +
+ +### Check it + +Open your browser at http://127.0.0.1:8000/items/5?q=somequery. + +You will see the JSON response as: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +You already created an API that: + +* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. +* Both _paths_ take `GET` operations (also known as HTTP _methods_). +* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. +* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. + +### Interactive API docs + +Now go to http://127.0.0.1:8000/docs. + +You will see the automatic interactive API documentation (provided by Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternative API docs + +And now, go to http://127.0.0.1:8000/redoc. + +You will see the alternative automatic documentation (provided by ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Example upgrade + +Now modify the file `main.py` to receive a body from a `PUT` request. + +Declare the body using standard Python types, thanks to Pydantic. + +```Python hl_lines="2 7 8 9 10 23 24 25" +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: bool = None + + +@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} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +The server should reload automatically (because you added `--reload` to the `uvicorn` command above). + +### Interactive API docs upgrade + +Now go to http://127.0.0.1:8000/docs. + +* The interactive API documentation will be automatically updated, including the new body: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternative API docs upgrade + +And now, go to http://127.0.0.1:8000/redoc. + +* The alternative documentation will also reflect the new query parameter and body: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Recap + +In summary, you declare **once** the types of parameters, body, etc. as function parameters. + +You do that with standard modern Python types. + +You don't have to learn a new syntax, the methods or classes of a specific library, etc. + +Just standard **Python 3.6+**. + +For example, for an `int`: + +```Python +item_id: int +``` + +or for a more complex `Item` model: + +```Python +item: Item +``` + +...and with that single declaration you get: + +* Editor support, including: + * Completion. + * Type checks. +* Validation of data: + * Automatic and clear errors when the data is invalid. + * Validation even for deeply nested JSON objects. +* Conversion of input data: coming from the network to Python data and types. Reading from: + * JSON. + * Path parameters. + * Query parameters. + * Cookies. + * Headers. + * Forms. + * Files. +* Conversion of output data: converting from Python data and types to network data (as JSON): + * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` objects. + * `UUID` objects. + * Database models. + * ...and many more. +* Automatic interactive API documentation, including 2 alternative user interfaces: + * Swagger UI. + * ReDoc. + +--- + +Coming back to the previous code example, **FastAPI** will: + +* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. +* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. + * If it is not, the client will see a useful, clear error. +* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. + * As the `q` parameter is declared with `= None`, it is optional. + * Without the `None` it would be required (as is the body in the case with `PUT`). +* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: + * Check that it has a required attribute `name` that should be a `str`. + * Check that it has a required attribute `price` that has to be a `float`. + * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. + * All this would also work for deeply nested JSON objects. +* Convert from and to JSON automatically. +* Document everything with OpenAPI, that can be used by: + * Interactive documentation systems. + * Automatic client code generation systems, for many languages. +* Provide 2 interactive documentation web interfaces directly. + +--- + +We just scratched the surface, but you already get the idea of how it all works. + +Try changing the line with: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...from: + +```Python + ... "item_name": item.name ... +``` + +...to: + +```Python + ... "item_price": item.price ... +``` + +...and see how your editor will auto-complete the attributes and know their types: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +For a more complete example including more features, see the Tutorial - User Guide. + +**Spoiler alert**: the tutorial - user guide includes: + +* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. +* How to set **validation constraints** as `maximum_length` or `regex`. +* A very powerful and easy to use **Dependency Injection** system. +* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. +* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). +* Many extra features (thanks to Starlette) as: + * **WebSockets** + * **GraphQL** + * extremely easy tests based on `requests` and `pytest` + * **CORS** + * **Cookie Sessions** + * ...and more. + +## Performance + +Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) + +To understand more about it, see the section Benchmarks. + +## Optional Dependencies + +Used by Pydantic: + +* ujson - for faster JSON "parsing". +* email_validator - for email validation. + +Used by Starlette: + +* requests - Required if you want to use the `TestClient`. +* aiofiles - Required if you want to use `FileResponse` or `StaticFiles`. +* jinja2 - Required if you want to use the default template configuration. +* python-multipart - Required if you want to support form "parsing", with `request.form()`. +* itsdangerous - Required for `SessionMiddleware` support. +* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). +* graphene - Required for `GraphQLApp` support. +* ujson - Required if you want to use `UJSONResponse`. + +Used by FastAPI / Starlette: + +* uvicorn - for the server that loads and serves your application. +* orjson - Required if you want to use `ORJSONResponse`. + +You can install all of these with `pip install fastapi[all]`. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml new file mode 100644 index 0000000000000..8fb7cf46949f3 --- /dev/null +++ b/docs/es/mkdocs.yml @@ -0,0 +1,58 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/es/ +theme: + name: material + palette: + primary: teal + accent: amber + logo: https://fastapi.tiangolo.com/img/icon-white.svg + favicon: https://fastapi.tiangolo.com/img/favicon.png + language: es +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +google_analytics: +- UA-133183413-1 +- auto +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- markdown_include.include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_div_format '' +extra: + social: + - type: github + link: https://github.com/tiangolo/typer + - type: twitter + link: https://twitter.com/tiangolo + - type: linkedin + link: https://www.linkedin.com/in/tiangolo + - type: rss + link: https://dev.to/tiangolo + - type: medium + link: https://medium.com/@tiangolo + - type: globe + link: https://tiangolo.com +extra_css: +- https://fastapi.tiangolo.com/css/termynal.css +- https://fastapi.tiangolo.com/css/custom.css +extra_javascript: +- https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js +- https://fastapi.tiangolo.com/js/termynal.js +- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/missing-translation.md b/docs/missing-translation.md new file mode 100644 index 0000000000000..32b6016f997cd --- /dev/null +++ b/docs/missing-translation.md @@ -0,0 +1,4 @@ +!!! warning + The current page still doesn't have a translation for this language. + + But you can help translating it: [Contributing](https://fastapi.tiangolo.com/contributing/){.internal-link target=_blank}. diff --git a/docs/src/additional_responses/tutorial001.py b/docs_src/additional_responses/tutorial001.py similarity index 100% rename from docs/src/additional_responses/tutorial001.py rename to docs_src/additional_responses/tutorial001.py diff --git a/docs/src/additional_responses/tutorial002.py b/docs_src/additional_responses/tutorial002.py similarity index 100% rename from docs/src/additional_responses/tutorial002.py rename to docs_src/additional_responses/tutorial002.py diff --git a/docs/src/additional_responses/tutorial003.py b/docs_src/additional_responses/tutorial003.py similarity index 100% rename from docs/src/additional_responses/tutorial003.py rename to docs_src/additional_responses/tutorial003.py diff --git a/docs/src/additional_responses/tutorial004.py b/docs_src/additional_responses/tutorial004.py similarity index 100% rename from docs/src/additional_responses/tutorial004.py rename to docs_src/additional_responses/tutorial004.py diff --git a/docs/src/additional_status_codes/tutorial001.py b/docs_src/additional_status_codes/tutorial001.py similarity index 100% rename from docs/src/additional_status_codes/tutorial001.py rename to docs_src/additional_status_codes/tutorial001.py diff --git a/docs/src/advanced_middleware/tutorial001.py b/docs_src/advanced_middleware/tutorial001.py similarity index 100% rename from docs/src/advanced_middleware/tutorial001.py rename to docs_src/advanced_middleware/tutorial001.py diff --git a/docs/src/advanced_middleware/tutorial002.py b/docs_src/advanced_middleware/tutorial002.py similarity index 100% rename from docs/src/advanced_middleware/tutorial002.py rename to docs_src/advanced_middleware/tutorial002.py diff --git a/docs/src/advanced_middleware/tutorial003.py b/docs_src/advanced_middleware/tutorial003.py similarity index 100% rename from docs/src/advanced_middleware/tutorial003.py rename to docs_src/advanced_middleware/tutorial003.py diff --git a/docs/src/app_testing/__init__.py b/docs_src/app_testing/__init__.py similarity index 100% rename from docs/src/app_testing/__init__.py rename to docs_src/app_testing/__init__.py diff --git a/docs/src/app_testing/main.py b/docs_src/app_testing/main.py similarity index 100% rename from docs/src/app_testing/main.py rename to docs_src/app_testing/main.py diff --git a/docs/src/app_testing/main_b.py b/docs_src/app_testing/main_b.py similarity index 100% rename from docs/src/app_testing/main_b.py rename to docs_src/app_testing/main_b.py diff --git a/docs/src/app_testing/test_main.py b/docs_src/app_testing/test_main.py similarity index 100% rename from docs/src/app_testing/test_main.py rename to docs_src/app_testing/test_main.py diff --git a/docs/src/app_testing/test_main_b.py b/docs_src/app_testing/test_main_b.py similarity index 100% rename from docs/src/app_testing/test_main_b.py rename to docs_src/app_testing/test_main_b.py diff --git a/docs/src/app_testing/tutorial001.py b/docs_src/app_testing/tutorial001.py similarity index 100% rename from docs/src/app_testing/tutorial001.py rename to docs_src/app_testing/tutorial001.py diff --git a/docs/src/app_testing/tutorial002.py b/docs_src/app_testing/tutorial002.py similarity index 100% rename from docs/src/app_testing/tutorial002.py rename to docs_src/app_testing/tutorial002.py diff --git a/docs/src/app_testing/tutorial003.py b/docs_src/app_testing/tutorial003.py similarity index 100% rename from docs/src/app_testing/tutorial003.py rename to docs_src/app_testing/tutorial003.py diff --git a/docs/src/application_configuration/tutorial001.py b/docs_src/application_configuration/tutorial001.py similarity index 100% rename from docs/src/application_configuration/tutorial001.py rename to docs_src/application_configuration/tutorial001.py diff --git a/docs/src/application_configuration/tutorial002.py b/docs_src/application_configuration/tutorial002.py similarity index 100% rename from docs/src/application_configuration/tutorial002.py rename to docs_src/application_configuration/tutorial002.py diff --git a/docs/src/application_configuration/tutorial003.py b/docs_src/application_configuration/tutorial003.py similarity index 100% rename from docs/src/application_configuration/tutorial003.py rename to docs_src/application_configuration/tutorial003.py diff --git a/docs/src/async_sql_databases/tutorial001.py b/docs_src/async_sql_databases/tutorial001.py similarity index 100% rename from docs/src/async_sql_databases/tutorial001.py rename to docs_src/async_sql_databases/tutorial001.py diff --git a/docs/src/background_tasks/tutorial001.py b/docs_src/background_tasks/tutorial001.py similarity index 100% rename from docs/src/background_tasks/tutorial001.py rename to docs_src/background_tasks/tutorial001.py diff --git a/docs/src/background_tasks/tutorial002.py b/docs_src/background_tasks/tutorial002.py similarity index 100% rename from docs/src/background_tasks/tutorial002.py rename to docs_src/background_tasks/tutorial002.py diff --git a/docs/src/bigger_applications/__init__.py b/docs_src/bigger_applications/__init__.py similarity index 100% rename from docs/src/bigger_applications/__init__.py rename to docs_src/bigger_applications/__init__.py diff --git a/docs/src/bigger_applications/app/__init__.py b/docs_src/bigger_applications/app/__init__.py similarity index 100% rename from docs/src/bigger_applications/app/__init__.py rename to docs_src/bigger_applications/app/__init__.py diff --git a/docs/src/bigger_applications/app/main.py b/docs_src/bigger_applications/app/main.py similarity index 100% rename from docs/src/bigger_applications/app/main.py rename to docs_src/bigger_applications/app/main.py diff --git a/docs/src/bigger_applications/app/routers/__init__.py b/docs_src/bigger_applications/app/routers/__init__.py similarity index 100% rename from docs/src/bigger_applications/app/routers/__init__.py rename to docs_src/bigger_applications/app/routers/__init__.py diff --git a/docs/src/bigger_applications/app/routers/items.py b/docs_src/bigger_applications/app/routers/items.py similarity index 100% rename from docs/src/bigger_applications/app/routers/items.py rename to docs_src/bigger_applications/app/routers/items.py diff --git a/docs/src/bigger_applications/app/routers/users.py b/docs_src/bigger_applications/app/routers/users.py similarity index 100% rename from docs/src/bigger_applications/app/routers/users.py rename to docs_src/bigger_applications/app/routers/users.py diff --git a/docs/src/body/tutorial001.py b/docs_src/body/tutorial001.py similarity index 100% rename from docs/src/body/tutorial001.py rename to docs_src/body/tutorial001.py diff --git a/docs/src/body/tutorial002.py b/docs_src/body/tutorial002.py similarity index 100% rename from docs/src/body/tutorial002.py rename to docs_src/body/tutorial002.py diff --git a/docs/src/body/tutorial003.py b/docs_src/body/tutorial003.py similarity index 100% rename from docs/src/body/tutorial003.py rename to docs_src/body/tutorial003.py diff --git a/docs/src/body/tutorial004.py b/docs_src/body/tutorial004.py similarity index 100% rename from docs/src/body/tutorial004.py rename to docs_src/body/tutorial004.py diff --git a/docs/src/body_fields/tutorial001.py b/docs_src/body_fields/tutorial001.py similarity index 100% rename from docs/src/body_fields/tutorial001.py rename to docs_src/body_fields/tutorial001.py diff --git a/docs/src/body_fields/tutorial002.py b/docs_src/body_fields/tutorial002.py similarity index 100% rename from docs/src/body_fields/tutorial002.py rename to docs_src/body_fields/tutorial002.py diff --git a/docs/src/body_multiple_params/tutorial001.py b/docs_src/body_multiple_params/tutorial001.py similarity index 100% rename from docs/src/body_multiple_params/tutorial001.py rename to docs_src/body_multiple_params/tutorial001.py diff --git a/docs/src/body_multiple_params/tutorial002.py b/docs_src/body_multiple_params/tutorial002.py similarity index 100% rename from docs/src/body_multiple_params/tutorial002.py rename to docs_src/body_multiple_params/tutorial002.py diff --git a/docs/src/body_multiple_params/tutorial003.py b/docs_src/body_multiple_params/tutorial003.py similarity index 100% rename from docs/src/body_multiple_params/tutorial003.py rename to docs_src/body_multiple_params/tutorial003.py diff --git a/docs/src/body_multiple_params/tutorial004.py b/docs_src/body_multiple_params/tutorial004.py similarity index 100% rename from docs/src/body_multiple_params/tutorial004.py rename to docs_src/body_multiple_params/tutorial004.py diff --git a/docs/src/body_multiple_params/tutorial005.py b/docs_src/body_multiple_params/tutorial005.py similarity index 100% rename from docs/src/body_multiple_params/tutorial005.py rename to docs_src/body_multiple_params/tutorial005.py diff --git a/docs/src/body_nested_models/tutorial001.py b/docs_src/body_nested_models/tutorial001.py similarity index 100% rename from docs/src/body_nested_models/tutorial001.py rename to docs_src/body_nested_models/tutorial001.py diff --git a/docs/src/body_nested_models/tutorial002.py b/docs_src/body_nested_models/tutorial002.py similarity index 100% rename from docs/src/body_nested_models/tutorial002.py rename to docs_src/body_nested_models/tutorial002.py diff --git a/docs/src/body_nested_models/tutorial003.py b/docs_src/body_nested_models/tutorial003.py similarity index 100% rename from docs/src/body_nested_models/tutorial003.py rename to docs_src/body_nested_models/tutorial003.py diff --git a/docs/src/body_nested_models/tutorial004.py b/docs_src/body_nested_models/tutorial004.py similarity index 100% rename from docs/src/body_nested_models/tutorial004.py rename to docs_src/body_nested_models/tutorial004.py diff --git a/docs/src/body_nested_models/tutorial005.py b/docs_src/body_nested_models/tutorial005.py similarity index 100% rename from docs/src/body_nested_models/tutorial005.py rename to docs_src/body_nested_models/tutorial005.py diff --git a/docs/src/body_nested_models/tutorial006.py b/docs_src/body_nested_models/tutorial006.py similarity index 100% rename from docs/src/body_nested_models/tutorial006.py rename to docs_src/body_nested_models/tutorial006.py diff --git a/docs/src/body_nested_models/tutorial007.py b/docs_src/body_nested_models/tutorial007.py similarity index 100% rename from docs/src/body_nested_models/tutorial007.py rename to docs_src/body_nested_models/tutorial007.py diff --git a/docs/src/body_nested_models/tutorial008.py b/docs_src/body_nested_models/tutorial008.py similarity index 100% rename from docs/src/body_nested_models/tutorial008.py rename to docs_src/body_nested_models/tutorial008.py diff --git a/docs/src/body_nested_models/tutorial009.py b/docs_src/body_nested_models/tutorial009.py similarity index 100% rename from docs/src/body_nested_models/tutorial009.py rename to docs_src/body_nested_models/tutorial009.py diff --git a/docs/src/body_updates/tutorial001.py b/docs_src/body_updates/tutorial001.py similarity index 100% rename from docs/src/body_updates/tutorial001.py rename to docs_src/body_updates/tutorial001.py diff --git a/docs/src/body_updates/tutorial002.py b/docs_src/body_updates/tutorial002.py similarity index 100% rename from docs/src/body_updates/tutorial002.py rename to docs_src/body_updates/tutorial002.py diff --git a/docs/src/cookie_params/tutorial001.py b/docs_src/cookie_params/tutorial001.py similarity index 100% rename from docs/src/cookie_params/tutorial001.py rename to docs_src/cookie_params/tutorial001.py diff --git a/docs/src/cors/tutorial001.py b/docs_src/cors/tutorial001.py similarity index 100% rename from docs/src/cors/tutorial001.py rename to docs_src/cors/tutorial001.py diff --git a/docs/src/custom_request_and_route/tutorial001.py b/docs_src/custom_request_and_route/tutorial001.py similarity index 100% rename from docs/src/custom_request_and_route/tutorial001.py rename to docs_src/custom_request_and_route/tutorial001.py diff --git a/docs/src/custom_request_and_route/tutorial002.py b/docs_src/custom_request_and_route/tutorial002.py similarity index 100% rename from docs/src/custom_request_and_route/tutorial002.py rename to docs_src/custom_request_and_route/tutorial002.py diff --git a/docs/src/custom_request_and_route/tutorial003.py b/docs_src/custom_request_and_route/tutorial003.py similarity index 100% rename from docs/src/custom_request_and_route/tutorial003.py rename to docs_src/custom_request_and_route/tutorial003.py diff --git a/docs/src/custom_response/tutorial001.py b/docs_src/custom_response/tutorial001.py similarity index 100% rename from docs/src/custom_response/tutorial001.py rename to docs_src/custom_response/tutorial001.py diff --git a/docs/src/custom_response/tutorial001b.py b/docs_src/custom_response/tutorial001b.py similarity index 100% rename from docs/src/custom_response/tutorial001b.py rename to docs_src/custom_response/tutorial001b.py diff --git a/docs/src/custom_response/tutorial002.py b/docs_src/custom_response/tutorial002.py similarity index 100% rename from docs/src/custom_response/tutorial002.py rename to docs_src/custom_response/tutorial002.py diff --git a/docs/src/custom_response/tutorial003.py b/docs_src/custom_response/tutorial003.py similarity index 100% rename from docs/src/custom_response/tutorial003.py rename to docs_src/custom_response/tutorial003.py diff --git a/docs/src/custom_response/tutorial004.py b/docs_src/custom_response/tutorial004.py similarity index 100% rename from docs/src/custom_response/tutorial004.py rename to docs_src/custom_response/tutorial004.py diff --git a/docs/src/custom_response/tutorial005.py b/docs_src/custom_response/tutorial005.py similarity index 100% rename from docs/src/custom_response/tutorial005.py rename to docs_src/custom_response/tutorial005.py diff --git a/docs/src/custom_response/tutorial006.py b/docs_src/custom_response/tutorial006.py similarity index 100% rename from docs/src/custom_response/tutorial006.py rename to docs_src/custom_response/tutorial006.py diff --git a/docs/src/custom_response/tutorial007.py b/docs_src/custom_response/tutorial007.py similarity index 100% rename from docs/src/custom_response/tutorial007.py rename to docs_src/custom_response/tutorial007.py diff --git a/docs/src/custom_response/tutorial008.py b/docs_src/custom_response/tutorial008.py similarity index 100% rename from docs/src/custom_response/tutorial008.py rename to docs_src/custom_response/tutorial008.py diff --git a/docs/src/custom_response/tutorial009.py b/docs_src/custom_response/tutorial009.py similarity index 100% rename from docs/src/custom_response/tutorial009.py rename to docs_src/custom_response/tutorial009.py diff --git a/docs/src/debugging/tutorial001.py b/docs_src/debugging/tutorial001.py similarity index 100% rename from docs/src/debugging/tutorial001.py rename to docs_src/debugging/tutorial001.py diff --git a/docs/src/dependencies/tutorial001.py b/docs_src/dependencies/tutorial001.py similarity index 100% rename from docs/src/dependencies/tutorial001.py rename to docs_src/dependencies/tutorial001.py diff --git a/docs/src/dependencies/tutorial002.py b/docs_src/dependencies/tutorial002.py similarity index 100% rename from docs/src/dependencies/tutorial002.py rename to docs_src/dependencies/tutorial002.py diff --git a/docs/src/dependencies/tutorial003.py b/docs_src/dependencies/tutorial003.py similarity index 100% rename from docs/src/dependencies/tutorial003.py rename to docs_src/dependencies/tutorial003.py diff --git a/docs/src/dependencies/tutorial004.py b/docs_src/dependencies/tutorial004.py similarity index 100% rename from docs/src/dependencies/tutorial004.py rename to docs_src/dependencies/tutorial004.py diff --git a/docs/src/dependencies/tutorial005.py b/docs_src/dependencies/tutorial005.py similarity index 100% rename from docs/src/dependencies/tutorial005.py rename to docs_src/dependencies/tutorial005.py diff --git a/docs/src/dependencies/tutorial006.py b/docs_src/dependencies/tutorial006.py similarity index 100% rename from docs/src/dependencies/tutorial006.py rename to docs_src/dependencies/tutorial006.py diff --git a/docs/src/dependencies/tutorial007.py b/docs_src/dependencies/tutorial007.py similarity index 100% rename from docs/src/dependencies/tutorial007.py rename to docs_src/dependencies/tutorial007.py diff --git a/docs/src/dependencies/tutorial008.py b/docs_src/dependencies/tutorial008.py similarity index 100% rename from docs/src/dependencies/tutorial008.py rename to docs_src/dependencies/tutorial008.py diff --git a/docs/src/dependencies/tutorial009.py b/docs_src/dependencies/tutorial009.py similarity index 100% rename from docs/src/dependencies/tutorial009.py rename to docs_src/dependencies/tutorial009.py diff --git a/docs/src/dependencies/tutorial010.py b/docs_src/dependencies/tutorial010.py similarity index 100% rename from docs/src/dependencies/tutorial010.py rename to docs_src/dependencies/tutorial010.py diff --git a/docs/src/dependencies/tutorial011.py b/docs_src/dependencies/tutorial011.py similarity index 100% rename from docs/src/dependencies/tutorial011.py rename to docs_src/dependencies/tutorial011.py diff --git a/docs/src/dependency_testing/tutorial001.py b/docs_src/dependency_testing/tutorial001.py similarity index 100% rename from docs/src/dependency_testing/tutorial001.py rename to docs_src/dependency_testing/tutorial001.py diff --git a/docs/src/encoder/tutorial001.py b/docs_src/encoder/tutorial001.py similarity index 100% rename from docs/src/encoder/tutorial001.py rename to docs_src/encoder/tutorial001.py diff --git a/docs/src/events/tutorial001.py b/docs_src/events/tutorial001.py similarity index 100% rename from docs/src/events/tutorial001.py rename to docs_src/events/tutorial001.py diff --git a/docs/src/events/tutorial002.py b/docs_src/events/tutorial002.py similarity index 100% rename from docs/src/events/tutorial002.py rename to docs_src/events/tutorial002.py diff --git a/docs/src/extending_openapi/tutorial001.py b/docs_src/extending_openapi/tutorial001.py similarity index 100% rename from docs/src/extending_openapi/tutorial001.py rename to docs_src/extending_openapi/tutorial001.py diff --git a/docs/src/extending_openapi/tutorial002.py b/docs_src/extending_openapi/tutorial002.py similarity index 100% rename from docs/src/extending_openapi/tutorial002.py rename to docs_src/extending_openapi/tutorial002.py diff --git a/docs/src/extra_data_types/tutorial001.py b/docs_src/extra_data_types/tutorial001.py similarity index 100% rename from docs/src/extra_data_types/tutorial001.py rename to docs_src/extra_data_types/tutorial001.py diff --git a/docs/src/extra_models/tutorial001.py b/docs_src/extra_models/tutorial001.py similarity index 100% rename from docs/src/extra_models/tutorial001.py rename to docs_src/extra_models/tutorial001.py diff --git a/docs/src/extra_models/tutorial002.py b/docs_src/extra_models/tutorial002.py similarity index 100% rename from docs/src/extra_models/tutorial002.py rename to docs_src/extra_models/tutorial002.py diff --git a/docs/src/extra_models/tutorial003.py b/docs_src/extra_models/tutorial003.py similarity index 100% rename from docs/src/extra_models/tutorial003.py rename to docs_src/extra_models/tutorial003.py diff --git a/docs/src/extra_models/tutorial004.py b/docs_src/extra_models/tutorial004.py similarity index 100% rename from docs/src/extra_models/tutorial004.py rename to docs_src/extra_models/tutorial004.py diff --git a/docs/src/extra_models/tutorial005.py b/docs_src/extra_models/tutorial005.py similarity index 100% rename from docs/src/extra_models/tutorial005.py rename to docs_src/extra_models/tutorial005.py diff --git a/docs/src/first_steps/tutorial001.py b/docs_src/first_steps/tutorial001.py similarity index 100% rename from docs/src/first_steps/tutorial001.py rename to docs_src/first_steps/tutorial001.py diff --git a/docs/src/first_steps/tutorial002.py b/docs_src/first_steps/tutorial002.py similarity index 100% rename from docs/src/first_steps/tutorial002.py rename to docs_src/first_steps/tutorial002.py diff --git a/docs/src/first_steps/tutorial003.py b/docs_src/first_steps/tutorial003.py similarity index 100% rename from docs/src/first_steps/tutorial003.py rename to docs_src/first_steps/tutorial003.py diff --git a/docs/src/graphql/tutorial001.py b/docs_src/graphql/tutorial001.py similarity index 100% rename from docs/src/graphql/tutorial001.py rename to docs_src/graphql/tutorial001.py diff --git a/docs/src/handling_errors/tutorial001.py b/docs_src/handling_errors/tutorial001.py similarity index 100% rename from docs/src/handling_errors/tutorial001.py rename to docs_src/handling_errors/tutorial001.py diff --git a/docs/src/handling_errors/tutorial002.py b/docs_src/handling_errors/tutorial002.py similarity index 100% rename from docs/src/handling_errors/tutorial002.py rename to docs_src/handling_errors/tutorial002.py diff --git a/docs/src/handling_errors/tutorial003.py b/docs_src/handling_errors/tutorial003.py similarity index 100% rename from docs/src/handling_errors/tutorial003.py rename to docs_src/handling_errors/tutorial003.py diff --git a/docs/src/handling_errors/tutorial004.py b/docs_src/handling_errors/tutorial004.py similarity index 100% rename from docs/src/handling_errors/tutorial004.py rename to docs_src/handling_errors/tutorial004.py diff --git a/docs/src/handling_errors/tutorial005.py b/docs_src/handling_errors/tutorial005.py similarity index 100% rename from docs/src/handling_errors/tutorial005.py rename to docs_src/handling_errors/tutorial005.py diff --git a/docs/src/handling_errors/tutorial006.py b/docs_src/handling_errors/tutorial006.py similarity index 100% rename from docs/src/handling_errors/tutorial006.py rename to docs_src/handling_errors/tutorial006.py diff --git a/docs/src/header_params/tutorial001.py b/docs_src/header_params/tutorial001.py similarity index 100% rename from docs/src/header_params/tutorial001.py rename to docs_src/header_params/tutorial001.py diff --git a/docs/src/header_params/tutorial002.py b/docs_src/header_params/tutorial002.py similarity index 100% rename from docs/src/header_params/tutorial002.py rename to docs_src/header_params/tutorial002.py diff --git a/docs/src/header_params/tutorial003.py b/docs_src/header_params/tutorial003.py similarity index 100% rename from docs/src/header_params/tutorial003.py rename to docs_src/header_params/tutorial003.py diff --git a/docs/src/middleware/tutorial001.py b/docs_src/middleware/tutorial001.py similarity index 100% rename from docs/src/middleware/tutorial001.py rename to docs_src/middleware/tutorial001.py diff --git a/docs/src/nosql_databases/tutorial001.py b/docs_src/nosql_databases/tutorial001.py similarity index 100% rename from docs/src/nosql_databases/tutorial001.py rename to docs_src/nosql_databases/tutorial001.py diff --git a/docs/src/openapi_callbacks/tutorial001.py b/docs_src/openapi_callbacks/tutorial001.py similarity index 100% rename from docs/src/openapi_callbacks/tutorial001.py rename to docs_src/openapi_callbacks/tutorial001.py diff --git a/docs/src/path_operation_advanced_configuration/tutorial001.py b/docs_src/path_operation_advanced_configuration/tutorial001.py similarity index 100% rename from docs/src/path_operation_advanced_configuration/tutorial001.py rename to docs_src/path_operation_advanced_configuration/tutorial001.py diff --git a/docs/src/path_operation_advanced_configuration/tutorial002.py b/docs_src/path_operation_advanced_configuration/tutorial002.py similarity index 100% rename from docs/src/path_operation_advanced_configuration/tutorial002.py rename to docs_src/path_operation_advanced_configuration/tutorial002.py diff --git a/docs/src/path_operation_advanced_configuration/tutorial003.py b/docs_src/path_operation_advanced_configuration/tutorial003.py similarity index 100% rename from docs/src/path_operation_advanced_configuration/tutorial003.py rename to docs_src/path_operation_advanced_configuration/tutorial003.py diff --git a/docs/src/path_operation_advanced_configuration/tutorial004.py b/docs_src/path_operation_advanced_configuration/tutorial004.py similarity index 100% rename from docs/src/path_operation_advanced_configuration/tutorial004.py rename to docs_src/path_operation_advanced_configuration/tutorial004.py diff --git a/docs/src/path_operation_configuration/tutorial001.py b/docs_src/path_operation_configuration/tutorial001.py similarity index 100% rename from docs/src/path_operation_configuration/tutorial001.py rename to docs_src/path_operation_configuration/tutorial001.py diff --git a/docs/src/path_operation_configuration/tutorial002.py b/docs_src/path_operation_configuration/tutorial002.py similarity index 100% rename from docs/src/path_operation_configuration/tutorial002.py rename to docs_src/path_operation_configuration/tutorial002.py diff --git a/docs/src/path_operation_configuration/tutorial003.py b/docs_src/path_operation_configuration/tutorial003.py similarity index 100% rename from docs/src/path_operation_configuration/tutorial003.py rename to docs_src/path_operation_configuration/tutorial003.py diff --git a/docs/src/path_operation_configuration/tutorial004.py b/docs_src/path_operation_configuration/tutorial004.py similarity index 100% rename from docs/src/path_operation_configuration/tutorial004.py rename to docs_src/path_operation_configuration/tutorial004.py diff --git a/docs/src/path_operation_configuration/tutorial005.py b/docs_src/path_operation_configuration/tutorial005.py similarity index 100% rename from docs/src/path_operation_configuration/tutorial005.py rename to docs_src/path_operation_configuration/tutorial005.py diff --git a/docs/src/path_operation_configuration/tutorial006.py b/docs_src/path_operation_configuration/tutorial006.py similarity index 100% rename from docs/src/path_operation_configuration/tutorial006.py rename to docs_src/path_operation_configuration/tutorial006.py diff --git a/docs/src/path_params/tutorial001.py b/docs_src/path_params/tutorial001.py similarity index 100% rename from docs/src/path_params/tutorial001.py rename to docs_src/path_params/tutorial001.py diff --git a/docs/src/path_params/tutorial002.py b/docs_src/path_params/tutorial002.py similarity index 100% rename from docs/src/path_params/tutorial002.py rename to docs_src/path_params/tutorial002.py diff --git a/docs/src/path_params/tutorial003.py b/docs_src/path_params/tutorial003.py similarity index 100% rename from docs/src/path_params/tutorial003.py rename to docs_src/path_params/tutorial003.py diff --git a/docs/src/path_params/tutorial004.py b/docs_src/path_params/tutorial004.py similarity index 100% rename from docs/src/path_params/tutorial004.py rename to docs_src/path_params/tutorial004.py diff --git a/docs/src/path_params/tutorial005.py b/docs_src/path_params/tutorial005.py similarity index 100% rename from docs/src/path_params/tutorial005.py rename to docs_src/path_params/tutorial005.py diff --git a/docs/src/path_params_numeric_validations/tutorial001.py b/docs_src/path_params_numeric_validations/tutorial001.py similarity index 100% rename from docs/src/path_params_numeric_validations/tutorial001.py rename to docs_src/path_params_numeric_validations/tutorial001.py diff --git a/docs/src/path_params_numeric_validations/tutorial002.py b/docs_src/path_params_numeric_validations/tutorial002.py similarity index 100% rename from docs/src/path_params_numeric_validations/tutorial002.py rename to docs_src/path_params_numeric_validations/tutorial002.py diff --git a/docs/src/path_params_numeric_validations/tutorial003.py b/docs_src/path_params_numeric_validations/tutorial003.py similarity index 100% rename from docs/src/path_params_numeric_validations/tutorial003.py rename to docs_src/path_params_numeric_validations/tutorial003.py diff --git a/docs/src/path_params_numeric_validations/tutorial004.py b/docs_src/path_params_numeric_validations/tutorial004.py similarity index 100% rename from docs/src/path_params_numeric_validations/tutorial004.py rename to docs_src/path_params_numeric_validations/tutorial004.py diff --git a/docs/src/path_params_numeric_validations/tutorial005.py b/docs_src/path_params_numeric_validations/tutorial005.py similarity index 100% rename from docs/src/path_params_numeric_validations/tutorial005.py rename to docs_src/path_params_numeric_validations/tutorial005.py diff --git a/docs/src/path_params_numeric_validations/tutorial006.py b/docs_src/path_params_numeric_validations/tutorial006.py similarity index 100% rename from docs/src/path_params_numeric_validations/tutorial006.py rename to docs_src/path_params_numeric_validations/tutorial006.py diff --git a/docs/src/python_types/tutorial001.py b/docs_src/python_types/tutorial001.py similarity index 100% rename from docs/src/python_types/tutorial001.py rename to docs_src/python_types/tutorial001.py diff --git a/docs/src/python_types/tutorial002.py b/docs_src/python_types/tutorial002.py similarity index 100% rename from docs/src/python_types/tutorial002.py rename to docs_src/python_types/tutorial002.py diff --git a/docs/src/python_types/tutorial003.py b/docs_src/python_types/tutorial003.py similarity index 100% rename from docs/src/python_types/tutorial003.py rename to docs_src/python_types/tutorial003.py diff --git a/docs/src/python_types/tutorial004.py b/docs_src/python_types/tutorial004.py similarity index 100% rename from docs/src/python_types/tutorial004.py rename to docs_src/python_types/tutorial004.py diff --git a/docs/src/python_types/tutorial005.py b/docs_src/python_types/tutorial005.py similarity index 100% rename from docs/src/python_types/tutorial005.py rename to docs_src/python_types/tutorial005.py diff --git a/docs/src/python_types/tutorial006.py b/docs_src/python_types/tutorial006.py similarity index 100% rename from docs/src/python_types/tutorial006.py rename to docs_src/python_types/tutorial006.py diff --git a/docs/src/python_types/tutorial007.py b/docs_src/python_types/tutorial007.py similarity index 100% rename from docs/src/python_types/tutorial007.py rename to docs_src/python_types/tutorial007.py diff --git a/docs/src/python_types/tutorial008.py b/docs_src/python_types/tutorial008.py similarity index 100% rename from docs/src/python_types/tutorial008.py rename to docs_src/python_types/tutorial008.py diff --git a/docs/src/python_types/tutorial009.py b/docs_src/python_types/tutorial009.py similarity index 100% rename from docs/src/python_types/tutorial009.py rename to docs_src/python_types/tutorial009.py diff --git a/docs/src/python_types/tutorial010.py b/docs_src/python_types/tutorial010.py similarity index 100% rename from docs/src/python_types/tutorial010.py rename to docs_src/python_types/tutorial010.py diff --git a/docs/src/query_params/tutorial001.py b/docs_src/query_params/tutorial001.py similarity index 100% rename from docs/src/query_params/tutorial001.py rename to docs_src/query_params/tutorial001.py diff --git a/docs/src/query_params/tutorial002.py b/docs_src/query_params/tutorial002.py similarity index 100% rename from docs/src/query_params/tutorial002.py rename to docs_src/query_params/tutorial002.py diff --git a/docs/src/query_params/tutorial003.py b/docs_src/query_params/tutorial003.py similarity index 100% rename from docs/src/query_params/tutorial003.py rename to docs_src/query_params/tutorial003.py diff --git a/docs/src/query_params/tutorial004.py b/docs_src/query_params/tutorial004.py similarity index 100% rename from docs/src/query_params/tutorial004.py rename to docs_src/query_params/tutorial004.py diff --git a/docs/src/query_params/tutorial005.py b/docs_src/query_params/tutorial005.py similarity index 100% rename from docs/src/query_params/tutorial005.py rename to docs_src/query_params/tutorial005.py diff --git a/docs/src/query_params/tutorial006.py b/docs_src/query_params/tutorial006.py similarity index 100% rename from docs/src/query_params/tutorial006.py rename to docs_src/query_params/tutorial006.py diff --git a/docs/src/query_params/tutorial007.py b/docs_src/query_params/tutorial007.py similarity index 100% rename from docs/src/query_params/tutorial007.py rename to docs_src/query_params/tutorial007.py diff --git a/docs/src/query_params_str_validations/tutorial001.py b/docs_src/query_params_str_validations/tutorial001.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial001.py rename to docs_src/query_params_str_validations/tutorial001.py diff --git a/docs/src/query_params_str_validations/tutorial002.py b/docs_src/query_params_str_validations/tutorial002.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial002.py rename to docs_src/query_params_str_validations/tutorial002.py diff --git a/docs/src/query_params_str_validations/tutorial003.py b/docs_src/query_params_str_validations/tutorial003.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial003.py rename to docs_src/query_params_str_validations/tutorial003.py diff --git a/docs/src/query_params_str_validations/tutorial004.py b/docs_src/query_params_str_validations/tutorial004.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial004.py rename to docs_src/query_params_str_validations/tutorial004.py diff --git a/docs/src/query_params_str_validations/tutorial005.py b/docs_src/query_params_str_validations/tutorial005.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial005.py rename to docs_src/query_params_str_validations/tutorial005.py diff --git a/docs/src/query_params_str_validations/tutorial006.py b/docs_src/query_params_str_validations/tutorial006.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial006.py rename to docs_src/query_params_str_validations/tutorial006.py diff --git a/docs/src/query_params_str_validations/tutorial007.py b/docs_src/query_params_str_validations/tutorial007.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial007.py rename to docs_src/query_params_str_validations/tutorial007.py diff --git a/docs/src/query_params_str_validations/tutorial008.py b/docs_src/query_params_str_validations/tutorial008.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial008.py rename to docs_src/query_params_str_validations/tutorial008.py diff --git a/docs/src/query_params_str_validations/tutorial009.py b/docs_src/query_params_str_validations/tutorial009.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial009.py rename to docs_src/query_params_str_validations/tutorial009.py diff --git a/docs/src/query_params_str_validations/tutorial010.py b/docs_src/query_params_str_validations/tutorial010.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial010.py rename to docs_src/query_params_str_validations/tutorial010.py diff --git a/docs/src/query_params_str_validations/tutorial011.py b/docs_src/query_params_str_validations/tutorial011.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial011.py rename to docs_src/query_params_str_validations/tutorial011.py diff --git a/docs/src/query_params_str_validations/tutorial012.py b/docs_src/query_params_str_validations/tutorial012.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial012.py rename to docs_src/query_params_str_validations/tutorial012.py diff --git a/docs/src/query_params_str_validations/tutorial013.py b/docs_src/query_params_str_validations/tutorial013.py similarity index 100% rename from docs/src/query_params_str_validations/tutorial013.py rename to docs_src/query_params_str_validations/tutorial013.py diff --git a/docs/src/request_files/tutorial001.py b/docs_src/request_files/tutorial001.py similarity index 100% rename from docs/src/request_files/tutorial001.py rename to docs_src/request_files/tutorial001.py diff --git a/docs/src/request_files/tutorial002.py b/docs_src/request_files/tutorial002.py similarity index 100% rename from docs/src/request_files/tutorial002.py rename to docs_src/request_files/tutorial002.py diff --git a/docs/src/request_forms/tutorial001.py b/docs_src/request_forms/tutorial001.py similarity index 100% rename from docs/src/request_forms/tutorial001.py rename to docs_src/request_forms/tutorial001.py diff --git a/docs/src/request_forms_and_files/tutorial001.py b/docs_src/request_forms_and_files/tutorial001.py similarity index 100% rename from docs/src/request_forms_and_files/tutorial001.py rename to docs_src/request_forms_and_files/tutorial001.py diff --git a/docs/src/response_change_status_code/tutorial001.py b/docs_src/response_change_status_code/tutorial001.py similarity index 100% rename from docs/src/response_change_status_code/tutorial001.py rename to docs_src/response_change_status_code/tutorial001.py diff --git a/docs/src/response_cookies/tutorial001.py b/docs_src/response_cookies/tutorial001.py similarity index 100% rename from docs/src/response_cookies/tutorial001.py rename to docs_src/response_cookies/tutorial001.py diff --git a/docs/src/response_cookies/tutorial002.py b/docs_src/response_cookies/tutorial002.py similarity index 100% rename from docs/src/response_cookies/tutorial002.py rename to docs_src/response_cookies/tutorial002.py diff --git a/docs/src/response_directly/tutorial001.py b/docs_src/response_directly/tutorial001.py similarity index 100% rename from docs/src/response_directly/tutorial001.py rename to docs_src/response_directly/tutorial001.py diff --git a/docs/src/response_directly/tutorial002.py b/docs_src/response_directly/tutorial002.py similarity index 100% rename from docs/src/response_directly/tutorial002.py rename to docs_src/response_directly/tutorial002.py diff --git a/docs/src/response_headers/tutorial001.py b/docs_src/response_headers/tutorial001.py similarity index 100% rename from docs/src/response_headers/tutorial001.py rename to docs_src/response_headers/tutorial001.py diff --git a/docs/src/response_headers/tutorial002.py b/docs_src/response_headers/tutorial002.py similarity index 100% rename from docs/src/response_headers/tutorial002.py rename to docs_src/response_headers/tutorial002.py diff --git a/docs/src/response_model/tutorial001.py b/docs_src/response_model/tutorial001.py similarity index 100% rename from docs/src/response_model/tutorial001.py rename to docs_src/response_model/tutorial001.py diff --git a/docs/src/response_model/tutorial002.py b/docs_src/response_model/tutorial002.py similarity index 100% rename from docs/src/response_model/tutorial002.py rename to docs_src/response_model/tutorial002.py diff --git a/docs/src/response_model/tutorial003.py b/docs_src/response_model/tutorial003.py similarity index 100% rename from docs/src/response_model/tutorial003.py rename to docs_src/response_model/tutorial003.py diff --git a/docs/src/response_model/tutorial004.py b/docs_src/response_model/tutorial004.py similarity index 100% rename from docs/src/response_model/tutorial004.py rename to docs_src/response_model/tutorial004.py diff --git a/docs/src/response_model/tutorial005.py b/docs_src/response_model/tutorial005.py similarity index 100% rename from docs/src/response_model/tutorial005.py rename to docs_src/response_model/tutorial005.py diff --git a/docs/src/response_model/tutorial006.py b/docs_src/response_model/tutorial006.py similarity index 100% rename from docs/src/response_model/tutorial006.py rename to docs_src/response_model/tutorial006.py diff --git a/docs/src/response_status_code/tutorial001.py b/docs_src/response_status_code/tutorial001.py similarity index 100% rename from docs/src/response_status_code/tutorial001.py rename to docs_src/response_status_code/tutorial001.py diff --git a/docs/src/response_status_code/tutorial002.py b/docs_src/response_status_code/tutorial002.py similarity index 100% rename from docs/src/response_status_code/tutorial002.py rename to docs_src/response_status_code/tutorial002.py diff --git a/docs/src/security/tutorial001.py b/docs_src/security/tutorial001.py similarity index 100% rename from docs/src/security/tutorial001.py rename to docs_src/security/tutorial001.py diff --git a/docs/src/security/tutorial002.py b/docs_src/security/tutorial002.py similarity index 100% rename from docs/src/security/tutorial002.py rename to docs_src/security/tutorial002.py diff --git a/docs/src/security/tutorial003.py b/docs_src/security/tutorial003.py similarity index 100% rename from docs/src/security/tutorial003.py rename to docs_src/security/tutorial003.py diff --git a/docs/src/security/tutorial004.py b/docs_src/security/tutorial004.py similarity index 100% rename from docs/src/security/tutorial004.py rename to docs_src/security/tutorial004.py diff --git a/docs/src/security/tutorial005.py b/docs_src/security/tutorial005.py similarity index 100% rename from docs/src/security/tutorial005.py rename to docs_src/security/tutorial005.py diff --git a/docs/src/security/tutorial006.py b/docs_src/security/tutorial006.py similarity index 100% rename from docs/src/security/tutorial006.py rename to docs_src/security/tutorial006.py diff --git a/docs/src/security/tutorial007.py b/docs_src/security/tutorial007.py similarity index 100% rename from docs/src/security/tutorial007.py rename to docs_src/security/tutorial007.py diff --git a/docs/src/sql_databases/__init__.py b/docs_src/sql_databases/__init__.py similarity index 100% rename from docs/src/sql_databases/__init__.py rename to docs_src/sql_databases/__init__.py diff --git a/docs/src/sql_databases/sql_app/__init__.py b/docs_src/sql_databases/sql_app/__init__.py similarity index 100% rename from docs/src/sql_databases/sql_app/__init__.py rename to docs_src/sql_databases/sql_app/__init__.py diff --git a/docs/src/sql_databases/sql_app/alt_main.py b/docs_src/sql_databases/sql_app/alt_main.py similarity index 100% rename from docs/src/sql_databases/sql_app/alt_main.py rename to docs_src/sql_databases/sql_app/alt_main.py diff --git a/docs/src/sql_databases/sql_app/crud.py b/docs_src/sql_databases/sql_app/crud.py similarity index 100% rename from docs/src/sql_databases/sql_app/crud.py rename to docs_src/sql_databases/sql_app/crud.py diff --git a/docs/src/sql_databases/sql_app/database.py b/docs_src/sql_databases/sql_app/database.py similarity index 100% rename from docs/src/sql_databases/sql_app/database.py rename to docs_src/sql_databases/sql_app/database.py diff --git a/docs/src/sql_databases/sql_app/main.py b/docs_src/sql_databases/sql_app/main.py similarity index 100% rename from docs/src/sql_databases/sql_app/main.py rename to docs_src/sql_databases/sql_app/main.py diff --git a/docs/src/sql_databases/sql_app/models.py b/docs_src/sql_databases/sql_app/models.py similarity index 100% rename from docs/src/sql_databases/sql_app/models.py rename to docs_src/sql_databases/sql_app/models.py diff --git a/docs/src/sql_databases/sql_app/schemas.py b/docs_src/sql_databases/sql_app/schemas.py similarity index 100% rename from docs/src/sql_databases/sql_app/schemas.py rename to docs_src/sql_databases/sql_app/schemas.py diff --git a/docs/src/sql_databases_peewee/__init__.py b/docs_src/sql_databases_peewee/__init__.py similarity index 100% rename from docs/src/sql_databases_peewee/__init__.py rename to docs_src/sql_databases_peewee/__init__.py diff --git a/docs/src/sql_databases_peewee/sql_app/__init__.py b/docs_src/sql_databases_peewee/sql_app/__init__.py similarity index 100% rename from docs/src/sql_databases_peewee/sql_app/__init__.py rename to docs_src/sql_databases_peewee/sql_app/__init__.py diff --git a/docs/src/sql_databases_peewee/sql_app/crud.py b/docs_src/sql_databases_peewee/sql_app/crud.py similarity index 100% rename from docs/src/sql_databases_peewee/sql_app/crud.py rename to docs_src/sql_databases_peewee/sql_app/crud.py diff --git a/docs/src/sql_databases_peewee/sql_app/database.py b/docs_src/sql_databases_peewee/sql_app/database.py similarity index 100% rename from docs/src/sql_databases_peewee/sql_app/database.py rename to docs_src/sql_databases_peewee/sql_app/database.py diff --git a/docs/src/sql_databases_peewee/sql_app/main.py b/docs_src/sql_databases_peewee/sql_app/main.py similarity index 100% rename from docs/src/sql_databases_peewee/sql_app/main.py rename to docs_src/sql_databases_peewee/sql_app/main.py diff --git a/docs/src/sql_databases_peewee/sql_app/models.py b/docs_src/sql_databases_peewee/sql_app/models.py similarity index 100% rename from docs/src/sql_databases_peewee/sql_app/models.py rename to docs_src/sql_databases_peewee/sql_app/models.py diff --git a/docs/src/sql_databases_peewee/sql_app/schemas.py b/docs_src/sql_databases_peewee/sql_app/schemas.py similarity index 100% rename from docs/src/sql_databases_peewee/sql_app/schemas.py rename to docs_src/sql_databases_peewee/sql_app/schemas.py diff --git a/docs/src/static_files/tutorial001.py b/docs_src/static_files/tutorial001.py similarity index 100% rename from docs/src/static_files/tutorial001.py rename to docs_src/static_files/tutorial001.py diff --git a/docs/src/sub_applications/tutorial001.py b/docs_src/sub_applications/tutorial001.py similarity index 100% rename from docs/src/sub_applications/tutorial001.py rename to docs_src/sub_applications/tutorial001.py diff --git a/docs/src/templates/static/styles.css b/docs_src/templates/static/styles.css similarity index 100% rename from docs/src/templates/static/styles.css rename to docs_src/templates/static/styles.css diff --git a/docs/src/templates/templates/item.html b/docs_src/templates/templates/item.html similarity index 100% rename from docs/src/templates/templates/item.html rename to docs_src/templates/templates/item.html diff --git a/docs/src/templates/tutorial001.py b/docs_src/templates/tutorial001.py similarity index 100% rename from docs/src/templates/tutorial001.py rename to docs_src/templates/tutorial001.py diff --git a/docs/src/using_request_directly/tutorial001.py b/docs_src/using_request_directly/tutorial001.py similarity index 100% rename from docs/src/using_request_directly/tutorial001.py rename to docs_src/using_request_directly/tutorial001.py diff --git a/docs/src/websockets/__init__.py b/docs_src/websockets/__init__.py similarity index 100% rename from docs/src/websockets/__init__.py rename to docs_src/websockets/__init__.py diff --git a/docs/src/websockets/tutorial001.py b/docs_src/websockets/tutorial001.py similarity index 100% rename from docs/src/websockets/tutorial001.py rename to docs_src/websockets/tutorial001.py diff --git a/docs/src/websockets/tutorial002.py b/docs_src/websockets/tutorial002.py similarity index 100% rename from docs/src/websockets/tutorial002.py rename to docs_src/websockets/tutorial002.py diff --git a/docs/src/wsgi/tutorial001.py b/docs_src/wsgi/tutorial001.py similarity index 100% rename from docs/src/wsgi/tutorial001.py rename to docs_src/wsgi/tutorial001.py diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index bacae9baca886..0000000000000 --- a/mkdocs.yml +++ /dev/null @@ -1,150 +0,0 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/ - -theme: - name: 'material' - palette: - primary: 'teal' - accent: 'amber' - logo: 'img/icon-white.svg' - favicon: 'img/favicon.png' - -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -google_analytics: - - 'UA-133183413-1' - - 'auto' - -nav: - - FastAPI: 'index.md' - - Features: 'features.md' - - Python types intro: 'python-types.md' - - Tutorial - User Guide: - - Tutorial - User Guide - Intro: 'tutorial/index.md' - - First Steps: 'tutorial/first-steps.md' - - Path Parameters: 'tutorial/path-params.md' - - Query Parameters: 'tutorial/query-params.md' - - Request Body: 'tutorial/body.md' - - Query Parameters and String Validations: 'tutorial/query-params-str-validations.md' - - Path Parameters and Numeric Validations: 'tutorial/path-params-numeric-validations.md' - - Body - Multiple Parameters: 'tutorial/body-multiple-params.md' - - Body - Fields: 'tutorial/body-fields.md' - - Body - Nested Models: 'tutorial/body-nested-models.md' - - Extra data types: 'tutorial/extra-data-types.md' - - Cookie Parameters: 'tutorial/cookie-params.md' - - Header Parameters: 'tutorial/header-params.md' - - Response Model: 'tutorial/response-model.md' - - Extra Models: 'tutorial/extra-models.md' - - Response Status Code: 'tutorial/response-status-code.md' - - Form Data: 'tutorial/request-forms.md' - - Request Files: 'tutorial/request-files.md' - - Request Forms and Files: 'tutorial/request-forms-and-files.md' - - Handling Errors: 'tutorial/handling-errors.md' - - Path Operation Configuration: 'tutorial/path-operation-configuration.md' - - JSON Compatible Encoder: 'tutorial/encoder.md' - - Body - updates: 'tutorial/body-updates.md' - - Dependencies: - - First Steps: 'tutorial/dependencies/index.md' - - Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md' - - Sub-dependencies: 'tutorial/dependencies/sub-dependencies.md' - - Dependencies in path operation decorators: 'tutorial/dependencies/dependencies-in-path-operation-decorators.md' - - Dependencies with yield: 'tutorial/dependencies/dependencies-with-yield.md' - - Security: - - Security Intro: 'tutorial/security/index.md' - - First Steps: 'tutorial/security/first-steps.md' - - Get Current User: 'tutorial/security/get-current-user.md' - - Simple OAuth2 with Password and Bearer: 'tutorial/security/simple-oauth2.md' - - OAuth2 with Password (and hashing), Bearer with JWT tokens: 'tutorial/security/oauth2-jwt.md' - - Middleware: 'tutorial/middleware.md' - - CORS (Cross-Origin Resource Sharing): 'tutorial/cors.md' - - SQL (Relational) Databases: 'tutorial/sql-databases.md' - - Bigger Applications - Multiple Files: 'tutorial/bigger-applications.md' - - Background Tasks: 'tutorial/background-tasks.md' - - Application Configuration: 'tutorial/application-configuration.md' - - Static Files: 'tutorial/static-files.md' - - Testing: 'tutorial/testing.md' - - Debugging: 'tutorial/debugging.md' - - Advanced User Guide: - - Advanced User Guide - Intro: 'advanced/index.md' - - Path Operation Advanced Configuration: 'advanced/path-operation-advanced-configuration.md' - - Additional Status Codes: 'advanced/additional-status-codes.md' - - Return a Response Directly: 'advanced/response-directly.md' - - Custom Response - HTML, Stream, File, others: 'advanced/custom-response.md' - - Additional Responses in OpenAPI: 'advanced/additional-responses.md' - - Response Cookies: 'advanced/response-cookies.md' - - Response Headers: 'advanced/response-headers.md' - - Response - Change Status Code: 'advanced/response-change-status-code.md' - - Advanced Dependencies: 'advanced/advanced-dependencies.md' - - Advanced Security: - - Advanced Security - Intro: 'advanced/security/index.md' - - OAuth2 scopes: 'advanced/security/oauth2-scopes.md' - - HTTP Basic Auth: 'advanced/security/http-basic-auth.md' - - Using the Request Directly: 'advanced/using-request-directly.md' - - Advanced Middleware: 'advanced/middleware.md' - - SQL (Relational) Databases with Peewee: 'advanced/sql-databases-peewee.md' - - Async SQL (Relational) Databases: 'advanced/async-sql-databases.md' - - NoSQL (Distributed / Big Data) Databases: 'advanced/nosql-databases.md' - - Sub Applications - Behind a Proxy, Mounts: 'advanced/sub-applications-proxy.md' - - Templates: 'advanced/templates.md' - - GraphQL: 'advanced/graphql.md' - - WebSockets: 'advanced/websockets.md' - - 'Events: startup - shutdown': 'advanced/events.md' - - Custom Request and APIRoute class: 'advanced/custom-request-and-route.md' - - Testing WebSockets: 'advanced/testing-websockets.md' - - 'Testing Events: startup - shutdown': 'advanced/testing-events.md' - - Testing Dependencies with Overrides: 'advanced/testing-dependencies.md' - - Extending OpenAPI: 'advanced/extending-openapi.md' - - OpenAPI Callbacks: 'advanced/openapi-callbacks.md' - - Including WSGI - Flask, Django, others: 'advanced/wsgi.md' - - Concurrency and async / await: 'async.md' - - Deployment: 'deployment.md' - - Project Generation - Template: 'project-generation.md' - - Alternatives, Inspiration and Comparisons: 'alternatives.md' - - History, Design and Future: 'history-design-future.md' - - External Links and Articles: 'external-links.md' - - Benchmarks: 'benchmarks.md' - - Help FastAPI - Get Help: 'help-fastapi.md' - - Development - Contributing: 'contributing.md' - - Release Notes: 'release-notes.md' - -markdown_extensions: - - toc: - permalink: true - - markdown.extensions.codehilite: - guess_lang: false - - markdown_include.include: - base_path: docs - - admonition - - codehilite - - extra - - pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_div_format - -extra: - social: - - type: 'github' - link: 'https://github.com/tiangolo/typer' - - type: 'twitter' - link: 'https://twitter.com/tiangolo' - - type: 'linkedin' - link: 'https://www.linkedin.com/in/tiangolo' - - type: 'rss' - link: 'https://dev.to/tiangolo' - - type: 'medium' - link: 'https://medium.com/@tiangolo' - - type: 'globe' - link: 'https://tiangolo.com' - -extra_css: - - 'css/termynal.css' - - 'css/custom.css' - -extra_javascript: - - 'https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js' - - 'js/termynal.js' - - 'js/custom.js' diff --git a/pyproject.toml b/pyproject.toml index 58e53c3eec2f5..eadff42bb8dde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,10 @@ test = [ doc = [ "mkdocs", "mkdocs-material", - "markdown-include" + "markdown-include", + "typer", + "typer-cli", + "pyyaml" ] dev = [ "pyjwt", diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh index 4f4ae2f74e403..383ad3f4465c7 100755 --- a/scripts/build-docs.sh +++ b/scripts/build-docs.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -python -m mkdocs build +set -e +set -x -cp ./docs/index.md ./README.md +python ./scripts/docs.py build-all diff --git a/scripts/docs.py b/scripts/docs.py new file mode 100644 index 0000000000000..490f0526483c8 --- /dev/null +++ b/scripts/docs.py @@ -0,0 +1,340 @@ +import os +import shutil +from http.server import HTTPServer, SimpleHTTPRequestHandler +from pathlib import Path +from typing import Dict, Optional, Tuple + +import mkdocs.commands.build +import mkdocs.commands.serve +import mkdocs.config +import mkdocs.utils +import typer +import yaml + +app = typer.Typer() + +mkdocs_name = "mkdocs.yml" + +missing_translation_snippet = """ +{!../../../docs/missing-translation.md!} +""" + +docs_path = Path("docs") + + +def lang_callback(lang: Optional[str]): + if lang is None: + return + if not lang.isalpha() or len(lang) != 2: + typer.echo("Use a 2 letter language code, like: es") + raise typer.Abort() + lang = lang.lower() + return lang + + +def complete_existing_lang(incomplete: str): + lang_path: Path + for lang_path in docs_path.iterdir(): + if lang_path.is_dir() and lang_path.name.startswith(incomplete): + yield lang_path.name + + +@app.command() +def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): + """ + Generate a new docs translation directory for the language LANG. + + LANG should be a 2-letter language code, like: en, es, de, pt, etc. + """ + new_path: Path = Path("docs") / lang + if new_path.exists(): + typer.echo(f"The language was already created: {lang}") + raise typer.Abort() + new_path.mkdir() + en_docs_path = Path("docs/en") + en_config_path: Path = en_docs_path / mkdocs_name + en_config: dict = mkdocs.utils.yaml_load(en_config_path.read_text()) + fastapi_url_base = "https://fastapi.tiangolo.com/" + new_config = {} + new_config["site_name"] = en_config["site_name"] + new_config["site_description"] = en_config["site_description"] + new_config["site_url"] = en_config["site_url"] + f"{lang}/" + new_config["theme"] = en_config["theme"] + new_config["theme"]["logo"] = fastapi_url_base + en_config["theme"]["logo"] + new_config["theme"]["favicon"] = fastapi_url_base + en_config["theme"]["favicon"] + new_config["theme"]["language"] = lang + new_config["repo_name"] = en_config["repo_name"] + new_config["repo_url"] = en_config["repo_url"] + new_config["edit_uri"] = en_config["edit_uri"] + new_config["google_analytics"] = en_config["google_analytics"] + new_config["nav"] = en_config["nav"][:2] + + new_config["markdown_extensions"] = en_config["markdown_extensions"] + new_config["extra"] = en_config["extra"] + + extra_css = [] + css: str + for css in en_config["extra_css"]: + if css.startswith("http"): + extra_css.append(css) + else: + extra_css.append(fastapi_url_base + css) + new_config["extra_css"] = extra_css + + extra_js = [] + js: str + for js in en_config["extra_javascript"]: + if js.startswith("http"): + extra_js.append(js) + else: + extra_js.append(fastapi_url_base + js) + new_config["extra_javascript"] = extra_js + new_config_path: Path = Path(new_path) / mkdocs_name + new_config_path.write_text(yaml.dump(new_config, sort_keys=False, width=200)) + new_config_docs_path: Path = new_path / "docs" + new_config_docs_path.mkdir() + en_index_path: Path = en_docs_path / "docs" / "index.md" + new_index_path: Path = new_config_docs_path / "index.md" + en_index_content = en_index_path.read_text() + new_index_content = f"{missing_translation_snippet}\n\n{en_index_content}" + new_index_path.write_text(new_index_content) + typer.secho(f"Successfully initialized: {new_path}", color=typer.colors.GREEN) + update_languages(lang=None) + + +@app.command() +def build_lang( + lang: str = typer.Argument( + ..., callback=lang_callback, autocompletion=complete_existing_lang + ) +): + """ + Build the docs for a language, filling missing pages with translation notifications. + """ + lang_path: Path = Path("docs") / lang + if not lang_path.is_dir(): + typer.echo(f"The language translation doesn't seem to exist yet: {lang}") + raise typer.Abort() + typer.echo(f"Building docs for: {lang}") + build_dir_path = Path("docs_build") + build_dir_path.mkdir(exist_ok=True) + build_lang_path = build_dir_path / lang + en_lang_path = Path("docs/en") + site_path = Path("site").absolute() + if lang == "en": + dist_path = site_path + else: + dist_path: Path = site_path / lang + shutil.rmtree(build_lang_path, ignore_errors=True) + shutil.copytree(lang_path, build_lang_path) + en_config_path: Path = en_lang_path / mkdocs_name + en_config: dict = mkdocs.utils.yaml_load(en_config_path.read_text()) + nav = en_config["nav"] + lang_config_path: Path = lang_path / mkdocs_name + lang_config: dict = mkdocs.utils.yaml_load(lang_config_path.read_text()) + lang_nav = lang_config["nav"] + # Exclude first 2 entries FastAPI and Languages, for custom handling + use_nav = nav[2:] + lang_use_nav = lang_nav[2:] + file_to_nav = get_file_to_nav_map(use_nav) + sections = get_sections(use_nav) + lang_file_to_nav = get_file_to_nav_map(lang_use_nav) + use_lang_file_to_nav = get_file_to_nav_map(lang_use_nav) + for file in file_to_nav: + file_path = Path(file) + lang_file_path: Path = build_lang_path / "docs" / file_path + en_file_path: Path = en_lang_path / "docs" / file_path + lang_file_path.parent.mkdir(parents=True, exist_ok=True) + if not lang_file_path.is_file(): + en_text = en_file_path.read_text() + lang_text = get_text_with_translate_missing(en_text) + lang_file_path.write_text(lang_text) + file_key = file_to_nav[file] + use_lang_file_to_nav[file] = file_key + if file_key: + composite_key = () + new_key = () + for key_part in file_key: + composite_key += (key_part,) + key_first_file = sections[composite_key] + if key_first_file in lang_file_to_nav: + new_key = lang_file_to_nav[key_first_file] + else: + new_key += (key_part,) + use_lang_file_to_nav[file] = new_key + key_to_section = {(): []} + for file, file_key in use_lang_file_to_nav.items(): + section = get_key_section(key_to_section=key_to_section, key=file_key) + section.append(file) + new_nav = key_to_section[()] + export_lang_nav = [lang_nav[0], nav[1]] + new_nav + lang_config["nav"] = export_lang_nav + build_lang_config_path: Path = build_lang_path / mkdocs_name + build_lang_config_path.write_text( + yaml.dump(lang_config, sort_keys=False, width=200) + ) + current_dir = os.getcwd() + os.chdir(build_lang_path) + mkdocs.commands.build.build(mkdocs.config.load_config(site_dir=str(dist_path))) + os.chdir(current_dir) + typer.secho(f"Successfully built docs for: {lang}", color=typer.colors.GREEN) + + +@app.command() +def build_all(): + """ + Build mkdocs site for en, and then build each language inside, end result is located + at directory ./site/ with each language inside. + """ + site_path = Path("site").absolute() + update_languages(lang=None) + en_build_path: Path = docs_path / "en" + current_dir = os.getcwd() + os.chdir(en_build_path) + typer.echo(f"Building docs for: en") + mkdocs.commands.build.build(mkdocs.config.load_config(site_dir=str(site_path))) + os.chdir(current_dir) + for lang in docs_path.iterdir(): + if lang == en_build_path or not lang.is_dir(): + continue + build_lang(lang.name) + typer.echo("Copying en index.md to README.md") + en_index = en_build_path / "docs" / "index.md" + shutil.copyfile(en_index, "README.md") + + +@app.command() +def update_languages( + lang: str = typer.Argument( + None, callback=lang_callback, autocompletion=complete_existing_lang + ) +): + """ + Update the mkdocs.yml file Languages section including all the available languages. + + The LANG argument is a 2-letter language code. If it's not provided, update all the + mkdocs.yml files (for all the languages). + """ + if lang is None: + for lang_path in docs_path.iterdir(): + if lang_path.is_dir(): + typer.echo(f"Updating {lang_path.name}") + update_config(lang_path.name) + else: + typer.echo(f"Updating {lang}") + update_config(lang) + + +@app.command() +def serve(): + """ + A quick server to preview a built site with translations. + + For development, prefer the command live (or just mkdocs serve). + + This is here only to preview a site with translations already built. + + Make sure you run the build-all command first. + """ + typer.echo( + "Warning: this is a very simple server." + + "For development, use mkdocs serve instead." + ) + typer.echo("This is here only to preview a site with translations already built.") + typer.echo("Make sure you run the build-all command first.") + os.chdir("site") + server_address = ("", 8008) + server = HTTPServer(server_address, SimpleHTTPRequestHandler) + typer.echo(f"Serving at: http://0.0.0.0:8008") + server.serve_forever() + + +@app.command() +def live( + lang: str = typer.Argument( + None, callback=lang_callback, autocompletion=complete_existing_lang + ) +): + """ + Serve with livereload a docs site for a specific language. + + This only shows the actual translated files, not the placeholders created with + build-all. + + Takes an optional LANG argument with the name of the language to serve, by default + en. + """ + if lang is None: + lang = "en" + lang_path: Path = docs_path / lang + os.chdir(lang_path) + mkdocs.commands.serve.serve(dev_addr="0.0.0.0:8008") + + +def update_config(lang: str): + lang_path: Path = docs_path / lang + config_path = lang_path / mkdocs_name + config: dict = mkdocs.utils.yaml_load(config_path.read_text()) + languages = [{"en": "/"}] + for lang in docs_path.iterdir(): + if lang.name == "en" or not lang.is_dir(): + continue + name = lang.name + languages.append({name: f"/{name}/"}) + config["nav"][1] = {"Languages": languages} + config_path.write_text(yaml.dump(config, sort_keys=False, width=200)) + + +def get_key_section( + *, key_to_section: Dict[Tuple[str, ...], list], key: Tuple[str, ...] +) -> list: + if key in key_to_section: + return key_to_section[key] + super_key = key[:-1] + title = key[-1] + super_section = get_key_section(key_to_section=key_to_section, key=super_key) + new_section = [] + super_section.append({title: new_section}) + key_to_section[key] = new_section + return new_section + + +def get_text_with_translate_missing(text: str) -> str: + lines = text.splitlines() + lines.insert(1, missing_translation_snippet) + new_text = "\n".join(lines) + return new_text + + +def get_file_to_nav_map(nav: list) -> Dict[str, Tuple[str, ...]]: + file_to_nav = {} + for item in nav: + if type(item) is str: + file_to_nav[item] = tuple() + elif type(item) is dict: + item_key = list(item.keys())[0] + sub_nav = item[item_key] + sub_file_to_nav = get_file_to_nav_map(sub_nav) + for k, v in sub_file_to_nav.items(): + file_to_nav[k] = (item_key,) + v + return file_to_nav + + +def get_sections(nav: list) -> Dict[Tuple[str, ...], str]: + sections = {} + for item in nav: + if type(item) is str: + continue + elif type(item) is dict: + item_key = list(item.keys())[0] + sub_nav = item[item_key] + sections[(item_key,)] = sub_nav[0] + sub_sections = get_sections(sub_nav) + for k, v in sub_sections.items(): + new_key = (item_key,) + k + sections[new_key] = v + return sections + + +if __name__ == "__main__": + app() diff --git a/scripts/format-imports.sh b/scripts/format-imports.sh index cfa66bbd46069..d2c0b7a5874a1 100755 --- a/scripts/format-imports.sh +++ b/scripts/format-imports.sh @@ -2,5 +2,5 @@ set -x # Sort imports one per line, so autoflake can remove unused imports -isort --recursive --force-single-line-imports --thirdparty fastapi --apply fastapi tests docs/src +isort --recursive --force-single-line-imports --thirdparty fastapi --apply fastapi tests docs_src scripts sh ./scripts/format.sh diff --git a/scripts/format.sh b/scripts/format.sh index c11eaf749483b..bbcb04354b01a 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,6 +1,6 @@ #!/bin/sh -e set -x -autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place docs/src/ fastapi tests --exclude=__init__.py -black fastapi tests docs/src -isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --thirdparty fastapi --apply fastapi tests docs/src +autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place docs_src fastapi tests scripts --exclude=__init__.py +black fastapi tests docs_src scripts +isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --thirdparty fastapi --apply fastapi tests docs_src scripts diff --git a/scripts/test.sh b/scripts/test.sh index d1a4cf630ee39..468b2c6674c83 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -9,6 +9,6 @@ if [ -f ./test.db ]; then fi bash ./scripts/lint.sh # Check README.md is up to date -diff --brief docs/index.md README.md -export PYTHONPATH=./docs/src -pytest --cov=fastapi --cov=tests --cov=docs/src --cov-report=term-missing ${@} +diff --brief docs/en/docs/index.md README.md +export PYTHONPATH=./docs_src +pytest --cov=fastapi --cov=tests --cov=docs/src --cov-report=term-missing tests ${@} diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index 09a06d957771e..998070b3d66c4 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -107,7 +107,7 @@ def test_path_operation(): def test_path_operation_img(): - shutil.copy("./docs/img/favicon.png", "./image.png") + shutil.copy("./docs/en/docs/img/favicon.png", "./image.png") response = client.get("/items/foo?img=1") assert response.status_code == 200 assert response.headers["Content-Type"] == "image/png" diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index d1285c8ea2009..11f1227e45a68 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -110,7 +110,7 @@ def test_path_operation(): def test_path_operation_img(): - shutil.copy("./docs/img/favicon.png", "./image.png") + shutil.copy("./docs/en/docs/img/favicon.png", "./image.png") response = client.get("/items/foo?img=1") assert response.status_code == 200 assert response.headers["Content-Type"] == "image/png" diff --git a/tests/test_tutorial/test_templates/test_tutorial001.py b/tests/test_tutorial/test_templates/test_tutorial001.py index 48d6456b49ad1..95c5b1ed36eab 100644 --- a/tests/test_tutorial/test_templates/test_tutorial001.py +++ b/tests/test_tutorial/test_templates/test_tutorial001.py @@ -4,8 +4,8 @@ def test_main(): - shutil.copytree("./docs/src/templates/templates/", "./templates") - shutil.copytree("./docs/src/templates/static/", "./static") + shutil.copytree("./docs_src/templates/templates/", "./templates") + shutil.copytree("./docs_src/templates/static/", "./static") from templates.tutorial001 import app client = TestClient(app) From 71c2abb41d468d61cb16180fcb143f1aca07421b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 26 Mar 2020 20:13:03 +0100 Subject: [PATCH 004/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d5218e063fb8a..fb1ca7767da97 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add support for docs translations. New docs: [Development - Contributing: Docs: Translations](https://fastapi.tiangolo.com/contributing/#translations). PR [#1168](https://github.com/tiangolo/fastapi/pull/1168). * Update terminal styles in docs and add note about [**Typer**, the FastAPI of CLIs](https://typer.tiangolo.com/). PR [#1139](https://github.com/tiangolo/fastapi/pull/1139). ## 0.52.0 From c13b54ad0e60a662ff16a17ea102021ca3c48b8b Mon Sep 17 00:00:00 2001 From: Camila Gutierrez Date: Thu, 26 Mar 2020 22:22:34 +0100 Subject: [PATCH 005/153] :checkered_flag: Change docs address to localhost for Windows (#1169) --- docs/en/docs/contributing.md | 8 ++++---- scripts/docs.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index a08991a1579a1..12387106c2c4d 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -201,14 +201,14 @@ During local development, there is a script that builds the site and checks for ```console $ python ./scripts/docs.py live -[INFO] Serving on http://0.0.0.0:8008 +[INFO] Serving on http://127.0.0.1:8008 [INFO] Start watching changes [INFO] Start detecting changes ``` -It will serve the documentation on `http://0.0.0.0:8008`. +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. @@ -280,14 +280,14 @@ Now run the live server for the docs in Spanish: // Use the command "live" and pass the language code as a CLI argument $ python ./scripts/docs.py live es -[INFO] Serving on http://0.0.0.0:8008 +[INFO] Serving on http://127.0.0.1:8008 [INFO] Start watching changes [INFO] Start detecting changes ``` -Now you can go to http://0.0.0.0:8008 and see your changes live. +Now you can go to http://127.0.0.1:8008 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 are not translated and have a notification about the the translation is missing. diff --git a/scripts/docs.py b/scripts/docs.py index 490f0526483c8..03bf3ca818caf 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -245,7 +245,7 @@ def serve(): os.chdir("site") server_address = ("", 8008) server = HTTPServer(server_address, SimpleHTTPRequestHandler) - typer.echo(f"Serving at: http://0.0.0.0:8008") + typer.echo(f"Serving at: http://127.0.0.1:8008") server.serve_forever() @@ -268,7 +268,7 @@ def live( lang = "en" lang_path: Path = docs_path / lang os.chdir(lang_path) - mkdocs.commands.serve.serve(dev_addr="0.0.0.0:8008") + mkdocs.commands.serve.serve(dev_addr="127.0.0.1:8008") def update_config(lang: str): From 53e773a2e135c535e7662e69073ef2a71e5348d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 26 Mar 2020 22:24:36 +0100 Subject: [PATCH 006/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fb1ca7767da97..c162bb0500de4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update development of FastAPI docs, set address to `127.0.0.1` to improve Windows support. PR [#1169](https://github.com/tiangolo/fastapi/pull/1169) by [@mariacamilagl](https://github.com/mariacamilagl). * Add support for docs translations. New docs: [Development - Contributing: Docs: Translations](https://fastapi.tiangolo.com/contributing/#translations). PR [#1168](https://github.com/tiangolo/fastapi/pull/1168). * Update terminal styles in docs and add note about [**Typer**, the FastAPI of CLIs](https://typer.tiangolo.com/). PR [#1139](https://github.com/tiangolo/fastapi/pull/1139). From 7625e1e386dd2bbe21300703a7d9377cb001d673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Mar 2020 10:20:39 +0100 Subject: [PATCH 007/153] :memo: Update and simplify translations docs (#1171) --- docs/en/docs/contributing.md | 96 ++++++++++++++++++++++++++---------- scripts/docs.py | 6 +-- 2 files changed, 71 insertions(+), 31 deletions(-) diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 12387106c2c4d..316eaf5164fa5 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -253,15 +253,19 @@ Help with translations is VERY MUCH appreciated! And it can't be done without th Here are the steps to help with translations. -#### Tips +#### Tips and guidelines -✨ Add a single Pull Request per page translated. That will make it much easier for others to review it. +* 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. +* 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. -✨ To check the 2-letter code for the language you want to translate you can use the table List of ISO 639-1 codes. +* 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 List of ISO 639-1 codes. #### Existing language @@ -293,42 +297,40 @@ If you look at the FastAPI docs website, you will see that every language has al 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 [Advanced User Guide: Extending OpenAPI](advanced/extending-openapi.md){.internal-link target=_blank}. +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/advanced/extending-openapi.md +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/advanced/extending-openapi.md +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 at: +* Now open the MkDocs config file for English at: ``` docs/en/docs/mkdocs.yml ``` -* Find the place where that `advanced/extending-openapi.md` is located in the config file. Somewhere like: +* Find the place where that `docs/features.md` is located in the config file. Somewhere like: -```YAML hl_lines="9" +```YAML hl_lines="8" site_name: FastAPI # More stuff nav: - FastAPI: index.md -# More stuff -- Advanced User Guide: - # More stuff - - advanced/testing-dependencies.md - - advanced/extending-openapi.md - - advanced/openapi-callbacks.md +- Languages: + - en: / + - es: /es/ +- features.md ``` * Open the MkDocs config file for the language you are editing, e.g.: @@ -337,24 +339,20 @@ nav: docs/es/docs/mkdocs.yml ``` -* Add the equivalent structure code and add it at the exact same location it would be, e.g.: +* Add it there at the exact same location it was for English, e.g.: -```YAML hl_lines="6 9" +```YAML hl_lines="8" site_name: FastAPI # More stuff nav: - FastAPI: index.md -# More stuff -- Guía de Usuario Avanzada: - # More stuff - - advanced/testing-dependencies.md - - advanced/extending-openapi.md - - advanced/openapi-callbacks.md +- Languages: + - en: / + - es: /es/ +- features.md ``` -Notice that the `Advanced User Guide` is translated to `Guía de Usuario Avanzada`. - -Also, make sure that if there are other entries, the new entry with your translation is in exactly in the same order as in the English version. +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. 🎉 @@ -416,6 +414,50 @@ 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: + +
+ +```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 +``` + +
+ +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`: + +
+ +```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 +``` + +
+ ## Tests There is a script that you can run locally to test all the code and generate coverage reports in HTML: diff --git a/scripts/docs.py b/scripts/docs.py index 03bf3ca818caf..2ee9b3adcc5d5 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -236,10 +236,8 @@ def serve(): Make sure you run the build-all command first. """ - typer.echo( - "Warning: this is a very simple server." - + "For development, use mkdocs serve instead." - ) + typer.echo("Warning: this is a very simple server.") + typer.echo("For development, use the command live instead.") typer.echo("This is here only to preview a site with translations already built.") typer.echo("Make sure you run the build-all command first.") os.chdir("site") From 433d7862eaf647de234d80e8ebfb436d2b4e6adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Mar 2020 10:21:40 +0100 Subject: [PATCH 008/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c162bb0500de4..c1a730275dd0c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update and simplify translations docs. PR [#1171](https://github.com/tiangolo/fastapi/pull/1171). * Update development of FastAPI docs, set address to `127.0.0.1` to improve Windows support. PR [#1169](https://github.com/tiangolo/fastapi/pull/1169) by [@mariacamilagl](https://github.com/mariacamilagl). * Add support for docs translations. New docs: [Development - Contributing: Docs: Translations](https://fastapi.tiangolo.com/contributing/#translations). PR [#1168](https://github.com/tiangolo/fastapi/pull/1168). * Update terminal styles in docs and add note about [**Typer**, the FastAPI of CLIs](https://typer.tiangolo.com/). PR [#1139](https://github.com/tiangolo/fastapi/pull/1139). From 2738df3801ac42e8499a482c4506e6e3117011c9 Mon Sep 17 00:00:00 2001 From: Aakash Nand <14219201+aakashnand@users.noreply.github.com> Date: Fri, 27 Mar 2020 23:32:15 +0900 Subject: [PATCH 009/153] :sparkles: Add Gitter chat to docs (#1061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * implemented chat feature using sidecar from starlette docs * :fire: Use external Gitter sidecar JS * :sparkles: Re-implement Gitter chat after multi-lang * :fire: Remove moved file Co-authored-by: Aakash Nand Co-authored-by: Sebastián Ramírez --- docs/en/docs/js/chat.js | 3 +++ docs/en/mkdocs.yml | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 docs/en/docs/js/chat.js diff --git a/docs/en/docs/js/chat.js b/docs/en/docs/js/chat.js new file mode 100644 index 0000000000000..debdef4dad5d0 --- /dev/null +++ b/docs/en/docs/js/chat.js @@ -0,0 +1,3 @@ +((window.gitter = {}).chat = {}).options = { + room: 'tiangolo/fastapi' +}; diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index eb3b54e9481cd..6fbaf960cdab3 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -145,3 +145,5 @@ extra_javascript: - https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js - js/termynal.js - js/custom.js +- js/chat.js +- https://sidecar.gitter.im/dist/sidecar.v1.js From 869c7389e22dc9ad659940fa271da76c4f3ba3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Mar 2020 15:34:16 +0100 Subject: [PATCH 010/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c1a730275dd0c..dd7f3a94e2caa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add Gitter chat to docs. PR [#1061](https://github.com/tiangolo/fastapi/pull/1061) by [@aakashnand](https://github.com/aakashnand). * Update and simplify translations docs. PR [#1171](https://github.com/tiangolo/fastapi/pull/1171). * Update development of FastAPI docs, set address to `127.0.0.1` to improve Windows support. PR [#1169](https://github.com/tiangolo/fastapi/pull/1169) by [@mariacamilagl](https://github.com/mariacamilagl). * Add support for docs translations. New docs: [Development - Contributing: Docs: Translations](https://fastapi.tiangolo.com/contributing/#translations). PR [#1168](https://github.com/tiangolo/fastapi/pull/1168). From aea04ee32ee1942e6e1a904527bb8da6ba76abd9 Mon Sep 17 00:00:00 2001 From: juhovh-aiven Date: Sat, 28 Mar 2020 02:19:17 +1100 Subject: [PATCH 011/153] :bug: Fix exclude_unset and aliases in response model validation (#1074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix exclude_unset and aliases in response model validation. * :sparkles: Use by_alias from param :shrug: Co-authored-by: Sebastián Ramírez --- fastapi/routing.py | 32 +++-- tests/test_serialize_response_model.py | 154 +++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 tests/test_serialize_response_model.py diff --git a/fastapi/routing.py b/fastapi/routing.py index b36104869648a..b90935e15ffe7 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -48,6 +48,28 @@ from pydantic.fields import Field as ModelField # type: ignore +def _prepare_response_content( + res: Any, *, by_alias: bool = True, exclude_unset: bool +) -> Any: + if isinstance(res, BaseModel): + if PYDANTIC_1: + return res.dict(by_alias=by_alias, exclude_unset=exclude_unset) + else: + return res.dict( + by_alias=by_alias, skip_defaults=exclude_unset + ) # pragma: nocover + elif isinstance(res, list): + return [ + _prepare_response_content(item, exclude_unset=exclude_unset) for item in res + ] + elif isinstance(res, dict): + return { + k: _prepare_response_content(v, exclude_unset=exclude_unset) + for k, v in res.items() + } + return res + + async def serialize_response( *, field: ModelField = None, @@ -60,13 +82,9 @@ async def serialize_response( ) -> Any: if field: errors = [] - if exclude_unset and isinstance(response_content, BaseModel): - if PYDANTIC_1: - response_content = response_content.dict(exclude_unset=exclude_unset) - else: - response_content = response_content.dict( - skip_defaults=exclude_unset - ) # pragma: nocover + response_content = _prepare_response_content( + response_content, by_alias=by_alias, exclude_unset=exclude_unset + ) if is_coroutine: value, errors_ = field.validate(response_content, {}, loc=("response",)) else: diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py new file mode 100644 index 0000000000000..adb7fda344f6a --- /dev/null +++ b/tests/test_serialize_response_model.py @@ -0,0 +1,154 @@ +from typing import Dict, List + +from fastapi import FastAPI +from pydantic import BaseModel, Field +from starlette.testclient import TestClient + +app = FastAPI() + + +class Item(BaseModel): + name: str = Field(..., alias="aliased_name") + price: float = None + owner_ids: List[int] = None + + +@app.get("/items/valid", response_model=Item) +def get_valid(): + return Item(aliased_name="valid", price=1.0) + + +@app.get("/items/coerce", response_model=Item) +def get_coerce(): + return Item(aliased_name="coerce", price="1.0") + + +@app.get("/items/validlist", response_model=List[Item]) +def get_validlist(): + return [ + Item(aliased_name="foo"), + Item(aliased_name="bar", price=1.0), + Item(aliased_name="baz", price=2.0, owner_ids=[1, 2, 3]), + ] + + +@app.get("/items/validdict", response_model=Dict[str, Item]) +def get_validdict(): + return { + "k1": Item(aliased_name="foo"), + "k2": Item(aliased_name="bar", price=1.0), + "k3": Item(aliased_name="baz", price=2.0, owner_ids=[1, 2, 3]), + } + + +@app.get( + "/items/valid-exclude-unset", response_model=Item, response_model_exclude_unset=True +) +def get_valid_exclude_unset(): + return Item(aliased_name="valid", price=1.0) + + +@app.get( + "/items/coerce-exclude-unset", + response_model=Item, + response_model_exclude_unset=True, +) +def get_coerce_exclude_unset(): + return Item(aliased_name="coerce", price="1.0") + + +@app.get( + "/items/validlist-exclude-unset", + response_model=List[Item], + response_model_exclude_unset=True, +) +def get_validlist_exclude_unset(): + return [ + Item(aliased_name="foo"), + Item(aliased_name="bar", price=1.0), + Item(aliased_name="baz", price=2.0, owner_ids=[1, 2, 3]), + ] + + +@app.get( + "/items/validdict-exclude-unset", + response_model=Dict[str, Item], + response_model_exclude_unset=True, +) +def get_validdict_exclude_unset(): + return { + "k1": Item(aliased_name="foo"), + "k2": Item(aliased_name="bar", price=1.0), + "k3": Item(aliased_name="baz", price=2.0, owner_ids=[1, 2, 3]), + } + + +client = TestClient(app) + + +def test_valid(): + response = client.get("/items/valid") + response.raise_for_status() + assert response.json() == {"aliased_name": "valid", "price": 1.0, "owner_ids": None} + + +def test_coerce(): + response = client.get("/items/coerce") + response.raise_for_status() + assert response.json() == { + "aliased_name": "coerce", + "price": 1.0, + "owner_ids": None, + } + + +def test_validlist(): + response = client.get("/items/validlist") + response.raise_for_status() + assert response.json() == [ + {"aliased_name": "foo", "price": None, "owner_ids": None}, + {"aliased_name": "bar", "price": 1.0, "owner_ids": None}, + {"aliased_name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, + ] + + +def test_validdict(): + response = client.get("/items/validdict") + response.raise_for_status() + assert response.json() == { + "k1": {"aliased_name": "foo", "price": None, "owner_ids": None}, + "k2": {"aliased_name": "bar", "price": 1.0, "owner_ids": None}, + "k3": {"aliased_name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, + } + + +def test_valid_exclude_unset(): + response = client.get("/items/valid-exclude-unset") + response.raise_for_status() + assert response.json() == {"aliased_name": "valid", "price": 1.0} + + +def test_coerce_exclude_unset(): + response = client.get("/items/coerce-exclude-unset") + response.raise_for_status() + assert response.json() == {"aliased_name": "coerce", "price": 1.0} + + +def test_validlist_exclude_unset(): + response = client.get("/items/validlist-exclude-unset") + response.raise_for_status() + assert response.json() == [ + {"aliased_name": "foo"}, + {"aliased_name": "bar", "price": 1.0}, + {"aliased_name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, + ] + + +def test_validdict_exclude_unset(): + response = client.get("/items/validdict-exclude-unset") + response.raise_for_status() + assert response.json() == { + "k1": {"aliased_name": "foo"}, + "k2": {"aliased_name": "bar", "price": 1.0}, + "k3": {"aliased_name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, + } From bc99ad0ad1c137efdb62011bbe958a4d30168b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Mar 2020 16:21:28 +0100 Subject: [PATCH 012/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index dd7f3a94e2caa..a29b0e05442bc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix serialization when using `by_alias` or `exclude_unset` and returning data with Pydantic models. PR [#1074](https://github.com/tiangolo/fastapi/pull/1074) by [@juhovh-aiven](https://github.com/juhovh-aiven). * Add Gitter chat to docs. PR [#1061](https://github.com/tiangolo/fastapi/pull/1061) by [@aakashnand](https://github.com/aakashnand). * Update and simplify translations docs. PR [#1171](https://github.com/tiangolo/fastapi/pull/1171). * Update development of FastAPI docs, set address to `127.0.0.1` to improve Windows support. PR [#1169](https://github.com/tiangolo/fastapi/pull/1169) by [@mariacamilagl](https://github.com/mariacamilagl). From bd37d8d04f5f6b49841e2d67e08f718b0d27e207 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 27 Mar 2020 17:02:18 +0100 Subject: [PATCH 013/153] :memo: Add new external link (#1112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added new external link I added an article in which I briefly explain how to build an Apache Kafka producer / consumer with FastAPI and aiokafka. * :memo: Update format Co-authored-by: Sebastián Ramírez --- docs/en/docs/external-links.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/external-links.md b/docs/en/docs/external-links.md index 4546befd042c5..6b63ee349640a 100644 --- a/docs/en/docs/external-links.md +++ b/docs/en/docs/external-links.md @@ -59,6 +59,8 @@ Here's an incomplete list of some of them. * Create and Deploy FastAPI app to Heroku without using Docker by Navule Pavan Kumar Rao. +* Apache Kafka producer and consumer with FastAPI and aiokafka by Benjamin Ramser. + ### Japanese * FastAPI|DB接続してCRUDするPython製APIサーバーを構築 by @mtitg. From 38fd8a674be4d628a215c78af8f962bb52884597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Mar 2020 17:04:25 +0100 Subject: [PATCH 014/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a29b0e05442bc..fd30aa131b3bf 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add external link [Apache Kafka producer and consumer with FastAPI and aiokafka](https://iwpnd.pw/articles/2020-03/apache-kafka-fastapi-geostream) to docs. PR [#1112](https://github.com/tiangolo/fastapi/pull/1112) by [@iwpnd](https://github.com/iwpnd). * Fix serialization when using `by_alias` or `exclude_unset` and returning data with Pydantic models. PR [#1074](https://github.com/tiangolo/fastapi/pull/1074) by [@juhovh-aiven](https://github.com/juhovh-aiven). * Add Gitter chat to docs. PR [#1061](https://github.com/tiangolo/fastapi/pull/1061) by [@aakashnand](https://github.com/aakashnand). * Update and simplify translations docs. PR [#1171](https://github.com/tiangolo/fastapi/pull/1171). From 15241b53a834d90fb4aa98225efab8fc29a67441 Mon Sep 17 00:00:00 2001 From: Tomoya Yoshioka Date: Sat, 28 Mar 2020 01:15:26 +0900 Subject: [PATCH 015/153] :memo: Clarify function name in example (#1121) --- docs_src/path_params/tutorial004.py | 2 +- tests/test_tutorial/test_path_params/test_tutorial004.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs_src/path_params/tutorial004.py b/docs_src/path_params/tutorial004.py index 76adf38217a10..2961e6178e7a6 100644 --- a/docs_src/path_params/tutorial004.py +++ b/docs_src/path_params/tutorial004.py @@ -4,5 +4,5 @@ @app.get("/files/{file_path:path}") -async def read_user_me(file_path: str): +async def read_file(file_path: str): return {"file_path": file_path} diff --git a/tests/test_tutorial/test_path_params/test_tutorial004.py b/tests/test_tutorial/test_path_params/test_tutorial004.py index a7004b91ad988..52edbf1b57ea0 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial004.py +++ b/tests/test_tutorial/test_path_params/test_tutorial004.py @@ -26,8 +26,8 @@ }, }, }, - "summary": "Read User Me", - "operationId": "read_user_me_files__file_path__get", + "summary": "Read File", + "operationId": "read_file_files__file_path__get", "parameters": [ { "required": True, From d1f067dc5b8a84bd248086b654ffe8bba900ab98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Mar 2020 17:16:45 +0100 Subject: [PATCH 016/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fd30aa131b3bf..1458bea70fb00 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Clarify function name in example in docs. PR [#1121](https://github.com/tiangolo/fastapi/pull/1121) by [@tmsick](https://github.com/tmsick). * Add external link [Apache Kafka producer and consumer with FastAPI and aiokafka](https://iwpnd.pw/articles/2020-03/apache-kafka-fastapi-geostream) to docs. PR [#1112](https://github.com/tiangolo/fastapi/pull/1112) by [@iwpnd](https://github.com/iwpnd). * Fix serialization when using `by_alias` or `exclude_unset` and returning data with Pydantic models. PR [#1074](https://github.com/tiangolo/fastapi/pull/1074) by [@juhovh-aiven](https://github.com/juhovh-aiven). * Add Gitter chat to docs. PR [#1061](https://github.com/tiangolo/fastapi/pull/1061) by [@aakashnand](https://github.com/aakashnand). From 1f53fef70a7cbb20267e0fb1166c29f90c84cba1 Mon Sep 17 00:00:00 2001 From: adg-mh <40580891+adg-mh@users.noreply.github.com> Date: Fri, 27 Mar 2020 11:18:23 -0500 Subject: [PATCH 017/153] :pencil2: Update doc string with correct class name (#1126) --- fastapi/security/oauth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 781293bb9570d..c9edbae428c3c 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -81,7 +81,7 @@ def login(form_data: Oauth2PasswordRequestFormStrict = Depends()): grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password". This dependency is strict about it. If you want to be permissive, use instead the - OAuth2PasswordRequestFormStrict dependency class. + OAuth2PasswordRequestForm dependency class. username: username string. The OAuth2 spec requires the exact field name "username". password: password string. The OAuth2 spec requires the exact field name "password". scope: Optional string. Several scopes (each one a string) separated by spaces. E.g. From 670b64360d8274f272870df612c1d24ca01b6061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Mar 2020 17:20:01 +0100 Subject: [PATCH 018/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1458bea70fb00..6330457a01d23 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix class name in docstring for `OAuth2PasswordRequestFormStrict`. PR [#1126](https://github.com/tiangolo/fastapi/pull/1126) by [@adg-mh](https://github.com/adg-mh). * Clarify function name in example in docs. PR [#1121](https://github.com/tiangolo/fastapi/pull/1121) by [@tmsick](https://github.com/tmsick). * Add external link [Apache Kafka producer and consumer with FastAPI and aiokafka](https://iwpnd.pw/articles/2020-03/apache-kafka-fastapi-geostream) to docs. PR [#1112](https://github.com/tiangolo/fastapi/pull/1112) by [@iwpnd](https://github.com/iwpnd). * Fix serialization when using `by_alias` or `exclude_unset` and returning data with Pydantic models. PR [#1074](https://github.com/tiangolo/fastapi/pull/1074) by [@juhovh-aiven](https://github.com/juhovh-aiven). From 651ced68bf68f71152f4a61eebc65ccd10d3ca46 Mon Sep 17 00:00:00 2001 From: Ari Bajo Date: Fri, 27 Mar 2020 17:27:57 +0100 Subject: [PATCH 019/153] :bug: Fix GZipMiddleware code block linking to TrustedHostMiddleware in docs (#1138) --- docs/en/docs/advanced/middleware.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/advanced/middleware.md b/docs/en/docs/advanced/middleware.md index 4db8af275c4e6..6e28940c10544 100644 --- a/docs/en/docs/advanced/middleware.md +++ b/docs/en/docs/advanced/middleware.md @@ -78,8 +78,8 @@ Handles GZip responses for any request that includes `"gzip"` in the `Accept-Enc The middleware will handle both standard and streaming responses. -```Python hl_lines="2 6 7 8" -{!../../../docs_src/advanced_middleware/tutorial002.py!} +```Python hl_lines="2 6" +{!../../../docs_src/advanced_middleware/tutorial003.py!} ``` The following arguments are supported: From 02a6fcad98c431af1c551a3e50b082f056b3a83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Mar 2020 17:29:11 +0100 Subject: [PATCH 020/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6330457a01d23..e7033585b7102 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix included example for `GZipMiddleware`. PR [#1138](https://github.com/tiangolo/fastapi/pull/1138) by [@arimbr](https://github.com/arimbr). * Fix class name in docstring for `OAuth2PasswordRequestFormStrict`. PR [#1126](https://github.com/tiangolo/fastapi/pull/1126) by [@adg-mh](https://github.com/adg-mh). * Clarify function name in example in docs. PR [#1121](https://github.com/tiangolo/fastapi/pull/1121) by [@tmsick](https://github.com/tmsick). * Add external link [Apache Kafka producer and consumer with FastAPI and aiokafka](https://iwpnd.pw/articles/2020-03/apache-kafka-fastapi-geostream) to docs. PR [#1112](https://github.com/tiangolo/fastapi/pull/1112) by [@iwpnd](https://github.com/iwpnd). From 3f8bfd62b7add84bad99dd9e302dedf320419a30 Mon Sep 17 00:00:00 2001 From: Michael Oliver <55017335+michael0liver@users.noreply.github.com> Date: Fri, 27 Mar 2020 16:32:35 +0000 Subject: [PATCH 021/153] :heavy_plus_sign: Add orjson to pip install fastapi[all] (#1161) --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index eadff42bb8dde..ea876c0a22f62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,7 @@ all = [ "pyyaml", "graphene", "ujson", + "orjson", "email_validator", "uvicorn", "async_exit_stack", From 372ed586772580455685a784e9ea01ec755a6bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Mar 2020 17:33:51 +0100 Subject: [PATCH 022/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e7033585b7102..aeb7b09146e60 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add `orjson` to `pip install fastapi[all]`. PR [#1161](https://github.com/tiangolo/fastapi/pull/1161) by [@michael0liver](https://github.com/michael0liver). * Fix included example for `GZipMiddleware`. PR [#1138](https://github.com/tiangolo/fastapi/pull/1138) by [@arimbr](https://github.com/arimbr). * Fix class name in docstring for `OAuth2PasswordRequestFormStrict`. PR [#1126](https://github.com/tiangolo/fastapi/pull/1126) by [@adg-mh](https://github.com/adg-mh). * Clarify function name in example in docs. PR [#1121](https://github.com/tiangolo/fastapi/pull/1121) by [@tmsick](https://github.com/tmsick). From 53a7798e58f62c1ffe91c6d298fe67148521cff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Mar 2020 17:51:58 +0100 Subject: [PATCH 023/153] :art: Update badge for coverage (#1175) from shields.io, as Codecov's one shows coverage for the last PR from a fork at master --- README.md | 2 +- docs/en/docs/index.md | 2 +- docs/es/docs/index.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 11c9453720dc7..7a12add10a7fe 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Build Status - Coverage + Coverage Package version diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 11c9453720dc7..7a12add10a7fe 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -9,7 +9,7 @@ Build Status - Coverage + Coverage Package version diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 11c9453720dc7..7a12add10a7fe 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -9,7 +9,7 @@ Build Status - Coverage + Coverage Package version From 10485cad5a14da47ebd91d07a0072289881f348d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Mar 2020 17:52:33 +0100 Subject: [PATCH 024/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index aeb7b09146e60..fdd8cf0740189 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update test coverage badge. PR [#1175](https://github.com/tiangolo/fastapi/pull/1175). * Add `orjson` to `pip install fastapi[all]`. PR [#1161](https://github.com/tiangolo/fastapi/pull/1161) by [@michael0liver](https://github.com/michael0liver). * Fix included example for `GZipMiddleware`. PR [#1138](https://github.com/tiangolo/fastapi/pull/1138) by [@arimbr](https://github.com/arimbr). * Fix class name in docstring for `OAuth2PasswordRequestFormStrict`. PR [#1126](https://github.com/tiangolo/fastapi/pull/1126) by [@adg-mh](https://github.com/adg-mh). From 56e43ba204e6a7f2f7468d55c8cc835e7b5616c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Mar 2020 17:56:41 +0100 Subject: [PATCH 025/153] :bookmark: Release version 0.53.0 --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fdd8cf0740189..4c077c396fe97 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +## 0.53.0 + * Update test coverage badge. PR [#1175](https://github.com/tiangolo/fastapi/pull/1175). * Add `orjson` to `pip install fastapi[all]`. PR [#1161](https://github.com/tiangolo/fastapi/pull/1161) by [@michael0liver](https://github.com/michael0liver). * Fix included example for `GZipMiddleware`. PR [#1138](https://github.com/tiangolo/fastapi/pull/1138) by [@arimbr](https://github.com/arimbr). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index af924de869cf2..0a6c7aa2dbf7d 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.52.0" +__version__ = "0.53.0" from starlette import status From 459f0e11e5e09b7a380f3f859b58e20455378f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 29 Mar 2020 17:04:04 +0200 Subject: [PATCH 026/153] :checkered_flag: Update Windows development environment and tests (#1179) * :checkered_flag: Fix ./scripts/docs.py encoding for Windows * :fire: Remove ujson from tests as it prevents Windows development It's still tested by Starlette anyway * :memo: Update development instructions for Windows * :art: Update format for WSGIMiddleware example * :white_check_mark: Update tests to run on Windows --- docs/en/docs/advanced/wsgi.md | 2 +- docs/en/docs/contributing.md | 32 +++++++++-------- docs_src/wsgi/tutorial001.py | 2 +- pyproject.toml | 1 - scripts/docs.py | 28 +++++++++------ .../test_tutorial003.py | 2 +- .../test_custom_response/test_tutorial001.py | 36 ------------------- 7 files changed, 37 insertions(+), 66 deletions(-) delete mode 100644 tests/test_tutorial/test_custom_response/test_tutorial001.py diff --git a/docs/en/docs/advanced/wsgi.md b/docs/en/docs/advanced/wsgi.md index 4b81aff2e77fa..0b917e7f7c1d9 100644 --- a/docs/en/docs/advanced/wsgi.md +++ b/docs/en/docs/advanced/wsgi.md @@ -12,7 +12,7 @@ Then wrap the WSGI (e.g. Flask) app with the middleware. And then mount that under a path. -```Python hl_lines="1 3 22" +```Python hl_lines="2 3 22" {!../../../docs_src/wsgi/tutorial001.py!} ``` diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 316eaf5164fa5..57767f4884fda 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -113,13 +113,25 @@ $ flit install --deps develop --symlink +If you are on Windows, use `--pth-file` instead of `--symlink`: + +
+ +```console +$ flit install --deps develop --pth-file + +---> 100% +``` + +
+ 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`, when you run that Python file again, it will use the fresh version of FastAPI you just edited. +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. @@ -137,17 +149,7 @@ $ bash scripts/format.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: - -
- -```console -$ flit install --symlink - ----> 100% -``` - -
+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 @@ -293,7 +295,7 @@ $ python ./scripts/docs.py live es Now you can go to
http://127.0.0.1:8008 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 are not translated and have a notification about the the translation is missing. +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. @@ -474,10 +476,10 @@ This command generates a directory `./htmlcov/`, if you open the file `./htmlcov ### Tests in your editor -If you want to use the integrated tests in your editor add `./docs/src` to your `PYTHONPATH` variable. +If you want to use the integrated tests in your editor add `./docs_src` to your `PYTHONPATH` variable. For example, in VS Code you can create a file `.env` with: ```env -PYTHONPATH=./docs/src +PYTHONPATH=./docs_src ``` diff --git a/docs_src/wsgi/tutorial001.py b/docs_src/wsgi/tutorial001.py index 7a4011e7e42cb..500ecf883eaf6 100644 --- a/docs_src/wsgi/tutorial001.py +++ b/docs_src/wsgi/tutorial001.py @@ -1,6 +1,6 @@ -from flask import Flask, escape, request from fastapi import FastAPI from fastapi.middleware.wsgi import WSGIMiddleware +from flask import Flask, escape, request flask_app = Flask(__name__) diff --git a/pyproject.toml b/pyproject.toml index ea876c0a22f62..07610fca0872f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,6 @@ test = [ "async_generator", "python-multipart", "aiofiles", - "ujson", "flask" ] doc = [ diff --git a/scripts/docs.py b/scripts/docs.py index 2ee9b3adcc5d5..345135e91b5d0 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -53,7 +53,7 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): new_path.mkdir() en_docs_path = Path("docs/en") en_config_path: Path = en_docs_path / mkdocs_name - en_config: dict = mkdocs.utils.yaml_load(en_config_path.read_text()) + en_config: dict = mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) fastapi_url_base = "https://fastapi.tiangolo.com/" new_config = {} new_config["site_name"] = en_config["site_name"] @@ -90,14 +90,16 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): extra_js.append(fastapi_url_base + js) new_config["extra_javascript"] = extra_js new_config_path: Path = Path(new_path) / mkdocs_name - new_config_path.write_text(yaml.dump(new_config, sort_keys=False, width=200)) + new_config_path.write_text( + yaml.dump(new_config, sort_keys=False, width=200), encoding="utf-8" + ) new_config_docs_path: Path = new_path / "docs" new_config_docs_path.mkdir() en_index_path: Path = en_docs_path / "docs" / "index.md" new_index_path: Path = new_config_docs_path / "index.md" - en_index_content = en_index_path.read_text() + en_index_content = en_index_path.read_text(encoding="utf-8") new_index_content = f"{missing_translation_snippet}\n\n{en_index_content}" - new_index_path.write_text(new_index_content) + new_index_path.write_text(new_index_content, encoding="utf-8") typer.secho(f"Successfully initialized: {new_path}", color=typer.colors.GREEN) update_languages(lang=None) @@ -128,10 +130,12 @@ def build_lang( shutil.rmtree(build_lang_path, ignore_errors=True) shutil.copytree(lang_path, build_lang_path) en_config_path: Path = en_lang_path / mkdocs_name - en_config: dict = mkdocs.utils.yaml_load(en_config_path.read_text()) + en_config: dict = mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) nav = en_config["nav"] lang_config_path: Path = lang_path / mkdocs_name - lang_config: dict = mkdocs.utils.yaml_load(lang_config_path.read_text()) + lang_config: dict = mkdocs.utils.yaml_load( + lang_config_path.read_text(encoding="utf-8") + ) lang_nav = lang_config["nav"] # Exclude first 2 entries FastAPI and Languages, for custom handling use_nav = nav[2:] @@ -146,9 +150,9 @@ def build_lang( en_file_path: Path = en_lang_path / "docs" / file_path lang_file_path.parent.mkdir(parents=True, exist_ok=True) if not lang_file_path.is_file(): - en_text = en_file_path.read_text() + en_text = en_file_path.read_text(encoding="utf-8") lang_text = get_text_with_translate_missing(en_text) - lang_file_path.write_text(lang_text) + lang_file_path.write_text(lang_text, encoding="utf-8") file_key = file_to_nav[file] use_lang_file_to_nav[file] = file_key if file_key: @@ -171,7 +175,7 @@ def build_lang( lang_config["nav"] = export_lang_nav build_lang_config_path: Path = build_lang_path / mkdocs_name build_lang_config_path.write_text( - yaml.dump(lang_config, sort_keys=False, width=200) + yaml.dump(lang_config, sort_keys=False, width=200), encoding="utf-8" ) current_dir = os.getcwd() os.chdir(build_lang_path) @@ -272,7 +276,7 @@ def live( def update_config(lang: str): lang_path: Path = docs_path / lang config_path = lang_path / mkdocs_name - config: dict = mkdocs.utils.yaml_load(config_path.read_text()) + config: dict = mkdocs.utils.yaml_load(config_path.read_text(encoding="utf-8")) languages = [{"en": "/"}] for lang in docs_path.iterdir(): if lang.name == "en" or not lang.is_dir(): @@ -280,7 +284,9 @@ def update_config(lang: str): name = lang.name languages.append({name: f"/{name}/"}) config["nav"][1] = {"Languages": languages} - config_path.write_text(yaml.dump(config, sort_keys=False, width=200)) + config_path.write_text( + yaml.dump(config, sort_keys=False, width=200), encoding="utf-8" + ) def get_key_section( diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py index 73905f9f5db3e..376b1f69ba6fd 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py @@ -15,4 +15,4 @@ def test_get_timed(): response = client.get("/timed") assert response.json() == {"message": "It's the time of my life"} assert "X-Response-Time" in response.headers - assert float(response.headers["X-Response-Time"]) > 0 + assert float(response.headers["X-Response-Time"]) >= 0 diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py deleted file mode 100644 index 0152d0134c9e2..0000000000000 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ /dev/null @@ -1,36 +0,0 @@ -from fastapi.testclient import TestClient - -from custom_response.tutorial001 import app - -client = TestClient(app) - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == openapi_schema - - -def test_get_custom_response(): - response = client.get("/items/") - assert response.status_code == 200 - assert response.json() == [{"item_id": "Foo"}] From c2ad214a84525417c423917f5f732527aec7b729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 29 Mar 2020 17:05:03 +0200 Subject: [PATCH 027/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4c077c396fe97..26c77b7221b0c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +* Update Windows development environment instructions. PR [#1179](https://github.com/tiangolo/fastapi/pull/1179). + ## 0.53.0 * Update test coverage badge. PR [#1175](https://github.com/tiangolo/fastapi/pull/1175). From c83c50b27df8e6224c9e3378d182f46487c5a894 Mon Sep 17 00:00:00 2001 From: Paul-Louis NECH <1821404+PLNech@users.noreply.github.com> Date: Sun, 29 Mar 2020 17:51:58 +0200 Subject: [PATCH 028/153] :pencil2: Fix typo (#1148) --- docs/en/docs/tutorial/security/first-steps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/security/first-steps.md b/docs/en/docs/tutorial/security/first-steps.md index c14ec2ad984ff..287e92c23ef39 100644 --- a/docs/en/docs/tutorial/security/first-steps.md +++ b/docs/en/docs/tutorial/security/first-steps.md @@ -54,7 +54,7 @@ You will see something like this: !!! check "Authorize button!" - You already have a shinny new "Authorize" button. + You already have a shiny new "Authorize" button. And your *path operation* has a little lock in the top-right corner that you can click. From 8ab916baed81e99240b301437183cac18b868ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 29 Mar 2020 17:53:49 +0200 Subject: [PATCH 029/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 26c77b7221b0c..1b4e3e46cbb05 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix typo in docs. PR [#1148](https://github.com/tiangolo/fastapi/pull/1148) by [@PLNech](https://github.com/PLNech). * Update Windows development environment instructions. PR [#1179](https://github.com/tiangolo/fastapi/pull/1179). ## 0.53.0 From 9d54215a3a440215c69786c1372923302e4f0d82 Mon Sep 17 00:00:00 2001 From: YangQuan Date: Mon, 30 Mar 2020 00:50:29 +0800 Subject: [PATCH 030/153] :memo: Add example of Pycharm in tutorial/debugging.md (#1096) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add example of pycharm in tutorial/debugging.md * :memo: Update PyCharm debug instructions and screenshot * :truck: Move image to new location in docs Co-authored-by: Sebastián Ramírez --- docs/en/docs/img/tutorial/debugging/image02.png | Bin 0 -> 99443 bytes docs/en/docs/tutorial/debugging.md | 15 +++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 docs/en/docs/img/tutorial/debugging/image02.png diff --git a/docs/en/docs/img/tutorial/debugging/image02.png b/docs/en/docs/img/tutorial/debugging/image02.png new file mode 100644 index 0000000000000000000000000000000000000000..8394f50e2d12d23937aa734f415620988eaa76ec GIT binary patch literal 99443 zcmb5WV{~P~);7AsPSWYvwr!)sj;$TrwmWvZW3z)D+wR!5ZJRfJ-tWAB?j7SBxBjeJ zyY^UBtET5OYlSPwNqmFDfdc>l-+oAnDggixT>twVxLVr|&;hU|?WY zw&XWIZ;_nEG@O;~Or6~f98Cbq<}S|8CXPk{ldu2)5#WcYu!{TgX{MXI%0TnS2Di<` zz0Qx)bpoyfQUF@=~mQ!!OObAYBo;islSpbM1%wb3I6l*A`TtM zOriF_w_*Mi{~h_icEuwm7@PQYv*mw@VgsUSSJJeg>L zR9NDHHFTbB0eXe(I57xM{M{!Ha4h*Mvaa7c`J!eq$q;_vp>#~H{j{GUk&5fHEHdX{ zB_*Yx&;lue`461gT3AriR`$9kPvzuW9OD$I&fI2VJJjYyBhx?uoLhW?>67@;Wg9jv zrjlAYkQ$L~_YJfPqaDJppykCB^9N}uW%-BlB$Om5DEh6eF(Yw=@nu@|yJ=Yqdw03o zOUrVEHz`edy7Oj^tD2#N!6UZdDFS2gRUI{vjC)z&ablrflU&nv45NPc|12<4H zeVN9ykfDfYab{jLI=6NiuIo=h>d&=0jD)36=-rCO2&IUgHEDoba~KHe3p%O{cTUe4 zgkAO-tde+VkM>fU=_~#*wJyJcFPZzpL;r2v7=UNP5AoR%cE4-A=Q^&qO^Nu{l=zMsT ztmbcmhG>>{>P)R)-e#OY%ZPsNZCVP|Dpfekg?!hm%K!7fywUv40l{SbZc9~DJ74pg z?OP4{J~`|gc*i^O%uL$?i|+9@_qMHVGJ8S`8+ z1iL1#*Rg?+1KmYD2?&6s>dMC%=K$oJsMR5CL`KfqfhRFHPdP>cz8n1v|Bz$0-ur*EN@7s*!1Sd%0D>?;OCSah^Q>zarQ^>->K%MkJW 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%0Ae+xjN}Ek)ClYte8+UP6D5uJo&cs0*K(nPx9ie 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 + +--- + +If you use Pycharm, you can: + +* Open the "Run" menu. +* Select the option "Debug...". +* Then a context menu shows up. +* Select the file to debug (in this case, `main.py`). + +It will then start the server with your **FastAPI** code, stop at your breakpoints, etc. + +Here's how it might look: + + From 0d165d1efa6f126b68b58e6be7b55ba90de896e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 29 Mar 2020 18:52:33 +0200 Subject: [PATCH 031/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1b4e3e46cbb05..23431cc2f840c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add docs for [Pycharm Debugging](https://fastapi.tiangolo.com/tutorial/debugging/). PR [#1096](https://github.com/tiangolo/fastapi/pull/1096) by [@youngquan](https://github.com/youngquan). * Fix typo in docs. PR [#1148](https://github.com/tiangolo/fastapi/pull/1148) by [@PLNech](https://github.com/PLNech). * Update Windows development environment instructions. PR [#1179](https://github.com/tiangolo/fastapi/pull/1179). From 0f152b4e97a102c0105f26d76d6e1bba3b12fc2a Mon Sep 17 00:00:00 2001 From: voegtlel <5764745+voegtlel@users.noreply.github.com> Date: Sun, 29 Mar 2020 19:26:29 +0200 Subject: [PATCH 032/153] :bug: Check already cloned fields in create_cloned_field to support recursive models (#1164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * FIX: #894 Include recursion check for create_cloned_field. Added test for recursive model. * :recycle: Refactor and format create_cloned_field() Co-authored-by: Lukas Voegtle Co-authored-by: Sebastián Ramírez --- fastapi/utils.py | 26 ++++++-- tests/test_validate_response_recursive.py | 80 +++++++++++++++++++++++ 2 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 tests/test_validate_response_recursive.py diff --git a/fastapi/utils.py b/fastapi/utils.py index f24f280739d19..154dd9aa180e3 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -131,17 +131,26 @@ def create_response_field( ) -def create_cloned_field(field: ModelField) -> ModelField: +def create_cloned_field( + field: ModelField, *, cloned_types: Dict[Type[BaseModel], Type[BaseModel]] = None, +) -> ModelField: + # _cloned_types has already cloned types, to support recursive models + if cloned_types is None: + cloned_types = dict() original_type = field.type_ if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"): original_type = original_type.__pydantic_model__ # type: ignore use_type = original_type if lenient_issubclass(original_type, BaseModel): original_type = cast(Type[BaseModel], original_type) - use_type = create_model(original_type.__name__, __base__=original_type) - for f in original_type.__fields__.values(): - use_type.__fields__[f.name] = create_cloned_field(f) - + use_type = cloned_types.get(original_type) + if use_type is None: + use_type = create_model(original_type.__name__, __base__=original_type) + cloned_types[original_type] = use_type + for f in original_type.__fields__.values(): + use_type.__fields__[f.name] = create_cloned_field( + f, cloned_types=cloned_types + ) new_field = create_response_field(name=field.name, type_=use_type) new_field.has_alias = field.has_alias new_field.alias = field.alias @@ -157,10 +166,13 @@ def create_cloned_field(field: ModelField) -> ModelField: new_field.validate_always = field.validate_always if field.sub_fields: new_field.sub_fields = [ - create_cloned_field(sub_field) for sub_field in field.sub_fields + create_cloned_field(sub_field, cloned_types=cloned_types) + for sub_field in field.sub_fields ] if field.key_field: - new_field.key_field = create_cloned_field(field.key_field) + new_field.key_field = create_cloned_field( + field.key_field, cloned_types=cloned_types + ) new_field.validators = field.validators if PYDANTIC_1: new_field.pre_validators = field.pre_validators diff --git a/tests/test_validate_response_recursive.py b/tests/test_validate_response_recursive.py new file mode 100644 index 0000000000000..8b77ed14a9285 --- /dev/null +++ b/tests/test_validate_response_recursive.py @@ -0,0 +1,80 @@ +from typing import List + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class RecursiveItem(BaseModel): + sub_items: List["RecursiveItem"] = [] + name: str + + +RecursiveItem.update_forward_refs() + + +class RecursiveSubitemInSubmodel(BaseModel): + sub_items2: List["RecursiveItemViaSubmodel"] = [] + name: str + + +class RecursiveItemViaSubmodel(BaseModel): + sub_items1: List[RecursiveSubitemInSubmodel] = [] + name: str + + +RecursiveSubitemInSubmodel.update_forward_refs() + + +@app.get("/items/recursive", response_model=RecursiveItem) +def get_recursive(): + return {"name": "item", "sub_items": [{"name": "subitem", "sub_items": []}]} + + +@app.get("/items/recursive-submodel", response_model=RecursiveItemViaSubmodel) +def get_recursive_submodel(): + return { + "name": "item", + "sub_items1": [ + { + "name": "subitem", + "sub_items2": [ + { + "name": "subsubitem", + "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], + } + ], + } + ], + } + + +client = TestClient(app) + + +def test_recursive(): + response = client.get("/items/recursive") + assert response.status_code == 200 + assert response.json() == { + "sub_items": [{"name": "subitem", "sub_items": []}], + "name": "item", + } + + response = client.get("/items/recursive-submodel") + assert response.status_code == 200 + assert response.json() == { + "name": "item", + "sub_items1": [ + { + "name": "subitem", + "sub_items2": [ + { + "name": "subsubitem", + "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], + } + ], + } + ], + } From be21b74ad5587bf2c716604d57b64b3aff14c0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 29 Mar 2020 19:28:53 +0200 Subject: [PATCH 033/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 23431cc2f840c..08bcda13ff433 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix using recursive models in `response_model`. PR [#1164](https://github.com/tiangolo/fastapi/pull/1164) by [@voegtlel](https://github.com/voegtlel). * Add docs for [Pycharm Debugging](https://fastapi.tiangolo.com/tutorial/debugging/). PR [#1096](https://github.com/tiangolo/fastapi/pull/1096) by [@youngquan](https://github.com/youngquan). * Fix typo in docs. PR [#1148](https://github.com/tiangolo/fastapi/pull/1148) by [@PLNech](https://github.com/PLNech). * Update Windows development environment instructions. PR [#1179](https://github.com/tiangolo/fastapi/pull/1179). From 016a4b74910572f85e4aabcd932e5a92bf2ee95c Mon Sep 17 00:00:00 2001 From: John Paton Date: Sun, 29 Mar 2020 21:43:31 +0200 Subject: [PATCH 034/153] :memo: Add documentation of example kwarg of Field (#1106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add documentation of example kwarg of Field * :memo: Update info about schema examples * :truck: Move example file to new directory Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/body-fields.md | 15 +++++++++++++-- docs_src/body_fields/tutorial003.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 docs_src/body_fields/tutorial003.py diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index daba2eaffa820..71843a3aae0f9 100644 --- a/docs/en/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -46,16 +46,27 @@ If you know JSON Schema and want to add extra information apart from what we hav !!! warning Have in mind that extra parameters passed won't add any validation, only annotation, for documentation purposes. -For example, you can use that functionality to pass a JSON Schema example field to a body request JSON Schema: +For example, you can use that functionality to pass an example for a body request: ```Python hl_lines="20 21 22 23 24 25" {!../../../docs_src/body_fields/tutorial002.py!} ``` -And it would look in the `/docs` like this: +Alternately, you can provide these extras on a per-field basis by using additional keyword arguments to `Field`: + +```Python hl_lines="2 8 9 10 11" +{!./src/body_fields/tutorial003.py!} +``` + +Either way, in the `/docs` it would look like this: +!!! note "Technical Details" + JSON Schema defines a field `examples` in the most recent versions, but OpenAPI is based on an older version of JSON Schema that didn't have `examples`. + + So, OpenAPI defines its own `example` for the same purpose (as `example`, not `examples`), and that's what is used by the docs UI (using Swagger UI). + ## Recap You can use Pydantic's `Field` to declare extra validations and metadata for model attributes. diff --git a/docs_src/body_fields/tutorial003.py b/docs_src/body_fields/tutorial003.py new file mode 100644 index 0000000000000..edf9897d08b98 --- /dev/null +++ b/docs_src/body_fields/tutorial003.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str = Field(..., example="Foo") + description: str = Field(None, example="A very nice Item") + price: float = Field(..., example=35.4) + tax: float = Field(None, example=3.2) + + +@app.put("/items/{item_id}") +async def update_item(*, item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results From 6c34600599afa8838c7e109093b526f5153097c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 29 Mar 2020 22:05:24 +0200 Subject: [PATCH 035/153] :bug: Fix include docs example file (#1182) --- docs/en/docs/tutorial/body-fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index 71843a3aae0f9..8792abee718dc 100644 --- a/docs/en/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -55,7 +55,7 @@ For example, you can use that functionality to pass an Date: Sun, 29 Mar 2020 22:06:02 +0200 Subject: [PATCH 036/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 08bcda13ff433..ec660441139d3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +* Fix included example after translations refactor. PR [#1182](https://github.com/tiangolo/fastapi/pull/1182). +* Add docs example for `example` in `Field`. Docs at [Body - Fields: JSON Schema extras](https://fastapi.tiangolo.com/tutorial/body-fields/#json-schema-extras). PR [#1106](https://github.com/tiangolo/fastapi/pull/1106) by [@JohnPaton](https://github.com/JohnPaton). * Fix using recursive models in `response_model`. PR [#1164](https://github.com/tiangolo/fastapi/pull/1164) by [@voegtlel](https://github.com/voegtlel). * Add docs for [Pycharm Debugging](https://fastapi.tiangolo.com/tutorial/debugging/). PR [#1096](https://github.com/tiangolo/fastapi/pull/1096) by [@youngquan](https://github.com/youngquan). * Fix typo in docs. PR [#1148](https://github.com/tiangolo/fastapi/pull/1148) by [@PLNech](https://github.com/PLNech). From e6b3b994be71896c9228be78aba3def8f02ca9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 29 Mar 2020 22:08:54 +0200 Subject: [PATCH 037/153] :bookmark: Release version 0.53.1 --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ec660441139d3..1abf560339260 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +## 0.53.1 + * Fix included example after translations refactor. PR [#1182](https://github.com/tiangolo/fastapi/pull/1182). * Add docs example for `example` in `Field`. Docs at [Body - Fields: JSON Schema extras](https://fastapi.tiangolo.com/tutorial/body-fields/#json-schema-extras). PR [#1106](https://github.com/tiangolo/fastapi/pull/1106) by [@JohnPaton](https://github.com/JohnPaton). * Fix using recursive models in `response_model`. PR [#1164](https://github.com/tiangolo/fastapi/pull/1164) by [@voegtlel](https://github.com/voegtlel). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 0a6c7aa2dbf7d..68517b952a5c1 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.53.0" +__version__ = "0.53.1" from starlette import status From 544afaff97c7abadaa9b2bed2264638a79104b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Mar 2020 11:58:48 +0200 Subject: [PATCH 038/153] :memo: Add docs for adding example to schema (#1185) --- docs/en/docs/tutorial/body-fields.md | 32 +--------- docs/en/docs/tutorial/schema-extra-example.md | 58 +++++++++++++++++++ docs/en/mkdocs.yml | 1 + docs_src/schema_extra_example/tutorial001.py | 27 +++++++++ .../tutorial002.py} | 0 .../tutorial003.py} | 0 6 files changed, 89 insertions(+), 29 deletions(-) create mode 100644 docs/en/docs/tutorial/schema-extra-example.md create mode 100644 docs_src/schema_extra_example/tutorial001.py rename docs_src/{body_fields/tutorial003.py => schema_extra_example/tutorial002.py} (100%) rename docs_src/{body_fields/tutorial002.py => schema_extra_example/tutorial003.py} (100%) diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index 8792abee718dc..28a81e41328bd 100644 --- a/docs/en/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -35,37 +35,11 @@ You can then use `Field` with model attributes: !!! tip Notice how each model's attribute with a type, default value and `Field` has the same structure as a *path operation function's* parameter, with `Field` instead of `Path`, `Query` and `Body`. -## JSON Schema extras +## Add extra information -In `Field`, `Path`, `Query`, `Body` and others you'll see later, you can declare extra parameters apart from those described before. +You can declare extra information in `Field`, `Query`, `Body`, etc. And it will be included in the generated JSON Schema. -Those parameters will be added as-is to the output JSON Schema. - -If you know JSON Schema and want to add extra information apart from what we have discussed here, you can pass that as extra keyword arguments. - -!!! warning - Have in mind that extra parameters passed won't add any validation, only annotation, for documentation purposes. - -For example, you can use that functionality to pass an example for a body request: - -```Python hl_lines="20 21 22 23 24 25" -{!../../../docs_src/body_fields/tutorial002.py!} -``` - -Alternately, you can provide these extras on a per-field basis by using additional keyword arguments to `Field`: - -```Python hl_lines="2 8 9 10 11" -{!../../../docs_src/body_fields/tutorial003.py!} -``` - -Either way, in the `/docs` it would look like this: - - - -!!! note "Technical Details" - JSON Schema defines a field `examples` in the most recent versions, but OpenAPI is based on an older version of JSON Schema that didn't have `examples`. - - So, OpenAPI defines its own `example` for the same purpose (as `example`, not `examples`), and that's what is used by the docs UI (using Swagger UI). +You will learn more about it later to declare examples examples. ## Recap diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md new file mode 100644 index 0000000000000..70c3d184669b4 --- /dev/null +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -0,0 +1,58 @@ +# Schema Extra - Example + +You can define extra information to go in JSON Schema. + +A common use case is to add an `example` that will be shown in the docs. + +There are several ways you can declare extra JSON Schema information. + +## Pydantic `schema_extra` + +You can declare an example for a Pydantic model using `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization: + +```Python hl_lines="13 14 15 16 17 18 19 20 21" +{!../../../docs_src/schema_extra_example/tutorial001.py!} +``` + +That extra info will be added as-is to the output JSON Schema. + +## `Field` additional arguments + +In `Field`, `Path`, `Query`, `Body` and others you'll see later, you can also declare extra info for the JSON Schema by passing any other arbitrary arguments to the function, for example, to add an `example`: + +```Python hl_lines="2 8 9 10 11" +{!../../../docs_src/schema_extra_example/tutorial002.py!} +``` + +!!! warning + Have in mind that those extra arguments passed won't add any validation, only annotation, for documentation purposes. + +## `Body` additional arguments + +The same way you can pass extra info to `Field`, you can do the same with `Path`, `Query`, `Body`, etc. + +For example, you can pass an `example` for a body request to `Body`: + +```Python hl_lines="20 21 22 23 24 25" +{!../../../docs_src/schema_extra_example/tutorial003.py!} +``` + +## Example in the docs UI + +With any of the methods above it would look like this in the `/docs`: + + + +## Technical Details + +About `example` vs `examples`... + +JSON Schema defines a field `examples` in the most recent versions, but OpenAPI is based on an older version of JSON Schema that didn't have `examples`. + +So, OpenAPI defined its own `example` for the same purpose (as `example`, not `examples`), and that's what is used by the docs UI (using Swagger UI). + +So, although `example` is not part of JSON Schema, it is part of OpenAPI, and that's what will be used by the docs UI. + +## Other info + +The same way, you could add your own custom extra info that would be added to the JSON Schema for each model, for example to customize a frontend user interface, etc. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 6fbaf960cdab3..db78ee2a7f2bd 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -33,6 +33,7 @@ nav: - tutorial/body-multiple-params.md - tutorial/body-fields.md - tutorial/body-nested-models.md + - tutorial/schema-extra-example.md - tutorial/extra-data-types.md - tutorial/cookie-params.md - tutorial/header-params.md diff --git a/docs_src/schema_extra_example/tutorial001.py b/docs_src/schema_extra_example/tutorial001.py new file mode 100644 index 0000000000000..cd4d45f51d4d2 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial001.py @@ -0,0 +1,27 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str = None + price: float + tax: float = None + + class Config: + schema_extra = { + "example": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + } + + +@app.put("/items/{item_id}") +async def update_item(*, item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_fields/tutorial003.py b/docs_src/schema_extra_example/tutorial002.py similarity index 100% rename from docs_src/body_fields/tutorial003.py rename to docs_src/schema_extra_example/tutorial002.py diff --git a/docs_src/body_fields/tutorial002.py b/docs_src/schema_extra_example/tutorial003.py similarity index 100% rename from docs_src/body_fields/tutorial002.py rename to docs_src/schema_extra_example/tutorial003.py From 618be44023388e85843c0cf09656cea0141e301c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Mar 2020 12:00:55 +0200 Subject: [PATCH 039/153] :wrench: Add .env to git, to simplify VS Code development --- .env | 1 + 1 file changed, 1 insertion(+) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000000000..7032423628aa4 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PYTHONPATH=./docs_src From 1d99681fd4e274b78b4ee604f11c3ec8d0d7b0ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Mar 2020 12:01:30 +0200 Subject: [PATCH 040/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1abf560339260..cbea94dc05d19 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +* Add docs for [Schema Extra - Example](https://fastapi.tiangolo.com/tutorial/schema-extra-example/). PR [#1185](https://github.com/tiangolo/fastapi/pull/1185). + ## 0.53.1 * Fix included example after translations refactor. PR [#1182](https://github.com/tiangolo/fastapi/pull/1182). From eb6be1d7251c5e3d88d85f42af75000371886701 Mon Sep 17 00:00:00 2001 From: Ikkyu <31848542+RunningIkkyu@users.noreply.github.com> Date: Tue, 31 Mar 2020 02:14:58 +0800 Subject: [PATCH 041/153] :speech_balloon: Add new language of docs: zh (#1187) --- docs/en/mkdocs.yml | 1 + docs/es/mkdocs.yml | 1 + docs/zh/docs/index.md | 441 ++++++++++++++++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 61 ++++++ 4 files changed, 504 insertions(+) create mode 100644 docs/zh/docs/index.md create mode 100644 docs/zh/mkdocs.yml diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index db78ee2a7f2bd..ace95e47dd14d 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -20,6 +20,7 @@ nav: - Languages: - en: / - es: /es/ + - zh: /zh/ - features.md - python-types.md - Tutorial - User Guide: diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 8fb7cf46949f3..39273b3e16dac 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -20,6 +20,7 @@ nav: - Languages: - en: / - es: /es/ + - zh: /zh/ markdown_extensions: - toc: permalink: true diff --git a/docs/zh/docs/index.md b/docs/zh/docs/index.md new file mode 100644 index 0000000000000..138d8bd607ab7 --- /dev/null +++ b/docs/zh/docs/index.md @@ -0,0 +1,441 @@ + +{!../../../docs/missing-translation.md!} + + +

    + FastAPI +

    +

    + FastAPI framework, high performance, easy to learn, fast to code, ready for production +

    +

    + + Build Status + + + Coverage + + + Package version + + + Join the chat at https://gitter.im/tiangolo/fastapi + +

    + +--- + +**Documentation**: https://fastapi.tiangolo.com + +**Source Code**: https://github.com/tiangolo/fastapi + +--- + +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. + +The key features are: + +* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). + +* **Fast to code**: Increase the speed to develop features by about 200% to 300% *. +* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * +* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. +* **Easy**: Designed to be easy to use and learn. Less time reading docs. +* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. +* **Robust**: Get production-ready code. With automatic interactive documentation. +* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. + +* estimation based on tests on an internal development team, building production applications. + +## Opinions + +"*[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products.*" + +
    Kabir Khan - Microsoft (ref)
    + +--- + +"*I’m over the moon excited about **FastAPI**. It’s so fun!*" + +
    Brian Okken - Python Bytes podcast host (ref)
    + +--- + +"*Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that.*" + +
    Timothy Crosley - Hug creator (ref)
    + +--- + +"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*" + +"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*" + +
    Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
    + +--- + +"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*" + +
    Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
    + +--- + +## **Typer**, the FastAPI of CLIs + + + +If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. + +**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 + +## Requirements + +Python 3.6+ + +FastAPI stands on the shoulders of giants: + +* Starlette for the web parts. +* Pydantic for the data parts. + +## Installation + +
    + +```console +$ pip install fastapi + +---> 100% +``` + +
    + +You will also need an ASGI server, for production such as Uvicorn or Hypercorn. + +
    + +```console +$ pip install uvicorn + +---> 100% +``` + +
    + +## Example + +### Create it + +* Create a file `main.py` 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} +``` + +
    +Or use async def... + +If your code uses `async` / `await`, use `async def`: + +```Python hl_lines="7 12" +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: str = None): + return {"item_id": item_id, "q": q} +``` + +**Note**: + +If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. + +
    + +### Run it + +Run the server with: + +
    + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
    + +
    +About the command uvicorn main:app --reload... + +The command `uvicorn main:app` refers to: + +* `main`: the file `main.py` (the Python "module"). +* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +* `--reload`: make the server restart after code changes. Only do this for development. + +
    + +### Check it + +Open your browser at http://127.0.0.1:8000/items/5?q=somequery. + +You will see the JSON response as: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +You already created an API that: + +* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. +* Both _paths_ take `GET` operations (also known as HTTP _methods_). +* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. +* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. + +### Interactive API docs + +Now go to http://127.0.0.1:8000/docs. + +You will see the automatic interactive API documentation (provided by Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternative API docs + +And now, go to http://127.0.0.1:8000/redoc. + +You will see the alternative automatic documentation (provided by ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Example upgrade + +Now modify the file `main.py` to receive a body from a `PUT` request. + +Declare the body using standard Python types, thanks to Pydantic. + +```Python hl_lines="2 7 8 9 10 23 24 25" +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: bool = None + + +@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} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +The server should reload automatically (because you added `--reload` to the `uvicorn` command above). + +### Interactive API docs upgrade + +Now go to http://127.0.0.1:8000/docs. + +* The interactive API documentation will be automatically updated, including the new body: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternative API docs upgrade + +And now, go to http://127.0.0.1:8000/redoc. + +* The alternative documentation will also reflect the new query parameter and body: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Recap + +In summary, you declare **once** the types of parameters, body, etc. as function parameters. + +You do that with standard modern Python types. + +You don't have to learn a new syntax, the methods or classes of a specific library, etc. + +Just standard **Python 3.6+**. + +For example, for an `int`: + +```Python +item_id: int +``` + +or for a more complex `Item` model: + +```Python +item: Item +``` + +...and with that single declaration you get: + +* Editor support, including: + * Completion. + * Type checks. +* Validation of data: + * Automatic and clear errors when the data is invalid. + * Validation even for deeply nested JSON objects. +* Conversion of input data: coming from the network to Python data and types. Reading from: + * JSON. + * Path parameters. + * Query parameters. + * Cookies. + * Headers. + * Forms. + * Files. +* Conversion of output data: converting from Python data and types to network data (as JSON): + * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` objects. + * `UUID` objects. + * Database models. + * ...and many more. +* Automatic interactive API documentation, including 2 alternative user interfaces: + * Swagger UI. + * ReDoc. + +--- + +Coming back to the previous code example, **FastAPI** will: + +* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. +* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. + * If it is not, the client will see a useful, clear error. +* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. + * As the `q` parameter is declared with `= None`, it is optional. + * Without the `None` it would be required (as is the body in the case with `PUT`). +* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: + * Check that it has a required attribute `name` that should be a `str`. + * Check that it has a required attribute `price` that has to be a `float`. + * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. + * All this would also work for deeply nested JSON objects. +* Convert from and to JSON automatically. +* Document everything with OpenAPI, that can be used by: + * Interactive documentation systems. + * Automatic client code generation systems, for many languages. +* Provide 2 interactive documentation web interfaces directly. + +--- + +We just scratched the surface, but you already get the idea of how it all works. + +Try changing the line with: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...from: + +```Python + ... "item_name": item.name ... +``` + +...to: + +```Python + ... "item_price": item.price ... +``` + +...and see how your editor will auto-complete the attributes and know their types: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +For a more complete example including more features, see the Tutorial - User Guide. + +**Spoiler alert**: the tutorial - user guide includes: + +* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. +* How to set **validation constraints** as `maximum_length` or `regex`. +* A very powerful and easy to use **Dependency Injection** system. +* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. +* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). +* Many extra features (thanks to Starlette) as: + * **WebSockets** + * **GraphQL** + * extremely easy tests based on `requests` and `pytest` + * **CORS** + * **Cookie Sessions** + * ...and more. + +## Performance + +Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) + +To understand more about it, see the section Benchmarks. + +## Optional Dependencies + +Used by Pydantic: + +* ujson - for faster JSON "parsing". +* email_validator - for email validation. + +Used by Starlette: + +* requests - Required if you want to use the `TestClient`. +* aiofiles - Required if you want to use `FileResponse` or `StaticFiles`. +* jinja2 - Required if you want to use the default template configuration. +* python-multipart - Required if you want to support form "parsing", with `request.form()`. +* itsdangerous - Required for `SessionMiddleware` support. +* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). +* graphene - Required for `GraphQLApp` support. +* ujson - Required if you want to use `UJSONResponse`. + +Used by FastAPI / Starlette: + +* uvicorn - for the server that loads and serves your application. +* orjson - Required if you want to use `ORJSONResponse`. + +You can install all of these with `pip install fastapi[all]`. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml new file mode 100644 index 0000000000000..dfd86b15d10f0 --- /dev/null +++ b/docs/zh/mkdocs.yml @@ -0,0 +1,61 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/zh/ +theme: + name: material + palette: + primary: teal + accent: amber + logo: https://fastapi.tiangolo.com/img/icon-white.svg + favicon: https://fastapi.tiangolo.com/img/favicon.png + language: zh +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +google_analytics: +- UA-133183413-1 +- auto +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ + - zh: /zh/ +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- markdown_include.include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_div_format '' +extra: + social: + - type: github + link: https://github.com/tiangolo/typer + - type: twitter + link: https://twitter.com/tiangolo + - type: linkedin + link: https://www.linkedin.com/in/tiangolo + - type: rss + link: https://dev.to/tiangolo + - type: medium + link: https://medium.com/@tiangolo + - type: globe + link: https://tiangolo.com +extra_css: +- https://fastapi.tiangolo.com/css/termynal.css +- https://fastapi.tiangolo.com/css/custom.css +extra_javascript: +- https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js +- https://fastapi.tiangolo.com/js/termynal.js +- https://fastapi.tiangolo.com/js/custom.js +- https://fastapi.tiangolo.com/js/chat.js +- https://sidecar.gitter.im/dist/sidecar.v1.js From 4f88a5fddbd8cecf04e0ee445261713b16e01dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Mar 2020 20:26:07 +0200 Subject: [PATCH 042/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index cbea94dc05d19..1e1fdacdb95a2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Start translations for Chinese. PR [#1187](https://github.com/tiangolo/fastapi/pull/1187) by [@RunningIkkyu](https://github.com/RunningIkkyu). * Add docs for [Schema Extra - Example](https://fastapi.tiangolo.com/tutorial/schema-extra-example/). PR [#1185](https://github.com/tiangolo/fastapi/pull/1185). ## 0.53.1 From 06eaa32bf0a7e3272d5175c26645fe0845286180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Mar 2020 20:41:50 +0200 Subject: [PATCH 043/153] :wrench: Update docs script to make sure languages are always sorted (#1189) --- scripts/docs.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/docs.py b/scripts/docs.py index 345135e91b5d0..186a3e4f89121 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -22,6 +22,10 @@ docs_path = Path("docs") +def get_lang_paths(): + return sorted(docs_path.iterdir()) + + def lang_callback(lang: Optional[str]): if lang is None: return @@ -34,7 +38,7 @@ def lang_callback(lang: Optional[str]): def complete_existing_lang(incomplete: str): lang_path: Path - for lang_path in docs_path.iterdir(): + for lang_path in get_lang_paths(): if lang_path.is_dir() and lang_path.name.startswith(incomplete): yield lang_path.name @@ -198,7 +202,7 @@ def build_all(): typer.echo(f"Building docs for: en") mkdocs.commands.build.build(mkdocs.config.load_config(site_dir=str(site_path))) os.chdir(current_dir) - for lang in docs_path.iterdir(): + for lang in get_lang_paths(): if lang == en_build_path or not lang.is_dir(): continue build_lang(lang.name) @@ -220,7 +224,7 @@ def update_languages( mkdocs.yml files (for all the languages). """ if lang is None: - for lang_path in docs_path.iterdir(): + for lang_path in get_lang_paths(): if lang_path.is_dir(): typer.echo(f"Updating {lang_path.name}") update_config(lang_path.name) @@ -278,7 +282,7 @@ def update_config(lang: str): config_path = lang_path / mkdocs_name config: dict = mkdocs.utils.yaml_load(config_path.read_text(encoding="utf-8")) languages = [{"en": "/"}] - for lang in docs_path.iterdir(): + for lang in get_lang_paths(): if lang.name == "en" or not lang.is_dir(): continue name = lang.name From 210af1fd3dc0f612a08fa02a0cb3f5adb81e5bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Mar 2020 20:42:46 +0200 Subject: [PATCH 044/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1e1fdacdb95a2..02acbf71c6361 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix docs script to ensure languages are always sorted. PR [#1189](https://github.com/tiangolo/fastapi/pull/1189). * Start translations for Chinese. PR [#1187](https://github.com/tiangolo/fastapi/pull/1187) by [@RunningIkkyu](https://github.com/RunningIkkyu). * Add docs for [Schema Extra - Example](https://fastapi.tiangolo.com/tutorial/schema-extra-example/). PR [#1185](https://github.com/tiangolo/fastapi/pull/1185). From 02441ff0313d5b471b662293244c53e712f1243f Mon Sep 17 00:00:00 2001 From: amitlissack Date: Mon, 30 Mar 2020 14:45:05 -0400 Subject: [PATCH 045/153] :bug: Fix dependency overrides in WebSockets (#1122) * add tests to test_ws_router to test dependencies and dependency overrides. * supply dependency_overrides_provider to APIWebSocketRoute upon creation --- fastapi/routing.py | 7 ++++++- tests/test_ws_router.py | 28 +++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index b90935e15ffe7..1ec0b693c87b0 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -498,7 +498,12 @@ def decorator(func: Callable) -> Callable: def add_api_websocket_route( self, path: str, endpoint: Callable, name: str = None ) -> None: - route = APIWebSocketRoute(path, endpoint=endpoint, name=name) + route = APIWebSocketRoute( + path, + endpoint=endpoint, + name=name, + dependency_overrides_provider=self.dependency_overrides_provider, + ) self.routes.append(route) def websocket(self, path: str, name: str = None) -> Callable: diff --git a/tests/test_ws_router.py b/tests/test_ws_router.py index fd19e650a9ace..dd0456127222a 100644 --- a/tests/test_ws_router.py +++ b/tests/test_ws_router.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, FastAPI, WebSocket +from fastapi import APIRouter, Depends, FastAPI, WebSocket from fastapi.testclient import TestClient router = APIRouter() @@ -34,6 +34,19 @@ async def routerindex(websocket: WebSocket): await websocket.close() +async def ws_dependency(): + return "Socket Dependency" + + +@router.websocket("/router-ws-depends/") +async def router_ws_decorator_depends( + websocket: WebSocket, data=Depends(ws_dependency) +): + await websocket.accept() + await websocket.send_text(data) + await websocket.close() + + app.include_router(router) app.include_router(prefix_router, prefix="/prefix") @@ -64,3 +77,16 @@ def test_router2(): with client.websocket_connect("/router2") as websocket: data = websocket.receive_text() assert data == "Hello, router!" + + +def test_router_ws_depends(): + client = TestClient(app) + with client.websocket_connect("/router-ws-depends/") as websocket: + assert websocket.receive_text() == "Socket Dependency" + + +def test_router_ws_depends_with_override(): + client = TestClient(app) + app.dependency_overrides[ws_dependency] = lambda: "Override" + with client.websocket_connect("/router-ws-depends/") as websocket: + assert websocket.receive_text() == "Override" From 042c697b6b544b6ce561280228dc3bab40c2f73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Mar 2020 20:49:24 +0200 Subject: [PATCH 046/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 02acbf71c6361..03bccb36e809f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix dependency overrides in WebSocket testing. PR [#1122](https://github.com/tiangolo/fastapi/pull/1122) by [@amitlissack](https://github.com/amitlissack). * Fix docs script to ensure languages are always sorted. PR [#1189](https://github.com/tiangolo/fastapi/pull/1189). * Start translations for Chinese. PR [#1187](https://github.com/tiangolo/fastapi/pull/1187) by [@RunningIkkyu](https://github.com/RunningIkkyu). * Add docs for [Schema Extra - Example](https://fastapi.tiangolo.com/tutorial/schema-extra-example/). PR [#1185](https://github.com/tiangolo/fastapi/pull/1185). From 90afc72e64b0e294f21bb823bdafb62d3352d5d0 Mon Sep 17 00:00:00 2001 From: Toan Vuong Date: Mon, 30 Mar 2020 12:44:43 -0700 Subject: [PATCH 047/153] :bug: Fix automatic embedding with dependencies and sub-dependencies (#1079) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Handle automatic embedding with Depends * :bug: Fix body embeds for sub-dependencies and simplify implementation * :white_check_mark: Add/update tests for body embeds in dependencies * :construction_worker: Trigger Travis Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 8 +- tests/test_dependency_duplicates.py | 232 ++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 tests/test_dependency_duplicates.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 543479be8814b..43ab4a0985bc3 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -704,8 +704,14 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: first_param = flat_dependant.body_params[0] field_info = get_field_info(first_param) embed = getattr(field_info, "embed", None) - if len(flat_dependant.body_params) == 1 and not embed: + body_param_names_set = set([param.name for param in flat_dependant.body_params]) + if len(body_param_names_set) == 1 and not embed: return get_schema_compatible_field(field=first_param) + # If one field requires to embed, all have to be embedded + # in case a sub-dependency is evaluated with a single unique body field + # That is combined (embedded) with other body fields + for param in flat_dependant.body_params: + setattr(get_field_info(param), "embed", True) model_name = "Body_" + name BodyModel = create_model(model_name) for f in flat_dependant.body_params: diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py new file mode 100644 index 0000000000000..0462b43c848cd --- /dev/null +++ b/tests/test_dependency_duplicates.py @@ -0,0 +1,232 @@ +from typing import List + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + +client = TestClient(app) + + +class Item(BaseModel): + data: str + + +def duplicate_dependency(item: Item): + return item + + +def dependency(item2: Item): + return item2 + + +def sub_duplicate_dependency( + item: Item, sub_item: Item = Depends(duplicate_dependency) +): + return [item, sub_item] + + +@app.post("/with-duplicates") +async def with_duplicates(item: Item, item2: Item = Depends(duplicate_dependency)): + return [item, item2] + + +@app.post("/no-duplicates") +async def no_duplicates(item: Item, item2: Item = Depends(dependency)): + return [item, item2] + + +@app.post("/with-duplicates-sub") +async def no_duplicates_sub( + item: Item, sub_items: List[Item] = Depends(sub_duplicate_dependency) +): + return [item, sub_items] + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/with-duplicates": { + "post": { + "summary": "With Duplicates", + "operationId": "with_duplicates_with_duplicates_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/no-duplicates": { + "post": { + "summary": "No Duplicates", + "operationId": "no_duplicates_no_duplicates_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_no_duplicates_no_duplicates_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/with-duplicates-sub": { + "post": { + "summary": "No Duplicates Sub", + "operationId": "no_duplicates_sub_with_duplicates_sub_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_no_duplicates_no_duplicates_post": { + "title": "Body_no_duplicates_no_duplicates_post", + "required": ["item", "item2"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["data"], + "type": "object", + "properties": {"data": {"title": "Data", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_no_duplicates_invalid(): + response = client.post("/no-duplicates", json={"item": {"data": "myitem"}}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "loc": ["body", "item2"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + + +def test_no_duplicates(): + response = client.post( + "/no-duplicates", + json={"item": {"data": "myitem"}, "item2": {"data": "myitem2"}}, + ) + assert response.status_code == 200 + assert response.json() == [{"data": "myitem"}, {"data": "myitem2"}] + + +def test_duplicates(): + response = client.post("/with-duplicates", json={"data": "myitem"}) + assert response.status_code == 200 + assert response.json() == [{"data": "myitem"}, {"data": "myitem"}] + + +def test_sub_duplicates(): + response = client.post("/with-duplicates-sub", json={"data": "myitem"}) + assert response.status_code == 200 + assert response.json() == [ + {"data": "myitem"}, + [{"data": "myitem"}, {"data": "myitem"}], + ] From b86d130eb6a082cc7e2c27a68a71677dabe115a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Mar 2020 21:47:11 +0200 Subject: [PATCH 048/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 03bccb36e809f..426f3a9928cbc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix automatic embedding of body fields for dependencies and sub-dependencies. Original PR [#1079](https://github.com/tiangolo/fastapi/pull/1079) by [@Toad2186](https://github.com/Toad2186). * Fix dependency overrides in WebSocket testing. PR [#1122](https://github.com/tiangolo/fastapi/pull/1122) by [@amitlissack](https://github.com/amitlissack). * Fix docs script to ensure languages are always sorted. PR [#1189](https://github.com/tiangolo/fastapi/pull/1189). * Start translations for Chinese. PR [#1187](https://github.com/tiangolo/fastapi/pull/1187) by [@RunningIkkyu](https://github.com/RunningIkkyu). From 6e1cd45a46ed4940e37bc3c47d27b9788349ba10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Mar 2020 21:49:50 +0200 Subject: [PATCH 049/153] :bookmark: Release version 0.53.2 --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 426f3a9928cbc..542a36a00d4c5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +## 0.53.2 + * Fix automatic embedding of body fields for dependencies and sub-dependencies. Original PR [#1079](https://github.com/tiangolo/fastapi/pull/1079) by [@Toad2186](https://github.com/Toad2186). * Fix dependency overrides in WebSocket testing. PR [#1122](https://github.com/tiangolo/fastapi/pull/1122) by [@amitlissack](https://github.com/amitlissack). * Fix docs script to ensure languages are always sorted. PR [#1189](https://github.com/tiangolo/fastapi/pull/1189). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 68517b952a5c1..99334962d8351 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.53.1" +__version__ = "0.53.2" from starlette import status From d4d5b21b2e70cdd7e36aedc2e41d7c29b430492b Mon Sep 17 00:00:00 2001 From: alexmitelman Date: Thu, 2 Apr 2020 07:55:20 +0300 Subject: [PATCH 050/153] :memo: Add documentation about settings and env vars (#1118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add doc and example for env var config * Syntax highlight for .env file * Add test for configuration docs * :memo: Update settings docs, add more examples * :white_check_mark: Add tests for settings * :truck: Rename "Application Configuration" to "Metadata and Docs URLs" to disambiguate between that and settings * :fire: Remove replaced example file Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/settings.md | 294 ++++++++++++++++++ .../image01.png | Bin ...plication-configuration.md => metadata.md} | 18 +- docs/en/mkdocs.yml | 3 +- .../tutorial001.py | 0 .../tutorial002.py | 0 .../tutorial003.py | 0 .../settings/app01}/__init__.py | 0 docs_src/settings/app01/config.py | 10 + docs_src/settings/app01/main.py | 14 + docs_src/settings/app02/__init__.py | 0 docs_src/settings/app02/config.py | 10 + docs_src/settings/app02/main.py | 21 ++ docs_src/settings/app02/test_main.py | 23 ++ docs_src/settings/app03/__init__.py | 0 docs_src/settings/app03/config.py | 13 + docs_src/settings/app03/main.py | 21 ++ docs_src/settings/tutorial001.py | 21 ++ tests/test_tutorial/test_metadata/__init__.py | 0 .../test_tutorial001.py | 2 +- tests/test_tutorial/test_settings/__init__.py | 0 .../test_tutorial/test_settings/test_app02.py | 9 + 22 files changed, 448 insertions(+), 11 deletions(-) create mode 100644 docs/en/docs/advanced/settings.md rename docs/en/docs/img/tutorial/{application-configuration => metadata}/image01.png (100%) rename docs/en/docs/tutorial/{application-configuration.md => metadata.md} (66%) rename docs_src/{application_configuration => metadata}/tutorial001.py (100%) rename docs_src/{application_configuration => metadata}/tutorial002.py (100%) rename docs_src/{application_configuration => metadata}/tutorial003.py (100%) rename {tests/test_tutorial/test_application_configuration => docs_src/settings/app01}/__init__.py (100%) create mode 100644 docs_src/settings/app01/config.py create mode 100644 docs_src/settings/app01/main.py create mode 100644 docs_src/settings/app02/__init__.py create mode 100644 docs_src/settings/app02/config.py create mode 100644 docs_src/settings/app02/main.py create mode 100644 docs_src/settings/app02/test_main.py create mode 100644 docs_src/settings/app03/__init__.py create mode 100644 docs_src/settings/app03/config.py create mode 100644 docs_src/settings/app03/main.py create mode 100644 docs_src/settings/tutorial001.py create mode 100644 tests/test_tutorial/test_metadata/__init__.py rename tests/test_tutorial/{test_application_configuration => test_metadata}/test_tutorial001.py (94%) create mode 100644 tests/test_tutorial/test_settings/__init__.py create mode 100644 tests/test_tutorial/test_settings/test_app02.py diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md new file mode 100644 index 0000000000000..2b3555c0cd43b --- /dev/null +++ b/docs/en/docs/advanced/settings.md @@ -0,0 +1,294 @@ +# 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 environment variable (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: + +
    + +```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 +``` + +
    + +Or in PowerShell in Windows: + +
    + +```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 +``` + +
    + +### 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 `os.getenv()` 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: + +
    + +```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 +``` + +
    + +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: + +
    + +```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 +``` + +
    + +!!! tip + You can read more about it at The Twelve-Factor App: Config. + +### 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 be done in code. + +## Pydantic `Settings` + +Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with Pydantic: Settings management. + +### 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 6 7 8 11" +{!../../../docs_src/settings/tutorial001.py!} +``` + +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 19 20" +{!../../../docs_src/settings/tutorial001.py!} +``` + +### Run the server + +Then you would run the server passing the configurations as environment variables, for example you could set an `ADMIN_EMAIL` and `APP_NAME` with: + +
    + +```console +$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" uvicorn main:app + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
    + +!!! 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](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 12 13" +{!../../../docs_src/settings/app01/main.py!} +``` + +!!! tip + You would also need a file `__init__.py` as you saw on [Bigger Applications - Multiple Files](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()`. + +Instead we declare its type as `Settings`, but the value as `None`. + +### The main app file + +Now we create a dependency that returns the `settings` object if we already created it. + +Otherwise we create a new one, assign it to `config.settings` and then return it from the dependency. + +```Python hl_lines="8 9 10 11 12" +{!../../../docs_src/settings/app02/main.py!} +``` + +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 19 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 Pydantic Settings: Dotenv (.env) support. + +!!! 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 Pydantic Model Config + +### Creating the settings object + +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, instead of reading it for each request. + +Because of that, in the dependency function, we first check if we already have a `settings` object, and create a new one (that could read from disk) only if it's still `None`, so, it would happen only the first time: + +```Python hl_lines="9 10 11 12" +{!../../../docs_src/settings/app03/main.py!} +``` + +## 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. +* Saving the settings in a variable lets you avoid reading the dotenv file again and again for each request. diff --git a/docs/en/docs/img/tutorial/application-configuration/image01.png b/docs/en/docs/img/tutorial/metadata/image01.png similarity index 100% rename from docs/en/docs/img/tutorial/application-configuration/image01.png rename to docs/en/docs/img/tutorial/metadata/image01.png diff --git a/docs/en/docs/tutorial/application-configuration.md b/docs/en/docs/tutorial/metadata.md similarity index 66% rename from docs/en/docs/tutorial/application-configuration.md rename to docs/en/docs/tutorial/metadata.md index 978273cc1a2ea..59e3f5b5ab55d 100644 --- a/docs/en/docs/tutorial/application-configuration.md +++ b/docs/en/docs/tutorial/metadata.md @@ -1,25 +1,25 @@ -# Application Configuration +# Metadata and Docs URLs -There are several things that you can configure in your FastAPI application. +You can customize several metadata configurations in your **FastAPI** application. ## Title, description, and version You can set the: -* Title: used as your API's title/name, in OpenAPI and the automatic API docs UIs. -* Description: the description of your API, in OpenAPI and the automatic API docs UIs. -* Version: the version of your API, e.g. `v2` or `2.5.0`. +* **Title**: used as your API's title/name, in OpenAPI and the automatic API docs UIs. +* **Description**: the description of your API, in OpenAPI and the automatic API docs UIs. +* **Version**: the version of your API, e.g. `v2` or `2.5.0`. * Useful for example if you had a previous version of the application, also using OpenAPI. To set them, use the parameters `title`, `description`, and `version`: ```Python hl_lines="4 5 6" -{!../../../docs_src/application_configuration/tutorial001.py!} +{!../../../docs_src/metadata/tutorial001.py!} ``` With this configuration, the automatic API docs would look like: - + ## OpenAPI URL @@ -30,7 +30,7 @@ But you can configure it with the parameter `openapi_url`. For example, to set it to be served at `/api/v1/openapi.json`: ```Python hl_lines="3" -{!../../../docs_src/application_configuration/tutorial002.py!} +{!../../../docs_src/metadata/tutorial002.py!} ``` If you want to disable the OpenAPI schema completely you can set `openapi_url=None`. @@ -49,5 +49,5 @@ You can configure the two documentation user interfaces included: For example, to set Swagger UI to be served at `/documentation` and disable ReDoc: ```Python hl_lines="3" -{!../../../docs_src/application_configuration/tutorial003.py!} +{!../../../docs_src/metadata/tutorial003.py!} ``` diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index ace95e47dd14d..107e1e046b567 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -65,7 +65,7 @@ nav: - tutorial/sql-databases.md - tutorial/bigger-applications.md - tutorial/background-tasks.md - - tutorial/application-configuration.md + - tutorial/metadata.md - tutorial/static-files.md - tutorial/testing.md - tutorial/debugging.md @@ -98,6 +98,7 @@ nav: - advanced/testing-websockets.md - advanced/testing-events.md - advanced/testing-dependencies.md + - advanced/settings.md - advanced/extending-openapi.md - advanced/openapi-callbacks.md - advanced/wsgi.md diff --git a/docs_src/application_configuration/tutorial001.py b/docs_src/metadata/tutorial001.py similarity index 100% rename from docs_src/application_configuration/tutorial001.py rename to docs_src/metadata/tutorial001.py diff --git a/docs_src/application_configuration/tutorial002.py b/docs_src/metadata/tutorial002.py similarity index 100% rename from docs_src/application_configuration/tutorial002.py rename to docs_src/metadata/tutorial002.py diff --git a/docs_src/application_configuration/tutorial003.py b/docs_src/metadata/tutorial003.py similarity index 100% rename from docs_src/application_configuration/tutorial003.py rename to docs_src/metadata/tutorial003.py diff --git a/tests/test_tutorial/test_application_configuration/__init__.py b/docs_src/settings/app01/__init__.py similarity index 100% rename from tests/test_tutorial/test_application_configuration/__init__.py rename to docs_src/settings/app01/__init__.py diff --git a/docs_src/settings/app01/config.py b/docs_src/settings/app01/config.py new file mode 100644 index 0000000000000..defede9db401e --- /dev/null +++ b/docs_src/settings/app01/config.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + +settings = Settings() diff --git a/docs_src/settings/app01/main.py b/docs_src/settings/app01/main.py new file mode 100644 index 0000000000000..53503797ecb31 --- /dev/null +++ b/docs_src/settings/app01/main.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI + +from . import config + +app = FastAPI() + + +@app.get("/info") +async def info(): + return { + "app_name": config.settings.app_name, + "admin_email": config.settings.admin_email, + "items_per_user": config.settings.items_per_user, + } diff --git a/docs_src/settings/app02/__init__.py b/docs_src/settings/app02/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs_src/settings/app02/config.py b/docs_src/settings/app02/config.py new file mode 100644 index 0000000000000..492a58f2e340e --- /dev/null +++ b/docs_src/settings/app02/config.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + +settings: Settings = None diff --git a/docs_src/settings/app02/main.py b/docs_src/settings/app02/main.py new file mode 100644 index 0000000000000..ba91d46042161 --- /dev/null +++ b/docs_src/settings/app02/main.py @@ -0,0 +1,21 @@ +from fastapi import Depends, FastAPI + +from . import config + +app = FastAPI() + + +def get_settings(): + if config.settings: + return config.settings + config.settings = config.Settings() + return config.settings + + +@app.get("/info") +async def info(settings: config.Settings = Depends(get_settings)): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/app02/test_main.py b/docs_src/settings/app02/test_main.py new file mode 100644 index 0000000000000..44d9f837963d8 --- /dev/null +++ b/docs_src/settings/app02/test_main.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from . import config, main + +client = TestClient(main.app) + + +def get_settings_override(): + return config.Settings(admin_email="testing_admin@example.com") + + +main.app.dependency_overrides[main.get_settings] = get_settings_override + + +def test_app(): + + response = client.get("/info") + data = response.json() + assert data == { + "app_name": "Awesome API", + "admin_email": "testing_admin@example.com", + "items_per_user": 50, + } diff --git a/docs_src/settings/app03/__init__.py b/docs_src/settings/app03/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs_src/settings/app03/config.py b/docs_src/settings/app03/config.py new file mode 100644 index 0000000000000..425a22d8144b2 --- /dev/null +++ b/docs_src/settings/app03/config.py @@ -0,0 +1,13 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" + + +settings: Settings = None diff --git a/docs_src/settings/app03/main.py b/docs_src/settings/app03/main.py new file mode 100644 index 0000000000000..ba91d46042161 --- /dev/null +++ b/docs_src/settings/app03/main.py @@ -0,0 +1,21 @@ +from fastapi import Depends, FastAPI + +from . import config + +app = FastAPI() + + +def get_settings(): + if config.settings: + return config.settings + config.settings = config.Settings() + return config.settings + + +@app.get("/info") +async def info(settings: config.Settings = Depends(get_settings)): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/tutorial001.py b/docs_src/settings/tutorial001.py new file mode 100644 index 0000000000000..0cfd1b6632f0b --- /dev/null +++ b/docs_src/settings/tutorial001.py @@ -0,0 +1,21 @@ +from fastapi import FastAPI +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + +settings = Settings() +app = FastAPI() + + +@app.get("/info") +async def info(): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/tests/test_tutorial/test_metadata/__init__.py b/tests/test_tutorial/test_metadata/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/test_tutorial/test_application_configuration/test_tutorial001.py b/tests/test_tutorial/test_metadata/test_tutorial001.py similarity index 94% rename from tests/test_tutorial/test_application_configuration/test_tutorial001.py rename to tests/test_tutorial/test_metadata/test_tutorial001.py index 40c545e0357ec..0cef510b30cb9 100644 --- a/tests/test_tutorial/test_application_configuration/test_tutorial001.py +++ b/tests/test_tutorial/test_metadata/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from application_configuration.tutorial001 import app +from metadata.tutorial001 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_settings/__init__.py b/tests/test_tutorial/test_settings/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/test_tutorial/test_settings/test_app02.py b/tests/test_tutorial/test_settings/test_app02.py new file mode 100644 index 0000000000000..3f100a4f4d874 --- /dev/null +++ b/tests/test_tutorial/test_settings/test_app02.py @@ -0,0 +1,9 @@ +from fastapi.testclient import TestClient + +from settings.app02 import main, test_main + +client = TestClient(main.app) + + +def test_setting_override(): + test_main.test_app() From 3b7e4e05443587da4490a863e5be426a9d6dd1af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 2 Apr 2020 06:59:19 +0200 Subject: [PATCH 051/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 542a36a00d4c5..3ee628684e96b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +* Add docs about [Settings and Environment Variables](https://fastapi.tiangolo.com/advanced/settings/). Initial PR [1118](https://github.com/tiangolo/fastapi/pull/1118) by [@alexmitelman](https://github.com/alexmitelman). + ## 0.53.2 * Fix automatic embedding of body fields for dependencies and sub-dependencies. Original PR [#1079](https://github.com/tiangolo/fastapi/pull/1079) by [@Toad2186](https://github.com/Toad2186). From 3a0c22ce7de56e92bfdda5ad0395cc635e20af7c Mon Sep 17 00:00:00 2001 From: Camila Gutierrez Date: Thu, 2 Apr 2020 07:00:50 +0200 Subject: [PATCH 052/153] :globe_with_meridians: Translate index to Spanish (#1201) --- docs/es/docs/index.md | 289 +++++++++++++++++++++--------------------- 1 file changed, 144 insertions(+), 145 deletions(-) diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 7a12add10a7fe..28a5d40679878 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -2,7 +2,7 @@ FastAPI

    - FastAPI framework, high performance, easy to learn, fast to code, ready for production + FastAPI framework, alto desempeño, fácil de aprender, rápido de programar, listo para producción

    @@ -21,29 +21,28 @@ --- -**Documentation**: https://fastapi.tiangolo.com +**Documentación**: https://fastapi.tiangolo.com -**Source Code**: https://github.com/tiangolo/fastapi +**Código Fuente**: https://github.com/tiangolo/fastapi --- +FastAPI es un web framework moderno y rápido (de alto rendimiento) para construir APIs con Python 3.6+ basado en las anotaciones de tipos estándar de Python. -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. +Sus características principales son: -The key features are: +* **Rapidez**: Alto rendimiento, a la par con **NodeJS** y **Go** (gracias a Starlette y Pydantic). [Uno de los frameworks de Python más rápidos](#rendimiento). -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). +* **Rápido de programar**: Incrementa la velocidad de desarrollo entre 200% y 300% *. +* **Menos errores**: Reduce los errores humanos (de programador) aproximadamente un 40% *. +* **Intuitivo**: Gran soporte en los editores con auto completado en todas partes. Gasta menos tiempo debugging. +* **Fácil**: Está diseñado para ser fácil de usar y aprender. Gastando menos tiempo leyendo documentación. +* **Corto**: Minimiza la duplicación de código. Múltiples funcionalidades con cada declaración de parámetros. Menos errores. +* **Robusto**: Crea código listo para producción con documentación automática interactiva. +* **Basado en estándares**: Basado y totalmente compatible con los estándares abiertos para APIs: OpenAPI (conocido previamente como Swagger) y JSON Schema. -* **Fast to code**: Increase the speed to develop features by about 200% to 300% *. -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. +* Esta estimación está basada en pruebas con un equipo de desarrollo interno contruyendo aplicaciones listas para producción. -* estimation based on tests on an internal development team, building production applications. - -## Opinions +## Opiniones "*[...] 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.*" @@ -77,24 +76,24 @@ The key features are: --- -## **Typer**, the FastAPI of CLIs +## **Typer**, el FastAPI de las CLIs -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. +Si estás construyendo un app de CLI para ser usada en la terminal en vez de una API web, fíjate en **Typer**. -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 +**Typer** es el hermano menor de FastAPI. La intención es que sea el **FastAPI de las CLIs**. ⌨️ 🚀 -## Requirements +## Requisitos Python 3.6+ -FastAPI stands on the shoulders of giants: +FastAPI está sobre los hombros de gigantes: -* Starlette for the web parts. -* Pydantic for the data parts. +* Starlette para las partes web. +* Pydantic para las partes de datos. -## Installation +## Instalación

    @@ -106,7 +105,7 @@ $ pip install fastapi
    -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. +También vas a necesitar un servidor ASGI para producción cómo Uvicorn o Hypercorn.
    @@ -118,11 +117,11 @@ $ pip install uvicorn
    -## Example +## Ejemplo -### Create it +### Créalo -* Create a file `main.py` with: +* Crea un archivo `main.py` con: ```Python from fastapi import FastAPI @@ -141,9 +140,9 @@ def read_item(item_id: int, q: str = None): ```
    -Or use async def... +O usa async def... -If your code uses `async` / `await`, use `async def`: +Si tu código usa `async` / `await`, usa `async def`: ```Python hl_lines="7 12" from fastapi import FastAPI @@ -161,15 +160,15 @@ async def read_item(item_id: int, q: str = None): return {"item_id": item_id, "q": q} ``` -**Note**: +**Nota**: -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. +Si no lo sabes, revisa la sección _"¿Con prisa?"_ sobre `async` y `await` en la documentación.
    -### Run it +### Córrelo -Run the server with: +Corre el servidor con:
    @@ -186,54 +185,54 @@ $ uvicorn main:app --reload
    -About the command uvicorn main:app --reload... +Sobre el comando uvicorn main:app --reload... -The command `uvicorn main:app` refers to: +El comando `uvicorn main:app` se refiere a: -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. +* `main`: el archivo `main.py` (el"modulo" de Python). +* `app`: el objeto creado dentro de `main.py` con la línea `app = FastAPI()`. +* `--reload`: hace que el servidor se reinicie después de cambios en el código. Esta opción solo debe ser usada en desarrollo.
    -### Check it +### Revísalo -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. +Abre tu navegador en http://127.0.0.1:8000/items/5?q=somequery. -You will see the JSON response as: +Verás la respuesta de JSON cómo: ```JSON {"item_id": 5, "q": "somequery"} ``` -You already created an API that: +Ya creaste una API que: -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. +* Recibe HTTP requests en los _paths_ `/` y `/items/{item_id}`. +* Ambos _paths_ toman operaciones `GET` (también conocido como HTTP _methods_). +* El _path_ `/items/{item_id}` tiene un _path parameter_ `item_id` que debería ser un `int`. +* El _path_ `/items/{item_id}` tiene un `str` _query parameter_ `q` opcional. -### Interactive API docs +### Documentación interactiva de APIs -Now go to http://127.0.0.1:8000/docs. +Ahora ve a http://127.0.0.1:8000/docs. -You will see the automatic interactive API documentation (provided by Swagger UI): +Verás la documentación automática e interactiva de la API (proveída por Swagger UI): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternative API docs +### Documentación alternativa de la API -And now, go to http://127.0.0.1:8000/redoc. +Ahora, ve a http://127.0.0.1:8000/redoc. -You will see the alternative automatic documentation (provided by ReDoc): +Ahora verás la documentación automática alternativa (proveída por ReDoc): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Example upgrade +## Mejora al ejemplo -Now modify the file `main.py` to receive a body from a `PUT` request. +Ahora modifica el archivo `main.py` para recibir un body del `PUT` request. -Declare the body using standard Python types, thanks to Pydantic. +Declara el body usando las declaraciones de tipo estándares de Python gracias a Pydantic. ```Python hl_lines="2 7 8 9 10 23 24 25" from fastapi import FastAPI @@ -263,175 +262,175 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). +El servidor debería recargar automáticamente (porque añadiste `--reload` al comando `uvicorn` que está más arriba). -### Interactive API docs upgrade +### Mejora a la documentación interactiva de APIs -Now go to http://127.0.0.1:8000/docs. +Ahora ve a http://127.0.0.1:8000/docs. -* The interactive API documentation will be automatically updated, including the new body: +* La documentación interactiva de la API se actualizará automáticamente, incluyendo el nuevo body: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: +* Haz clíck en el botón de "Try it out" que te permite llenar los parámetros e interactuar directamente con la API: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: +* Luego haz clíck en el botón de "Execute". La interfaz de usuario se comunicará con tu API, enviará los parámetros y recibirá los resultados para mostrarlos en pantalla: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Alternative API docs upgrade +### Mejora a la documentación alternativa de la API -And now, go to http://127.0.0.1:8000/redoc. +Ahora, ve a http://127.0.0.1:8000/redoc. -* The alternative documentation will also reflect the new query parameter and body: +* La documentación alternativa también reflejará el nuevo parámetro de query y el body: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Recap +### Resumen -In summary, you declare **once** the types of parameters, body, etc. as function parameters. +En resumen, declaras los tipos de parámetros, body, etc. **una vez** como parámetros de la función. -You do that with standard modern Python types. +Lo haces con tipos modernos estándar de Python. -You don't have to learn a new syntax, the methods or classes of a specific library, etc. +No tienes que aprender una sintáxis nueva, los métodos o clases de una library específica, etc. -Just standard **Python 3.6+**. +Solo **Python 3.6+** estándar. -For example, for an `int`: +Por ejemplo, para un `int`: ```Python item_id: int ``` -or for a more complex `Item` model: +o para un modelo más complejo de `Item`: ```Python item: Item ``` -...and with that single declaration you get: +...y con esa única declaración obtienes: -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: +* Soporte del editor incluyendo: + * Auto completado. + * Anotaciones de tipos. +* Validación de datos: + * Errores automáticos y claros cuándo los datos son inválidos. + * Validación, incluso para objetos JSON profundamente anidados. +* Conversión de datos de input: viniendo de la red a datos y tipos de Python. Leyendo desde: * JSON. * Path parameters. * Query parameters. * Cookies. * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: + * Formularios. + * Archivos. +* Conversión de datos de output: convirtiendo de datos y tipos de Python a datos para la red (como JSON): + * Convertir tipos de Python (`str`, `int`, `float`, `bool`, `list`, etc). + * Objetos `datetime`. + * Objetos `UUID`. + * Modelos de bases de datos. + * ...y muchos más. +* Documentación automática e interactiva incluyendo 2 interfaces de usuario alternativas: * Swagger UI. * ReDoc. --- -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. +Volviendo al ejemplo de código anterior, **FastAPI** va a: + +* Validar que existe un `item_id` en el path para requests usando `GET` y `PUT`. +* Validar que el `item_id` es del tipo `int` para requests de tipo `GET` y `PUT`. + * Si no lo es, el cliente verá un mensaje de error útil y claro. +* Revisar si existe un query parameter opcional llamado `q` (cómo en `http://127.0.0.1:8000/items/foo?q=somequery`) para requests de tipo `GET`. + * Como el parámetro `q` fue declarado con `= None` es opcional. + * Sin el `None` sería obligatorio (cómo lo es el body en el caso con `PUT`). +* Para requests de tipo `PUT` a `/items/{item_id}` leer el body como JSON: + * Revisar si tiene un atributo requerido `name` que debe ser un `str`. + * Revisar si tiene un atributo requerido `price` que debe ser un `float`. + * Revisar si tiene un atributo opcional `is_offer`, que debe ser un `bool`si está presente. + * Todo esto funcionaría para objetos JSON profundamente anidados. +* Convertir de y a JSON automáticamente. +* Documentar todo con OpenAPI que puede ser usado por: + * Sistemas de documentación interactiva. + * Sistemas de generación automática de código de cliente para muchos lenguajes. +* Proveer directamente 2 interfaces de documentación web interactivas. --- -We just scratched the surface, but you already get the idea of how it all works. +Hasta ahora, escasamente vimos lo básico pero ya tienes una idea de cómo funciona. -Try changing the line with: +Intenta cambiando la línea a: ```Python return {"item_name": item.name, "item_id": item_id} ``` -...from: +...de: ```Python ... "item_name": item.name ... ``` -...to: +...a: ```Python ... "item_price": item.price ... ``` -...and see how your editor will auto-complete the attributes and know their types: +... y mira como el editor va a auto-completar los atributos y sabrá sus tipos: -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) +![soporte de editor](https://fastapi.tiangolo.com/img/vscode-completion.png) -For a more complete example including more features, see the Tutorial - User Guide. +Para un ejemplo más completo que incluye más características ve el Tutorial - Guía de Usuario. -**Spoiler alert**: the tutorial - user guide includes: +**Spoiler alert**: el Tutorial - Guía de Usuario incluye: -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: +* Declaración de **parámetros** en otros lugares diferentes cómo los: **headers**, **cookies**, **formularios** y **archivos**. +* Cómo agregar **requisitos de validación** cómo `maximum_length` o `regex`. +* Un sistema de **Dependency Injection** poderoso y fácil de usar. +* Seguridad y autenticación incluyendo soporte para **OAuth2** con **JWT tokens** y **HTTP Basic** auth. +* Técnicas más avanzadas, pero igual de fáciles, para declarar **modelos de JSON profundamente anidados** (gracias a Pydantic). +* Muchas características extra (gracias a Starlette) como: * **WebSockets** * **GraphQL** - * extremely easy tests based on `requests` and `pytest` + * pruebas extremadamente fáciles con `requests` y `pytest` * **CORS** * **Cookie Sessions** - * ...and more. + * ...y mucho más. -## Performance +## Rendimiento -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) +Benchmarks independientes de TechEmpower muestran que aplicaciones de **FastAPI** corriendo con Uvicorn cómo uno de los frameworks de Python más rápidos, únicamente debajo de Starlette y Uvicorn (usados internamente por FastAPI). (*) -To understand more about it, see the section Benchmarks. +Para entender más al respecto revisa la sección Benchmarks. -## Optional Dependencies +## Dependencias Opcionales -Used by Pydantic: +Usadas por Pydantic: -* ujson - for faster JSON "parsing". -* email_validator - for email validation. +* ujson - para "parsing" de JSON más rápido. +* email_validator - para validación de emails. -Used by Starlette: +Usados por Starlette: -* requests - Required if you want to use the `TestClient`. -* aiofiles - Required if you want to use `FileResponse` or `StaticFiles`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. +* requests - Requerido si quieres usar el `TestClient`. +* aiofiles - Requerido si quieres usar `FileResponse` o `StaticFiles`. +* jinja2 - Requerido si quieres usar la configuración por defecto de templates. +* python-multipart - Requerido si quieres dar soporte a "parsing" de formularios, con `request.form()`. +* itsdangerous - Requerido para dar soporte a `SessionMiddleware`. +* pyyaml - Requerido para dar soporte al `SchemaGenerator` de Starlette (probablemente no lo necesites con FastAPI). +* graphene - Requerido para dar soporte a `GraphQLApp`. +* ujson - Requerido si quieres usar `UJSONResponse`. -Used by FastAPI / Starlette: +Usado por FastAPI / Starlette: -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. +* uvicorn - para el servidor que carga y sirve tu aplicación. +* orjson - Requerido si quieres usar `ORJSONResponse`. -You can install all of these with `pip install fastapi[all]`. +Puedes instalarlos con `pip install fastapi[all]`. -## License +## Licencia -This project is licensed under the terms of the MIT license. +Este proyecto está licenciado bajo los términos de la licencia del MIT. From 14b467db06684a78a9bf2853d21db6e9f1f99098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 2 Apr 2020 07:03:00 +0200 Subject: [PATCH 053/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3ee628684e96b..ae07855e344d7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add first translation to Spanish [FastAPI](https://fastapi.tiangolo.com/es/). PR [#1201](https://github.com/tiangolo/fastapi/pull/1201) by [@mariacamilagl](https://github.com/mariacamilagl). * Add docs about [Settings and Environment Variables](https://fastapi.tiangolo.com/advanced/settings/). Initial PR [1118](https://github.com/tiangolo/fastapi/pull/1118) by [@alexmitelman](https://github.com/alexmitelman). ## 0.53.2 From b76334f544368b82d3463c9205fe82d63ddb0ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 4 Apr 2020 21:39:15 +0200 Subject: [PATCH 054/153] :memo: Settings using lru_cache (#1214) * :sparkles: Update settings examples to use lru_cache * :memo: Update docs for Settings, using @lru_cache * :art: Update lru_cache colors to show difference in stored values --- docs/en/docs/advanced/settings.md | 116 ++++++++++++++++++++++++++---- docs_src/settings/app02/config.py | 3 - docs_src/settings/app02/main.py | 8 +-- docs_src/settings/app03/config.py | 3 - docs_src/settings/app03/main.py | 8 +-- 5 files changed, 109 insertions(+), 29 deletions(-) diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index 2b3555c0cd43b..f7d9576640d7e 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -119,7 +119,7 @@ Hello World from Python 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 be done in code. +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` @@ -137,6 +137,9 @@ You can use all the same validation features and tools you use for Pydantic mode {!../../../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`). @@ -151,7 +154,7 @@ Then you can use the new `settings` object in your application: ### Run the server -Then you would run the server passing the configurations as environment variables, for example you could set an `ADMIN_EMAIL` and `APP_NAME` with: +Next, you would run the server passing the configurations as environment variables, for example you could set an `ADMIN_EMAIL` and `APP_NAME` with:
    @@ -174,7 +177,7 @@ 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](bigger-applications.md){.internal-link target=_blank}. +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: @@ -189,7 +192,7 @@ And then use it in a file `main.py`: ``` !!! tip - You would also need a file `__init__.py` as you saw on [Bigger Applications - Multiple Files](bigger-applications.md){.internal-link target=_blank}. + 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 @@ -207,18 +210,19 @@ Coming from the previous example, your `config.py` file could look like: Notice that now we don't create a default instance `settings = Settings()`. -Instead we declare its type as `Settings`, but the value as `None`. - ### The main app file -Now we create a dependency that returns the `settings` object if we already created it. - -Otherwise we create a new one, assign it to `config.settings` and then return it from the dependency. +Now we create a dependency that returns a new `config.Settings()`. -```Python hl_lines="8 9 10 11 12" +```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 19 20" @@ -275,20 +279,102 @@ Here we create a class `Config` inside of your Pydantic `Settings` class, and se !!! tip The `Config` class is used just for Pydantic configuration. You can read more at Pydantic Model Config -### Creating the settings object +### 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() +``` -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, instead of reading it for each request. +we would create that object for each request, and we would be reading the `.env` file for each request. ⚠️ -Because of that, in the dependency function, we first check if we already have a `settings` object, and create a new one (that could read from disk) only if it's still `None`, so, it would happen only the first time: +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="9 10 11 12" +```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 Python docs for `@lru_cache()`. + ## 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. -* Saving the settings in a variable lets you avoid reading the dotenv file again and again for each request. +* Using `@lru_cache()` lets you avoid reading the dotenv file again and again for each request, while allowing you to override it during testing. diff --git a/docs_src/settings/app02/config.py b/docs_src/settings/app02/config.py index 492a58f2e340e..9a7829135690d 100644 --- a/docs_src/settings/app02/config.py +++ b/docs_src/settings/app02/config.py @@ -5,6 +5,3 @@ class Settings(BaseSettings): app_name: str = "Awesome API" admin_email: str items_per_user: int = 50 - - -settings: Settings = None diff --git a/docs_src/settings/app02/main.py b/docs_src/settings/app02/main.py index ba91d46042161..69bc8c6e0eb6b 100644 --- a/docs_src/settings/app02/main.py +++ b/docs_src/settings/app02/main.py @@ -1,3 +1,5 @@ +from functools import lru_cache + from fastapi import Depends, FastAPI from . import config @@ -5,11 +7,9 @@ app = FastAPI() +@lru_cache() def get_settings(): - if config.settings: - return config.settings - config.settings = config.Settings() - return config.settings + return config.Settings() @app.get("/info") diff --git a/docs_src/settings/app03/config.py b/docs_src/settings/app03/config.py index 425a22d8144b2..e1c3ee30063b7 100644 --- a/docs_src/settings/app03/config.py +++ b/docs_src/settings/app03/config.py @@ -8,6 +8,3 @@ class Settings(BaseSettings): class Config: env_file = ".env" - - -settings: Settings = None diff --git a/docs_src/settings/app03/main.py b/docs_src/settings/app03/main.py index ba91d46042161..69bc8c6e0eb6b 100644 --- a/docs_src/settings/app03/main.py +++ b/docs_src/settings/app03/main.py @@ -1,3 +1,5 @@ +from functools import lru_cache + from fastapi import Depends, FastAPI from . import config @@ -5,11 +7,9 @@ app = FastAPI() +@lru_cache() def get_settings(): - if config.settings: - return config.settings - config.settings = config.Settings() - return config.settings + return config.Settings() @app.get("/info") From 70bc46937387605d8ef3ada2d67650c93b0cabad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 4 Apr 2020 21:39:52 +0200 Subject: [PATCH 055/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ae07855e344d7..2359168f7edc6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update docs for Pydantic's `Settings` using a dependency with `@lru_cache()`. PR [#1214](https://github.com/tiangolo/fastapi/pull/1214). * Add first translation to Spanish [FastAPI](https://fastapi.tiangolo.com/es/). PR [#1201](https://github.com/tiangolo/fastapi/pull/1201) by [@mariacamilagl](https://github.com/mariacamilagl). * Add docs about [Settings and Environment Variables](https://fastapi.tiangolo.com/advanced/settings/). Initial PR [1118](https://github.com/tiangolo/fastapi/pull/1118) by [@alexmitelman](https://github.com/alexmitelman). From 0a77c613b09a3c427f95411db8e31e7f65f98277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Botaro?= Date: Sat, 4 Apr 2020 17:01:19 -0300 Subject: [PATCH 056/153] :globe_with_meridians: Add new language on docs: pt (#1210) --- docs/en/mkdocs.yml | 1 + docs/es/mkdocs.yml | 1 + docs/pt/docs/index.md | 441 ++++++++++++++++++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 62 ++++++ docs/zh/mkdocs.yml | 1 + 5 files changed, 506 insertions(+) create mode 100644 docs/pt/docs/index.md create mode 100644 docs/pt/mkdocs.yml diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 107e1e046b567..3a0dfbd7f0717 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -20,6 +20,7 @@ nav: - Languages: - en: / - es: /es/ + - pt: /pt/ - zh: /zh/ - features.md - python-types.md diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 39273b3e16dac..304c01592a723 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -20,6 +20,7 @@ nav: - Languages: - en: / - es: /es/ + - pt: /pt/ - zh: /zh/ markdown_extensions: - toc: diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md new file mode 100644 index 0000000000000..138d8bd607ab7 --- /dev/null +++ b/docs/pt/docs/index.md @@ -0,0 +1,441 @@ + +{!../../../docs/missing-translation.md!} + + +

    + FastAPI +

    +

    + FastAPI framework, high performance, easy to learn, fast to code, ready for production +

    +

    + + Build Status + + + Coverage + + + Package version + + + Join the chat at https://gitter.im/tiangolo/fastapi + +

    + +--- + +**Documentation**: https://fastapi.tiangolo.com + +**Source Code**: https://github.com/tiangolo/fastapi + +--- + +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. + +The key features are: + +* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). + +* **Fast to code**: Increase the speed to develop features by about 200% to 300% *. +* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * +* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. +* **Easy**: Designed to be easy to use and learn. Less time reading docs. +* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. +* **Robust**: Get production-ready code. With automatic interactive documentation. +* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. + +* estimation based on tests on an internal development team, building production applications. + +## Opinions + +"*[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products.*" + +
    Kabir Khan - Microsoft (ref)
    + +--- + +"*I’m over the moon excited about **FastAPI**. It’s so fun!*" + +
    Brian Okken - Python Bytes podcast host (ref)
    + +--- + +"*Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that.*" + +
    Timothy Crosley - Hug creator (ref)
    + +--- + +"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*" + +"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*" + +
    Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
    + +--- + +"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*" + +
    Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
    + +--- + +## **Typer**, the FastAPI of CLIs + + + +If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. + +**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 + +## Requirements + +Python 3.6+ + +FastAPI stands on the shoulders of giants: + +* Starlette for the web parts. +* Pydantic for the data parts. + +## Installation + +
    + +```console +$ pip install fastapi + +---> 100% +``` + +
    + +You will also need an ASGI server, for production such as Uvicorn or Hypercorn. + +
    + +```console +$ pip install uvicorn + +---> 100% +``` + +
    + +## Example + +### Create it + +* Create a file `main.py` 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} +``` + +
    +Or use async def... + +If your code uses `async` / `await`, use `async def`: + +```Python hl_lines="7 12" +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: str = None): + return {"item_id": item_id, "q": q} +``` + +**Note**: + +If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. + +
    + +### Run it + +Run the server with: + +
    + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
    + +
    +About the command uvicorn main:app --reload... + +The command `uvicorn main:app` refers to: + +* `main`: the file `main.py` (the Python "module"). +* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +* `--reload`: make the server restart after code changes. Only do this for development. + +
    + +### Check it + +Open your browser at http://127.0.0.1:8000/items/5?q=somequery. + +You will see the JSON response as: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +You already created an API that: + +* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. +* Both _paths_ take `GET` operations (also known as HTTP _methods_). +* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. +* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. + +### Interactive API docs + +Now go to http://127.0.0.1:8000/docs. + +You will see the automatic interactive API documentation (provided by Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternative API docs + +And now, go to http://127.0.0.1:8000/redoc. + +You will see the alternative automatic documentation (provided by ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Example upgrade + +Now modify the file `main.py` to receive a body from a `PUT` request. + +Declare the body using standard Python types, thanks to Pydantic. + +```Python hl_lines="2 7 8 9 10 23 24 25" +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: bool = None + + +@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} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +The server should reload automatically (because you added `--reload` to the `uvicorn` command above). + +### Interactive API docs upgrade + +Now go to http://127.0.0.1:8000/docs. + +* The interactive API documentation will be automatically updated, including the new body: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternative API docs upgrade + +And now, go to http://127.0.0.1:8000/redoc. + +* The alternative documentation will also reflect the new query parameter and body: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Recap + +In summary, you declare **once** the types of parameters, body, etc. as function parameters. + +You do that with standard modern Python types. + +You don't have to learn a new syntax, the methods or classes of a specific library, etc. + +Just standard **Python 3.6+**. + +For example, for an `int`: + +```Python +item_id: int +``` + +or for a more complex `Item` model: + +```Python +item: Item +``` + +...and with that single declaration you get: + +* Editor support, including: + * Completion. + * Type checks. +* Validation of data: + * Automatic and clear errors when the data is invalid. + * Validation even for deeply nested JSON objects. +* Conversion of input data: coming from the network to Python data and types. Reading from: + * JSON. + * Path parameters. + * Query parameters. + * Cookies. + * Headers. + * Forms. + * Files. +* Conversion of output data: converting from Python data and types to network data (as JSON): + * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` objects. + * `UUID` objects. + * Database models. + * ...and many more. +* Automatic interactive API documentation, including 2 alternative user interfaces: + * Swagger UI. + * ReDoc. + +--- + +Coming back to the previous code example, **FastAPI** will: + +* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. +* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. + * If it is not, the client will see a useful, clear error. +* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. + * As the `q` parameter is declared with `= None`, it is optional. + * Without the `None` it would be required (as is the body in the case with `PUT`). +* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: + * Check that it has a required attribute `name` that should be a `str`. + * Check that it has a required attribute `price` that has to be a `float`. + * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. + * All this would also work for deeply nested JSON objects. +* Convert from and to JSON automatically. +* Document everything with OpenAPI, that can be used by: + * Interactive documentation systems. + * Automatic client code generation systems, for many languages. +* Provide 2 interactive documentation web interfaces directly. + +--- + +We just scratched the surface, but you already get the idea of how it all works. + +Try changing the line with: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...from: + +```Python + ... "item_name": item.name ... +``` + +...to: + +```Python + ... "item_price": item.price ... +``` + +...and see how your editor will auto-complete the attributes and know their types: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +For a more complete example including more features, see the Tutorial - User Guide. + +**Spoiler alert**: the tutorial - user guide includes: + +* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. +* How to set **validation constraints** as `maximum_length` or `regex`. +* A very powerful and easy to use **Dependency Injection** system. +* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. +* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). +* Many extra features (thanks to Starlette) as: + * **WebSockets** + * **GraphQL** + * extremely easy tests based on `requests` and `pytest` + * **CORS** + * **Cookie Sessions** + * ...and more. + +## Performance + +Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) + +To understand more about it, see the section Benchmarks. + +## Optional Dependencies + +Used by Pydantic: + +* ujson - for faster JSON "parsing". +* email_validator - for email validation. + +Used by Starlette: + +* requests - Required if you want to use the `TestClient`. +* aiofiles - Required if you want to use `FileResponse` or `StaticFiles`. +* jinja2 - Required if you want to use the default template configuration. +* python-multipart - Required if you want to support form "parsing", with `request.form()`. +* itsdangerous - Required for `SessionMiddleware` support. +* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). +* graphene - Required for `GraphQLApp` support. +* ujson - Required if you want to use `UJSONResponse`. + +Used by FastAPI / Starlette: + +* uvicorn - for the server that loads and serves your application. +* orjson - Required if you want to use `ORJSONResponse`. + +You can install all of these with `pip install fastapi[all]`. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml new file mode 100644 index 0000000000000..9287548491882 --- /dev/null +++ b/docs/pt/mkdocs.yml @@ -0,0 +1,62 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/pt/ +theme: + name: material + palette: + primary: teal + accent: amber + logo: https://fastapi.tiangolo.com/img/icon-white.svg + favicon: https://fastapi.tiangolo.com/img/favicon.png + language: pt +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +google_analytics: +- UA-133183413-1 +- auto +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ + - pt: /pt/ + - zh: /zh/ +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- markdown_include.include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_div_format '' +extra: + social: + - type: github + link: https://github.com/tiangolo/typer + - type: twitter + link: https://twitter.com/tiangolo + - type: linkedin + link: https://www.linkedin.com/in/tiangolo + - type: rss + link: https://dev.to/tiangolo + - type: medium + link: https://medium.com/@tiangolo + - type: globe + link: https://tiangolo.com +extra_css: +- https://fastapi.tiangolo.com/css/termynal.css +- https://fastapi.tiangolo.com/css/custom.css +extra_javascript: +- https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js +- https://fastapi.tiangolo.com/js/termynal.js +- https://fastapi.tiangolo.com/js/custom.js +- https://fastapi.tiangolo.com/js/chat.js +- https://sidecar.gitter.im/dist/sidecar.v1.js diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index dfd86b15d10f0..1b7588cbbc675 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -20,6 +20,7 @@ nav: - Languages: - en: / - es: /es/ + - pt: /pt/ - zh: /zh/ markdown_extensions: - toc: From c398ac87d9a4f707f6ffb13748a75c4235fb7d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Botaro?= Date: Sat, 4 Apr 2020 17:03:17 -0300 Subject: [PATCH 057/153] :see_no_evil: Add Python venv "env" to gitignore (#1212) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3dbfdd44d16b5..505c67b88c6f4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ test.db log.txt Pipfile.lock env3.* +env docs_build From 651ee5e4d2d36d26156d1367ce11ebf33e3c5e8f Mon Sep 17 00:00:00 2001 From: Gao Chao Date: Sun, 5 Apr 2020 04:13:55 +0800 Subject: [PATCH 058/153] :art: Update log style in main page, for GitHub Markdown compatibility (#1200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update README.md fix string format * :art: Update log style in main page, for GitHub Markdown compatibility Co-authored-by: Sebastián Ramírez --- README.md | 10 +++++----- docs/en/docs/index.md | 10 +++++----- docs/es/docs/index.md | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 7a12add10a7fe..263b9f3371202 100644 --- a/README.md +++ b/README.md @@ -176,11 +176,11 @@ Run the server with: ```console $ uvicorn main:app --reload -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. +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. ```
    diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 7a12add10a7fe..263b9f3371202 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -176,11 +176,11 @@ Run the server with: ```console $ uvicorn main:app --reload -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. +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. ``` diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 28a5d40679878..084494ae0815d 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -175,11 +175,11 @@ Corre el servidor con: ```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. +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. ``` From 5c111caf409f742f4b2e35207e6d35b87225c8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 4 Apr 2020 22:05:22 +0200 Subject: [PATCH 059/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2359168f7edc6..b4f1d8d46995b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +* Add Python venv `env` to `.gitignore`. PR [#1212](https://github.com/tiangolo/fastapi/pull/1212) by [@cassiobotaro](https://github.com/cassiobotaro). +* Start Portuguese translations. PR [#1210](https://github.com/tiangolo/fastapi/pull/1210) by [@cassiobotaro](https://github.com/cassiobotaro). * Update docs for Pydantic's `Settings` using a dependency with `@lru_cache()`. PR [#1214](https://github.com/tiangolo/fastapi/pull/1214). * Add first translation to Spanish [FastAPI](https://fastapi.tiangolo.com/es/). PR [#1201](https://github.com/tiangolo/fastapi/pull/1201) by [@mariacamilagl](https://github.com/mariacamilagl). * Add docs about [Settings and Environment Variables](https://fastapi.tiangolo.com/advanced/settings/). Initial PR [1118](https://github.com/tiangolo/fastapi/pull/1118) by [@alexmitelman](https://github.com/alexmitelman). From a1a19b103ce9df516a3693e51058473355d04fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 4 Apr 2020 22:16:12 +0200 Subject: [PATCH 060/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b4f1d8d46995b..71d642d576d87 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update log style in README.md for GitHub Markdown compatibility. PR [#1200](https://github.com/tiangolo/fastapi/pull/1200) by [#geekgao](https://github.com/geekgao). * Add Python venv `env` to `.gitignore`. PR [#1212](https://github.com/tiangolo/fastapi/pull/1212) by [@cassiobotaro](https://github.com/cassiobotaro). * Start Portuguese translations. PR [#1210](https://github.com/tiangolo/fastapi/pull/1210) by [@cassiobotaro](https://github.com/cassiobotaro). * Update docs for Pydantic's `Settings` using a dependency with `@lru_cache()`. PR [#1214](https://github.com/tiangolo/fastapi/pull/1214). From 10fb7ace047e71616d8b0b1feaa996858d3f8b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 4 Apr 2020 22:36:10 +0200 Subject: [PATCH 061/153] :memo: Update contributing guidelines to review translation PRs (#1215) --- docs/en/docs/contributing.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 57767f4884fda..66fc8efc9fd92 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -257,7 +257,16 @@ Here are the steps to help with translations. #### Tips and guidelines -* Add a single Pull Request per page translated. That will make it much easier for others to review it. +* Check the currently existing pull requests for your language and add reviews requesting changes or approving them. + +!!! tip + You can add comments with change suggestions to existing pull requests. + + Check the docs about adding a pull request review to approve it or request changes. + +* Check in the issues 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. @@ -385,6 +394,11 @@ Updating en 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". From fd99dfc95b4501d7cd3d0d8aae0dcdbba9ebb527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 4 Apr 2020 22:38:17 +0200 Subject: [PATCH 062/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 71d642d576d87..d3c9cac165013 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update docs for [Development - Contributing: Translations](https://fastapi.tiangolo.com/contributing/#translations) including note about reviewing translation PRs. [#1215](https://github.com/tiangolo/fastapi/pull/1215). * Update log style in README.md for GitHub Markdown compatibility. PR [#1200](https://github.com/tiangolo/fastapi/pull/1200) by [#geekgao](https://github.com/geekgao). * Add Python venv `env` to `.gitignore`. PR [#1212](https://github.com/tiangolo/fastapi/pull/1212) by [@cassiobotaro](https://github.com/cassiobotaro). * Start Portuguese translations. PR [#1210](https://github.com/tiangolo/fastapi/pull/1210) by [@cassiobotaro](https://github.com/cassiobotaro). From d96223460bed36e117fb7292e032ee85c01567de Mon Sep 17 00:00:00 2001 From: duganchen Date: Sun, 5 Apr 2020 04:53:09 -0700 Subject: [PATCH 063/153] :memo: Add an example of setting up a test database (#1144) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add an example of setting up a test database. * :memo: Add/update docs for testing a DB with dependency overrides * :wrench: Update test script, remove line removing test file as it is removed during testing * :white_check_mark: Update testing coverage pragma Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/testing-database.md | 95 +++++++++++++++++++ docs/en/docs/advanced/testing-dependencies.md | 12 --- docs/en/docs/tutorial/sql-databases.md | 4 +- docs/en/mkdocs.yml | 1 + docs_src/sql_databases/sql_app/database.py | 2 +- .../sql_databases/sql_app/tests/__init__.py | 0 .../sql_app/tests/test_sql_app.py | 47 +++++++++ scripts/test.sh | 4 - .../test_sql_databases/test_sql_databases.py | 2 +- .../test_sql_databases_middleware.py | 2 +- .../test_testing_databases.py | 13 +++ 11 files changed, 161 insertions(+), 21 deletions(-) create mode 100644 docs/en/docs/advanced/testing-database.md create mode 100644 docs_src/sql_databases/sql_app/tests/__init__.py create mode 100644 docs_src/sql_databases/sql_app/tests/test_sql_app.py create mode 100644 tests/test_tutorial/test_sql_databases/test_testing_databases.py diff --git a/docs/en/docs/advanced/testing-database.md b/docs/en/docs/advanced/testing-database.md new file mode 100644 index 0000000000000..385d988005fae --- /dev/null +++ b/docs/en/docs/advanced/testing-database.md @@ -0,0 +1,95 @@ +# 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 10 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 9 10 11 12 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 20 21 22 23 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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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`. diff --git a/docs/en/docs/advanced/testing-dependencies.md b/docs/en/docs/advanced/testing-dependencies.md index 5ba7e219b9b76..f2c283947ff83 100644 --- a/docs/en/docs/advanced/testing-dependencies.md +++ b/docs/en/docs/advanced/testing-dependencies.md @@ -20,18 +20,6 @@ You probably want to test the external provider once, but not necessarily call i In this case, you can override the dependency that calls that provider, and use a custom dependency that returns a mock user, only for your tests. -### Use case: testing database - -Other example could be that you are using a specific database only for testing. - -Your normal dependency would return a database session. - -But then, after each test, you could want to rollback all the operations or remove data. - -Or you could want to alter the data before the tests run, etc. - -In this case, you could use a dependency override to return your *custom* database session instead of the one that would be used normally. - ### Use the `app.dependency_overrides` attribute For these cases, your **FastAPI** application has an attribute `app.dependency_overrides`, it is a simple `dict`. diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index 179e75f0a9ec6..726acc7c46c34 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -98,9 +98,9 @@ Let's refer to the file `sql_app/database.py`. In this example, we are "connecting" to a SQLite database (opening a file with the SQLite database). -The file will be located at the same directory in the file `test.db`. +The file will be located at the same directory in the file `sql_app.db`. -That's why the last part is `./test.db`. +That's why the last part is `./sql_app.db`. If you were using a **PostgreSQL** database instead, you would just have to uncomment the line: diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 3a0dfbd7f0717..717dc8e3c2a85 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -99,6 +99,7 @@ nav: - advanced/testing-websockets.md - advanced/testing-events.md - advanced/testing-dependencies.md + - advanced/testing-database.md - advanced/settings.md - advanced/extending-openapi.md - advanced/openapi-callbacks.md diff --git a/docs_src/sql_databases/sql_app/database.py b/docs_src/sql_databases/sql_app/database.py index 73fc456a24c26..45a8b9f6942b3 100644 --- a/docs_src/sql_databases/sql_app/database.py +++ b/docs_src/sql_databases/sql_app/database.py @@ -2,7 +2,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" +SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" # SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" engine = create_engine( diff --git a/docs_src/sql_databases/sql_app/tests/__init__.py b/docs_src/sql_databases/sql_app/tests/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs_src/sql_databases/sql_app/tests/test_sql_app.py b/docs_src/sql_databases/sql_app/tests/test_sql_app.py new file mode 100644 index 0000000000000..01c11a863ea60 --- /dev/null +++ b/docs_src/sql_databases/sql_app/tests/test_sql_app.py @@ -0,0 +1,47 @@ +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from ..database import Base +from ..main import app, get_db + +SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +Base.metadata.create_all(bind=engine) + + +def override_get_db(): + try: + db = TestingSessionLocal() + yield db + finally: + db.close() + + +app.dependency_overrides[get_db] = override_get_db + +client = TestClient(app) + + +def test_create_user(): + response = client.post( + "/users/", + json={"email": "deadpool@example.com", "password": "chimichangas4life"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["email"] == "deadpool@example.com" + assert "id" in data + user_id = data["id"] + + response = client.get(f"/users/{user_id}") + assert response.status_code == 200 + data = response.json() + assert data["email"] == "deadpool@example.com" + assert data["id"] == user_id diff --git a/scripts/test.sh b/scripts/test.sh index 468b2c6674c83..54fd7410bc309 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,10 +3,6 @@ set -e set -x -# Remove temporary DB -if [ -f ./test.db ]; then - rm ./test.db -fi bash ./scripts/lint.sh # Check README.md is up to date diff --brief docs/en/docs/index.md README.md diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases.py b/tests/test_tutorial/test_sql_databases/test_sql_databases.py index acafa011c29a3..b791ea386e15a 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases.py @@ -286,7 +286,7 @@ def client(): # Import while creating the client to create the DB after starting the test session from sql_databases.sql_app.main import app - test_db = Path("./test.db") + test_db = Path("./sql_app.db") with TestClient(app) as c: yield c test_db.unlink() diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py index 43bc87657c23d..96b48874d4a4b 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py @@ -286,7 +286,7 @@ def client(): # Import while creating the client to create the DB after starting the test session from sql_databases.sql_app.alt_main import app - test_db = Path("./test.db") + test_db = Path("./sql_app.db") with TestClient(app) as c: yield c test_db.unlink() diff --git a/tests/test_tutorial/test_sql_databases/test_testing_databases.py b/tests/test_tutorial/test_sql_databases/test_testing_databases.py new file mode 100644 index 0000000000000..fae66d3b87c1e --- /dev/null +++ b/tests/test_tutorial/test_sql_databases/test_testing_databases.py @@ -0,0 +1,13 @@ +from pathlib import Path + + +def test_testing_dbs(): + # Import while creating the client to create the DB after starting the test session + from sql_databases.sql_app.tests.test_sql_app import test_create_user + + test_db = Path("./test.db") + app_db = Path("./sql_app.db") + test_create_user() + test_db.unlink() + if app_db.is_file(): # pragma: nocover + app_db.unlink() From 766157bfb4e7dfccba09ab398e8ec444d14e947c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 5 Apr 2020 13:57:45 +0200 Subject: [PATCH 064/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d3c9cac165013..7b8c5a8229bdc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add example about [Testing a Database](https://fastapi.tiangolo.com/advanced/testing-database/). Initial PR [#1144](https://github.com/tiangolo/fastapi/pull/1144) by [@duganchen](https://github.com/duganchen). * Update docs for [Development - Contributing: Translations](https://fastapi.tiangolo.com/contributing/#translations) including note about reviewing translation PRs. [#1215](https://github.com/tiangolo/fastapi/pull/1215). * Update log style in README.md for GitHub Markdown compatibility. PR [#1200](https://github.com/tiangolo/fastapi/pull/1200) by [#geekgao](https://github.com/geekgao). * Add Python venv `env` to `.gitignore`. PR [#1212](https://github.com/tiangolo/fastapi/pull/1212) by [@cassiobotaro](https://github.com/cassiobotaro). From 3397d4d69a9c2d64c1219fcbf291ea5697a4abb8 Mon Sep 17 00:00:00 2001 From: voegtlel <5764745+voegtlel@users.noreply.github.com> Date: Sun, 5 Apr 2020 15:04:46 +0200 Subject: [PATCH 065/153] :sparkles: Implement response_model_exclude_defaults and response_model_exclude_none (#1166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implemented response_model_exclude_defaults and response_model_exclude_none to be compatible pydantic options. * :truck: Rename and invert include_none to exclude_none to keep in sync with Pydantic Co-authored-by: Lukas Voegtle Co-authored-by: Sebastián Ramírez --- fastapi/applications.py | 40 +++++++++++++++ fastapi/encoders.py | 22 +++++--- fastapi/openapi/utils.py | 4 +- fastapi/routing.py | 93 +++++++++++++++++++++++++++++++--- tests/test_jsonable_encoder.py | 16 ++++++ tests/test_skip_defaults.py | 64 ++++++++++++++++++++++- 6 files changed, 223 insertions(+), 16 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 8270e54fdf4d0..84a1b6de290b3 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -171,6 +171,8 @@ def add_api_route( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -197,6 +199,8 @@ def add_api_route( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -222,6 +226,8 @@ def api_route( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -250,6 +256,8 @@ def decorator(func: Callable) -> Callable: response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -309,6 +317,8 @@ def get( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -334,6 +344,8 @@ def get( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -359,6 +371,8 @@ def put( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -384,6 +398,8 @@ def put( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -409,6 +425,8 @@ def post( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -434,6 +452,8 @@ def post( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -459,6 +479,8 @@ def delete( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -484,6 +506,8 @@ def delete( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -509,6 +533,8 @@ def options( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -534,6 +560,8 @@ def options( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -559,6 +587,8 @@ def head( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -584,6 +614,8 @@ def head( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -609,6 +641,8 @@ def patch( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -634,6 +668,8 @@ def patch( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -659,6 +695,8 @@ def trace( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -684,6 +722,8 @@ def trace( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, diff --git a/fastapi/encoders.py b/fastapi/encoders.py index ae4794bae8d94..26ceb21445f15 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -34,7 +34,8 @@ def jsonable_encoder( by_alias: bool = True, skip_defaults: bool = None, exclude_unset: bool = False, - include_none: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, custom_encoder: dict = {}, sqlalchemy_safe: bool = True, ) -> Any: @@ -58,8 +59,12 @@ def jsonable_encoder( exclude=exclude, by_alias=by_alias, exclude_unset=bool(exclude_unset or skip_defaults), + exclude_none=exclude_none, + exclude_defaults=exclude_defaults, ) else: # pragma: nocover + if exclude_defaults: + raise ValueError("Cannot use exclude_defaults") obj_dict = obj.dict( include=include, exclude=exclude, @@ -68,7 +73,8 @@ def jsonable_encoder( ) return jsonable_encoder( obj_dict, - include_none=include_none, + exclude_none=exclude_none, + exclude_defaults=exclude_defaults, custom_encoder=encoder, sqlalchemy_safe=sqlalchemy_safe, ) @@ -87,14 +93,14 @@ def jsonable_encoder( or (not isinstance(key, str)) or (not key.startswith("_sa")) ) - and (value is not None or include_none) + and (value is not None or not exclude_none) and ((include and key in include) or key not in exclude) ): encoded_key = jsonable_encoder( key, by_alias=by_alias, exclude_unset=exclude_unset, - include_none=include_none, + exclude_none=exclude_none, custom_encoder=custom_encoder, sqlalchemy_safe=sqlalchemy_safe, ) @@ -102,7 +108,7 @@ def jsonable_encoder( value, by_alias=by_alias, exclude_unset=exclude_unset, - include_none=include_none, + exclude_none=exclude_none, custom_encoder=custom_encoder, sqlalchemy_safe=sqlalchemy_safe, ) @@ -118,7 +124,8 @@ def jsonable_encoder( exclude=exclude, by_alias=by_alias, exclude_unset=exclude_unset, - include_none=include_none, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, custom_encoder=custom_encoder, sqlalchemy_safe=sqlalchemy_safe, ) @@ -153,7 +160,8 @@ def jsonable_encoder( data, by_alias=by_alias, exclude_unset=exclude_unset, - include_none=include_none, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, custom_encoder=custom_encoder, sqlalchemy_safe=sqlalchemy_safe, ) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 91f90ec583785..c1e66fc8d29b1 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -81,7 +81,7 @@ def get_openapi_security_definitions(flat_dependant: Dependant) -> Tuple[Dict, L security_definition = jsonable_encoder( security_requirement.security_scheme.model, by_alias=True, - include_none=False, + exclude_none=True, ) security_name = security_requirement.security_scheme.scheme_name security_definitions[security_name] = security_definition @@ -310,4 +310,4 @@ def get_openapi( if components: output["components"] = components output["paths"] = paths - return jsonable_encoder(OpenAPI(**output), by_alias=True, include_none=False) + return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) diff --git a/fastapi/routing.py b/fastapi/routing.py index 1ec0b693c87b0..3ac420e6e2296 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -49,22 +49,43 @@ def _prepare_response_content( - res: Any, *, by_alias: bool = True, exclude_unset: bool + res: Any, + *, + by_alias: bool = True, + exclude_unset: bool, + exclude_defaults: bool = False, + exclude_none: bool = False, ) -> Any: if isinstance(res, BaseModel): if PYDANTIC_1: - return res.dict(by_alias=by_alias, exclude_unset=exclude_unset) + return res.dict( + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) else: return res.dict( - by_alias=by_alias, skip_defaults=exclude_unset + by_alias=by_alias, skip_defaults=exclude_unset, ) # pragma: nocover elif isinstance(res, list): return [ - _prepare_response_content(item, exclude_unset=exclude_unset) for item in res + _prepare_response_content( + item, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + for item in res ] elif isinstance(res, dict): return { - k: _prepare_response_content(v, exclude_unset=exclude_unset) + k: _prepare_response_content( + v, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) for k, v in res.items() } return res @@ -78,12 +99,18 @@ async def serialize_response( exclude: Union[SetIntStr, DictIntStrAny] = set(), by_alias: bool = True, exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, is_coroutine: bool = True, ) -> Any: if field: errors = [] response_content = _prepare_response_content( - response_content, by_alias=by_alias, exclude_unset=exclude_unset + response_content, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, ) if is_coroutine: value, errors_ = field.validate(response_content, {}, loc=("response",)) @@ -103,6 +130,8 @@ async def serialize_response( exclude=exclude, by_alias=by_alias, exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, ) else: return jsonable_encoder(response_content) @@ -131,6 +160,8 @@ def get_request_handler( response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, dependency_overrides_provider: Any = None, ) -> Callable: assert dependant.call is not None, "dependant.call must be a function" @@ -177,6 +208,8 @@ async def app(request: Request) -> Response: exclude=response_model_exclude, by_alias=response_model_by_alias, exclude_unset=response_model_exclude_unset, + exclude_defaults=response_model_exclude_defaults, + exclude_none=response_model_exclude_none, is_coroutine=is_coroutine, ) response = response_class( @@ -255,6 +288,8 @@ def __init__( response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Optional[Type[Response]] = None, dependency_overrides_provider: Any = None, @@ -326,6 +361,8 @@ def __init__( self.response_model_exclude = response_model_exclude self.response_model_by_alias = response_model_by_alias self.response_model_exclude_unset = response_model_exclude_unset + self.response_model_exclude_defaults = response_model_exclude_defaults + self.response_model_exclude_none = response_model_exclude_none self.include_in_schema = include_in_schema self.response_class = response_class @@ -352,6 +389,8 @@ def get_route_handler(self) -> Callable: response_model_exclude=self.response_model_exclude, response_model_by_alias=self.response_model_by_alias, response_model_exclude_unset=self.response_model_exclude_unset, + response_model_exclude_defaults=self.response_model_exclude_defaults, + response_model_exclude_none=self.response_model_exclude_none, dependency_overrides_provider=self.dependency_overrides_provider, ) @@ -400,6 +439,8 @@ def add_api_route( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -429,6 +470,8 @@ def add_api_route( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -457,6 +500,8 @@ def api_route( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -486,6 +531,8 @@ def decorator(func: Callable) -> Callable: response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -560,6 +607,8 @@ def include_router( response_model_exclude=route.response_model_exclude, response_model_by_alias=route.response_model_by_alias, response_model_exclude_unset=route.response_model_exclude_unset, + response_model_exclude_defaults=route.response_model_exclude_defaults, + response_model_exclude_none=route.response_model_exclude_none, include_in_schema=route.include_in_schema, response_class=route.response_class or default_response_class, name=route.name, @@ -606,6 +655,8 @@ def get( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -632,6 +683,8 @@ def get( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -657,6 +710,8 @@ def put( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -683,6 +738,8 @@ def put( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -708,6 +765,8 @@ def post( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -734,6 +793,8 @@ def post( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -759,6 +820,8 @@ def delete( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -785,6 +848,8 @@ def delete( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -810,6 +875,8 @@ def options( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -836,6 +903,8 @@ def options( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -861,6 +930,8 @@ def head( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -887,6 +958,8 @@ def head( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -912,6 +985,8 @@ def patch( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -938,6 +1013,8 @@ def patch( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -963,6 +1040,8 @@ def trace( response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -989,6 +1068,8 @@ def trace( response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index f94771aaeed94..adee443a8943c 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -70,6 +70,12 @@ class ModelWithAlias(BaseModel): foo: str = Field(..., alias="Foo") +class ModelWithDefault(BaseModel): + foo: str = ... + bar: str = "bar" + bla: str = "bla" + + @pytest.fixture( name="model_with_path", params=[PurePath, PurePosixPath, PureWindowsPath] ) @@ -121,6 +127,16 @@ def test_encode_model_with_alias(): assert jsonable_encoder(model) == {"Foo": "Bar"} +def test_encode_model_with_default(): + model = ModelWithDefault(foo="foo", bar="bar") + assert jsonable_encoder(model) == {"foo": "foo", "bar": "bar", "bla": "bla"} + assert jsonable_encoder(model, exclude_unset=True) == {"foo": "foo", "bar": "bar"} + assert jsonable_encoder(model, exclude_defaults=True) == {"foo": "foo"} + assert jsonable_encoder(model, exclude_unset=True, exclude_defaults=True) == { + "foo": "foo" + } + + def test_custom_encoders(): class safe_datetime(datetime): pass diff --git a/tests/test_skip_defaults.py b/tests/test_skip_defaults.py index c9a85a305a3c3..ed84df66c40b2 100644 --- a/tests/test_skip_defaults.py +++ b/tests/test_skip_defaults.py @@ -18,11 +18,53 @@ class Model(BaseModel): class ModelSubclass(Model): y: int + z: int = 0 + w: int = None + + +class ModelDefaults(BaseModel): + w: Optional[str] = None + x: Optional[str] = None + y: str = "y" + z: str = "z" @app.get("/", response_model=Model, response_model_exclude_unset=True) def get() -> ModelSubclass: - return ModelSubclass(sub={}, y=1) + return ModelSubclass(sub={}, y=1, z=0) + + +@app.get( + "/exclude_unset", response_model=ModelDefaults, response_model_exclude_unset=True +) +def get() -> ModelDefaults: + return ModelDefaults(x=None, y="y") + + +@app.get( + "/exclude_defaults", + response_model=ModelDefaults, + response_model_exclude_defaults=True, +) +def get() -> ModelDefaults: + return ModelDefaults(x=None, y="y") + + +@app.get( + "/exclude_none", response_model=ModelDefaults, response_model_exclude_none=True +) +def get() -> ModelDefaults: + return ModelDefaults(x=None, y="y") + + +@app.get( + "/exclude_unset_none", + response_model=ModelDefaults, + response_model_exclude_unset=True, + response_model_exclude_none=True, +) +def get() -> ModelDefaults: + return ModelDefaults(x=None, y="y") client = TestClient(app) @@ -31,3 +73,23 @@ def get() -> ModelSubclass: def test_return_defaults(): response = client.get("/") assert response.json() == {"sub": {}} + + +def test_return_exclude_unset(): + response = client.get("/exclude_unset") + assert response.json() == {"x": None, "y": "y"} + + +def test_return_exclude_defaults(): + response = client.get("/exclude_defaults") + assert response.json() == {} + + +def test_return_exclude_none(): + response = client.get("/exclude_none") + assert response.json() == {"y": "y", "z": "z"} + + +def test_return_exclude_unset_none(): + response = client.get("/exclude_unset_none") + assert response.json() == {"y": "y"} From 1cc30de32fb454fbee6ea3707b3d2a0722704180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 5 Apr 2020 15:07:00 +0200 Subject: [PATCH 066/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7b8c5a8229bdc..1bcd85ce2509a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest changes +* Add support for `response_model_exclude_defaults` and `response_model_exclude_none`: + * Deprecate the parameter `include_none` in `jsonable_encoder` and add the inverted `exclude_none`, to keep in sync with Pydantic. + * PR [#1166](https://github.com/tiangolo/fastapi/pull/1166) by [@voegtlel](https://github.com/voegtlel). * Add example about [Testing a Database](https://fastapi.tiangolo.com/advanced/testing-database/). Initial PR [#1144](https://github.com/tiangolo/fastapi/pull/1144) by [@duganchen](https://github.com/duganchen). * Update docs for [Development - Contributing: Translations](https://fastapi.tiangolo.com/contributing/#translations) including note about reviewing translation PRs. [#1215](https://github.com/tiangolo/fastapi/pull/1215). * Update log style in README.md for GitHub Markdown compatibility. PR [#1200](https://github.com/tiangolo/fastapi/pull/1200) by [#geekgao](https://github.com/geekgao). From 07e094fd5077e12150099390eb4ed2d8532db707 Mon Sep 17 00:00:00 2001 From: Harsha Laxman Date: Sun, 5 Apr 2020 06:15:39 -0700 Subject: [PATCH 067/153] :memo: Add note about Alembic in project generator in SQL docs (#1183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update sql-databases alembic docs Was helpful to refer to the full-stack project when integrating alembic into my own project * :memo: Update Alembic note in docs Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/sql-databases.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index 726acc7c46c34..c60eda0f80d73 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -437,6 +437,8 @@ And you would also use Alembic for "migrations" (that's its main job). A "migration" is the set of steps needed whenever you change the structure of your SQLAlchemy models, add a new attribute, etc. to replicate those changes in the database, add a new column, a new table, etc. +You can find an example of Alembic in a FastAPI project in the templates from [Project Generation - Template](../project-generation.md){.internal-link target=_blank}. Specifically in the `alembic` directory in the source code. + ### Create a dependency !!! info From c56342bf797460afcb9cd5f17d9a42ba63aae9ad Mon Sep 17 00:00:00 2001 From: Mickey Pashov Date: Sun, 5 Apr 2020 15:46:22 +0100 Subject: [PATCH 068/153] :pencil2: Fix minor grammatical mistakes in the async docs (#1188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix minor grammatical mistakes in the async docs. * :pencil2: Update wording and clarify with emojis Co-authored-by: Sebastián Ramírez --- docs/en/docs/async.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/en/docs/async.md b/docs/en/docs/async.md index dc17e5172c1cf..2d0a7311e4d5b 100644 --- a/docs/en/docs/async.md +++ b/docs/en/docs/async.md @@ -63,13 +63,13 @@ Let's see that phrase by parts in the sections below, below: ## Asynchronous Code -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". +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" 📝. -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 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. +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. -And then it takes the first task to finish (let's say, our "slow-file") and continues whatever it had to do with it. +Next, 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 I/O operations that are relatively "slow" (compared to the speed of the processor and the RAM memory), like waiting for: @@ -82,7 +82,7 @@ That "wait for something else" normally refers to I/O operations, so they call them "I/O bound". +As the execution time is consumed mostly by waiting for I/O operations, they call them "I/O bound" operations. 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. @@ -114,7 +114,7 @@ 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). -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. +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. 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. @@ -132,7 +132,7 @@ Then, when it's your turn, you do actual "productive" work, you process the menu 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 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. +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. 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. From 8d92557e53373bd01677e4ab4c6687131e7daf97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 5 Apr 2020 16:47:33 +0200 Subject: [PATCH 069/153] :memo: Update relase notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1bcd85ce2509a..9a267b498579c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix grammatical mistakes in async docs. PR [#1188](https://github.com/tiangolo/fastapi/pull/1188) by [@mickeypash](https://github.com/mickeypash). * Add support for `response_model_exclude_defaults` and `response_model_exclude_none`: * Deprecate the parameter `include_none` in `jsonable_encoder` and add the inverted `exclude_none`, to keep in sync with Pydantic. * PR [#1166](https://github.com/tiangolo/fastapi/pull/1166) by [@voegtlel](https://github.com/voegtlel). From 7372f6ba11abb515a7f11814dba52a1d1c0925f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 5 Apr 2020 16:50:16 +0200 Subject: [PATCH 070/153] :bookmark: Release version 0.54.0 --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9a267b498579c..2743f3f47df7a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +## 0.54.0 + * Fix grammatical mistakes in async docs. PR [#1188](https://github.com/tiangolo/fastapi/pull/1188) by [@mickeypash](https://github.com/mickeypash). * Add support for `response_model_exclude_defaults` and `response_model_exclude_none`: * Deprecate the parameter `include_none` in `jsonable_encoder` and add the inverted `exclude_none`, to keep in sync with Pydantic. diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 99334962d8351..b2cfecfe73f82 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.53.2" +__version__ = "0.54.0" from starlette import status From 483bce3ae1a987f6f248cd4bb5b96955652fb0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Apr 2020 06:25:01 +0200 Subject: [PATCH 071/153] :arrow_up: Upgrade to MkDocs Material 5 and update docs scripts (#1225) * :arrow_up: Upgrade mkdocs.yml configs for MkDocs Material 5 * :sparkles: Update docs.py to always update mkdocs.yml * :globe_with_meridians: Update mkdocs.yml for translations * :wrench: Update MkDocs config * :sparkles: Add tabs for alternative options * :arrow_up: Update termynal setup with new CSS classes * :wrench: Sync / Update mkdocs.yml for languages --- docs/en/docs/advanced/settings.md | 40 ++++++------ docs/en/docs/contributing.md | 100 +++++++++++++++++------------- docs/en/docs/deployment.md | 62 +++++++++--------- docs/en/docs/js/custom.js | 2 +- docs/en/mkdocs.yml | 15 +++-- docs/es/mkdocs.yml | 17 +++-- docs/pt/mkdocs.yml | 15 +++-- docs/zh/mkdocs.yml | 15 +++-- scripts/docs.py | 62 +++++++++--------- 9 files changed, 184 insertions(+), 144 deletions(-) diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index f7d9576640d7e..f30de226c7c22 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -15,35 +15,37 @@ An +=== "Linux, macOS, Windows Bash" -```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" + ```console + // You could create an env var MY_NAME with + $ export MY_NAME="Wade Wilson" -Hello Wade Wilson -``` + // Then you could use it with other programs, like + $ echo "Hello $MY_NAME" -
    + Hello Wade Wilson + ``` -Or in PowerShell in Windows: + -
    +=== "Windows PowerShell" -```console -// Create an env var MY_NAME -$ $Env:MY_NAME = "Wade Wilson" +
    -// Use it with other programs, like -$ echo "Hello $Env:MY_NAME" + ```console + // Create an env var MY_NAME + $ $Env:MY_NAME = "Wade Wilson" -Hello Wade Wilson -``` + // Use it with other programs, like + $ echo "Hello $Env:MY_NAME" -
    + Hello Wade Wilson + ``` + +
    ### Read env vars in Python diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 66fc8efc9fd92..416bc32f84838 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -24,59 +24,67 @@ That will create a directory `./env/` with the Python binaries and then you will Activate the new environment with: -
    +=== "Linux, macOS" -```console -$ source ./env/bin/activate -``` +
    -
    + ```console + $ source ./env/bin/activate + ``` -Or in Windows' PowerShell: +
    -
    -
    +=== "Windows Bash" -```console -$ source ./env/Scripts/activate -``` + Or if you use Bash for Windows (e.g. Git Bash): -
    +
    + + ```console + $ source ./env/Scripts/activate + ``` + +
    To check it worked, use: -
    +=== "Linux, macOS, Windows Bash" -```console -$ which pip +
    -some/directory/fastapi/env/bin/pip -``` + ```console + $ which pip -
    + some/directory/fastapi/env/bin/pip + ``` -If it shows the `pip` binary at `env/bin/pip` then it worked. 🎉 +
    -Or in Windows PowerShell: +=== "Windows PowerShell" -
    +
    -```console -$ Get-Command pip + ```console + $ Get-Command pip + + some/directory/fastapi/env/bin/pip + ``` + +
    + +If it shows the `pip` binary at `env/bin/pip` then it worked. 🎉 -some/directory/fastapi/env/bin/pip -``` -
    !!! tip Every time you install a new package with `pip` under that environment, activate the environment again. @@ -103,27 +111,31 @@ Now re-activate the environment to make sure you are using the `flit` you just i And now use `flit` to install the development dependencies: -
    +=== "Linux, macOS" -```console -$ flit install --deps develop --symlink +
    ----> 100% -``` + ```console + $ flit install --deps develop --symlink -
    + ---> 100% + ``` -If you are on Windows, use `--pth-file` instead of `--symlink`: +
    -
    +=== "Windows" -```console -$ flit install --deps develop --pth-file + If you are on Windows, use `--pth-file` instead of `--symlink`: ----> 100% -``` +
    -
    + ```console + $ flit install --deps develop --pth-file + + ---> 100% + ``` + +
    It will install all the dependencies and your local FastAPI in your local environment. diff --git a/docs/en/docs/deployment.md b/docs/en/docs/deployment.md index 8ca7c433ff6af..94ab1b90b9b07 100644 --- a/docs/en/docs/deployment.md +++ b/docs/en/docs/deployment.md @@ -329,55 +329,61 @@ You can deploy **FastAPI** directly without Docker too. You just need to install an ASGI compatible server like: -* Uvicorn, a lightning-fast ASGI server, built on uvloop and httptools. +=== "Uvicorn" -
    + * Uvicorn, a lightning-fast ASGI server, built on uvloop and httptools. -```console -$ pip install uvicorn +
    ----> 100% -``` + ```console + $ pip install uvicorn -
    + ---> 100% + ``` -* Hypercorn, an ASGI server also compatible with HTTP/2. +
    -
    +=== "Hypercorn" -```console -$ pip install hypercorn + * Hypercorn, an ASGI server also compatible with HTTP/2. ----> 100% -``` +
    -
    + ```console + $ pip install hypercorn + + ---> 100% + ``` -...or any other ASGI server. +
    + + ...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" -```console -$ uvicorn main:app --host 0.0.0.0 --port 80 +
    -INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) -``` + ```console + $ uvicorn main:app --host 0.0.0.0 --port 80 -
    + INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) + ``` -or with Hypercorn: +
    -
    +=== "Hypercorn" -```console -$ hypercorn main:app --bind 0.0.0.0:80 +
    -Running on 0.0.0.0:8080 over http (CTRL + C to quit) -``` + ```console + $ hypercorn main:app --bind 0.0.0.0:80 -
    + Running on 0.0.0.0:8080 over http (CTRL + C to quit) + ``` + +
    You might want to set up some tooling to make sure it is restarted automatically if it stops. diff --git a/docs/en/docs/js/custom.js b/docs/en/docs/js/custom.js index 0f1d485216167..72140df8ba8b8 100644 --- a/docs/en/docs/js/custom.js +++ b/docs/en/docs/js/custom.js @@ -35,7 +35,7 @@ function setupTermynal() { function createTermynals() { document - .querySelectorAll(`.${termynalActivateClass} .codehilite`) + .querySelectorAll(`.${termynalActivateClass} .highlight`) .forEach(node => { const text = node.textContent; const lines = text.split("\n"); diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 717dc8e3c2a85..9e13e68c5e063 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -6,6 +6,8 @@ theme: palette: primary: teal accent: amber + icon: + repo: fontawesome/brands/github-alt logo: img/icon-white.svg favicon: img/favicon.png language: en @@ -129,19 +131,20 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_div_format '' +- pymdownx.tabbed extra: social: - - type: github + - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/typer - - type: twitter + - icon: fontawesome/brands/twitter link: https://twitter.com/tiangolo - - type: linkedin + - icon: fontawesome/brands/linkedin link: https://www.linkedin.com/in/tiangolo - - type: rss + - icon: fontawesome/brands/dev link: https://dev.to/tiangolo - - type: medium + - icon: fontawesome/brands/medium link: https://medium.com/@tiangolo - - type: globe + - icon: fontawesome/solid/globe link: https://tiangolo.com extra_css: - css/termynal.css diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 304c01592a723..e9fe5a3c8ae4f 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -6,6 +6,8 @@ theme: palette: primary: teal accent: amber + icon: + repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg favicon: https://fastapi.tiangolo.com/img/favicon.png language: es @@ -37,19 +39,20 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_div_format '' +- pymdownx.tabbed extra: social: - - type: github + - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/typer - - type: twitter + - icon: fontawesome/brands/twitter link: https://twitter.com/tiangolo - - type: linkedin + - icon: fontawesome/brands/linkedin link: https://www.linkedin.com/in/tiangolo - - type: rss + - icon: fontawesome/brands/dev link: https://dev.to/tiangolo - - type: medium + - icon: fontawesome/brands/medium link: https://medium.com/@tiangolo - - type: globe + - icon: fontawesome/solid/globe link: https://tiangolo.com extra_css: - https://fastapi.tiangolo.com/css/termynal.css @@ -58,3 +61,5 @@ extra_javascript: - https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +- https://fastapi.tiangolo.com/js/chat.js +- https://sidecar.gitter.im/dist/sidecar.v1.js diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 9287548491882..70e8dd106d47f 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -6,6 +6,8 @@ theme: palette: primary: teal accent: amber + icon: + repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg favicon: https://fastapi.tiangolo.com/img/favicon.png language: pt @@ -37,19 +39,20 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_div_format '' +- pymdownx.tabbed extra: social: - - type: github + - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/typer - - type: twitter + - icon: fontawesome/brands/twitter link: https://twitter.com/tiangolo - - type: linkedin + - icon: fontawesome/brands/linkedin link: https://www.linkedin.com/in/tiangolo - - type: rss + - icon: fontawesome/brands/dev link: https://dev.to/tiangolo - - type: medium + - icon: fontawesome/brands/medium link: https://medium.com/@tiangolo - - type: globe + - icon: fontawesome/solid/globe link: https://tiangolo.com extra_css: - https://fastapi.tiangolo.com/css/termynal.css diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 1b7588cbbc675..6a95e605d640b 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -6,6 +6,8 @@ theme: palette: primary: teal accent: amber + icon: + repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg favicon: https://fastapi.tiangolo.com/img/favicon.png language: zh @@ -37,19 +39,20 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_div_format '' +- pymdownx.tabbed extra: social: - - type: github + - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/typer - - type: twitter + - icon: fontawesome/brands/twitter link: https://twitter.com/tiangolo - - type: linkedin + - icon: fontawesome/brands/linkedin link: https://www.linkedin.com/in/tiangolo - - type: rss + - icon: fontawesome/brands/dev link: https://dev.to/tiangolo - - type: medium + - icon: fontawesome/brands/medium link: https://medium.com/@tiangolo - - type: globe + - icon: fontawesome/solid/globe link: https://tiangolo.com extra_css: - https://fastapi.tiangolo.com/css/termynal.css diff --git a/scripts/docs.py b/scripts/docs.py index 186a3e4f89121..73f21371b80ed 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -20,6 +20,12 @@ """ docs_path = Path("docs") +en_docs_path = Path("docs/en") +en_config_path: Path = en_docs_path / mkdocs_name + + +def get_en_config() -> dict: + return mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) def get_lang_paths(): @@ -43,39 +49,15 @@ def complete_existing_lang(incomplete: str): yield lang_path.name -@app.command() -def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): - """ - Generate a new docs translation directory for the language LANG. - - LANG should be a 2-letter language code, like: en, es, de, pt, etc. - """ - new_path: Path = Path("docs") / lang - if new_path.exists(): - typer.echo(f"The language was already created: {lang}") - raise typer.Abort() - new_path.mkdir() - en_docs_path = Path("docs/en") - en_config_path: Path = en_docs_path / mkdocs_name - en_config: dict = mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) +def get_base_lang_config(lang: str): + en_config = get_en_config() fastapi_url_base = "https://fastapi.tiangolo.com/" - new_config = {} - new_config["site_name"] = en_config["site_name"] - new_config["site_description"] = en_config["site_description"] + new_config = en_config.copy() new_config["site_url"] = en_config["site_url"] + f"{lang}/" - new_config["theme"] = en_config["theme"] new_config["theme"]["logo"] = fastapi_url_base + en_config["theme"]["logo"] new_config["theme"]["favicon"] = fastapi_url_base + en_config["theme"]["favicon"] new_config["theme"]["language"] = lang - new_config["repo_name"] = en_config["repo_name"] - new_config["repo_url"] = en_config["repo_url"] - new_config["edit_uri"] = en_config["edit_uri"] - new_config["google_analytics"] = en_config["google_analytics"] new_config["nav"] = en_config["nav"][:2] - - new_config["markdown_extensions"] = en_config["markdown_extensions"] - new_config["extra"] = en_config["extra"] - extra_css = [] css: str for css in en_config["extra_css"]: @@ -93,6 +75,22 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): else: extra_js.append(fastapi_url_base + js) new_config["extra_javascript"] = extra_js + return new_config + + +@app.command() +def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): + """ + Generate a new docs translation directory for the language LANG. + + LANG should be a 2-letter language code, like: en, es, de, pt, etc. + """ + new_path: Path = Path("docs") / lang + if new_path.exists(): + typer.echo(f"The language was already created: {lang}") + raise typer.Abort() + new_path.mkdir() + new_config = get_base_lang_config(lang) new_config_path: Path = Path(new_path) / mkdocs_name new_config_path.write_text( yaml.dump(new_config, sort_keys=False, width=200), encoding="utf-8" @@ -280,7 +278,15 @@ def live( def update_config(lang: str): lang_path: Path = docs_path / lang config_path = lang_path / mkdocs_name - config: dict = mkdocs.utils.yaml_load(config_path.read_text(encoding="utf-8")) + current_config: dict = mkdocs.utils.yaml_load( + config_path.read_text(encoding="utf-8") + ) + if lang == "en": + config = get_en_config() + else: + config = get_base_lang_config(lang) + config["nav"] = current_config["nav"] + config["theme"]["language"] = current_config["theme"]["language"] languages = [{"en": "/"}] for lang in get_lang_paths(): if lang.name == "en" or not lang.is_dir(): From e9b189e9f22c70ef6625f67555cc4732ed0f4a88 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 8 Apr 2020 05:37:38 +0100 Subject: [PATCH 072/153] :white_check_mark: Improve test debugging (#1222) --- .../sql_app/tests/test_sql_app.py | 4 +-- tests/test_additional_properties.py | 4 +-- tests/test_additional_response_extra.py | 4 +-- ...tional_responses_custom_validationerror.py | 2 +- ...ional_responses_default_validationerror.py | 2 +- ...est_additional_responses_response_class.py | 2 +- tests/test_additional_responses_router.py | 8 ++--- tests/test_application.py | 6 ++-- tests/test_custom_swagger_ui_redirect.py | 4 +-- tests/test_dependency_cache.py | 12 +++---- tests/test_dependency_class.py | 2 +- tests/test_dependency_contextmanager.py | 16 +++++----- tests/test_dependency_duplicates.py | 10 +++--- tests/test_empty_router.py | 4 +-- tests/test_extra_routes.py | 16 +++++----- tests/test_filter_pydantic_sub_model.py | 4 +-- tests/test_forms_from_non_typing_sequences.py | 6 ++-- tests/test_include_route.py | 2 +- tests/test_infer_param_optionality.py | 20 ++++++------ .../test_modules_same_name_body/test_main.py | 10 +++--- tests/test_multi_body_errors.py | 8 ++--- tests/test_multi_query_errors.py | 6 ++-- tests/test_no_swagger_ui_redirect.py | 4 +-- tests/test_param_class.py | 4 +-- tests/test_param_in_path_and_dependency.py | 2 +- tests/test_path.py | 4 +-- tests/test_put_no_body.py | 6 ++-- ...test_request_body_parameters_media_type.py | 2 +- tests/test_response_change_status_code.py | 2 +- tests/test_response_class_no_mediatype.py | 2 +- tests/test_response_code_no_body.py | 2 +- tests/test_response_model_sub_types.py | 10 +++--- tests/test_router_events.py | 2 +- tests/test_router_prefix_with_template.py | 2 +- tests/test_security_api_key_cookie.py | 6 ++-- .../test_security_api_key_cookie_optional.py | 6 ++-- tests/test_security_api_key_header.py | 6 ++-- .../test_security_api_key_header_optional.py | 6 ++-- tests/test_security_api_key_query.py | 6 ++-- tests/test_security_api_key_query_optional.py | 6 ++-- tests/test_security_http_base.py | 6 ++-- tests/test_security_http_base_optional.py | 6 ++-- tests/test_security_http_basic_optional.py | 10 +++--- tests/test_security_http_basic_realm.py | 10 +++--- tests/test_security_http_bearer.py | 8 ++--- tests/test_security_http_bearer_optional.py | 8 ++--- tests/test_security_http_digest.py | 8 ++--- tests/test_security_http_digest_optional.py | 8 ++--- tests/test_security_oauth2.py | 8 ++--- ...curity_oauth2_authorization_code_bearer.py | 8 ++--- tests/test_security_oauth2_optional.py | 8 ++--- ...ecurity_oauth2_password_bearer_optional.py | 8 ++--- tests/test_security_openid_connect.py | 8 ++--- .../test_security_openid_connect_optional.py | 8 ++--- tests/test_starlette_exception.py | 10 +++--- tests/test_starlette_urlconvertors.py | 6 ++-- tests/test_sub_callbacks.py | 2 +- tests/test_swagger_ui_init_oauth.py | 2 +- .../test_tutorial001.py | 6 ++-- .../test_tutorial002.py | 6 ++-- .../test_tutorial003.py | 6 ++-- .../test_tutorial004.py | 6 ++-- .../test_tutorial001.py | 4 +-- .../test_tutorial001.py | 4 +-- .../test_tutorial002.py | 6 ++-- .../test_tutorial003.py | 4 +-- .../test_tutorial001.py | 6 ++-- .../test_background_tasks/test_tutorial001.py | 2 +- .../test_background_tasks/test_tutorial002.py | 2 +- .../test_bigger_applications/test_main.py | 8 ++--- .../test_body/test_tutorial001.py | 4 +-- .../test_body_fields/test_tutorial001.py | 2 +- .../test_tutorial001.py | 2 +- .../test_tutorial003.py | 2 +- .../test_tutorial009.py | 6 ++-- .../test_body_updates/test_tutorial001.py | 4 +-- .../test_cors/test_tutorial001.py | 6 ++-- .../test_custom_response/test_tutorial001b.py | 4 +-- .../test_custom_response/test_tutorial004.py | 4 +-- .../test_custom_response/test_tutorial005.py | 4 +-- .../test_custom_response/test_tutorial006.py | 2 +- .../test_dependencies/test_tutorial001.py | 2 +- .../test_dependencies/test_tutorial004.py | 2 +- .../test_dependencies/test_tutorial006.py | 10 +++--- .../test_events/test_tutorial001.py | 4 +-- .../test_events/test_tutorial002.py | 4 +-- .../test_tutorial001.py | 6 ++-- .../test_tutorial002.py | 8 ++--- .../test_extra_data_types/test_tutorial001.py | 4 +-- .../test_extra_models/test_tutorial003.py | 6 ++-- .../test_extra_models/test_tutorial004.py | 4 +-- .../test_extra_models/test_tutorial005.py | 4 +-- .../test_handling_errors/test_tutorial001.py | 6 ++-- .../test_handling_errors/test_tutorial002.py | 6 ++-- .../test_handling_errors/test_tutorial003.py | 6 ++-- .../test_handling_errors/test_tutorial004.py | 8 ++--- .../test_handling_errors/test_tutorial005.py | 6 ++-- .../test_handling_errors/test_tutorial006.py | 8 ++--- .../test_metadata/test_tutorial001.py | 4 +-- .../test_tutorial001.py | 2 +- .../test_tutorial001.py | 4 +-- .../test_tutorial002.py | 4 +-- .../test_tutorial003.py | 4 +-- .../test_tutorial004.py | 4 +-- .../test_tutorial005.py | 4 +-- .../test_tutorial006.py | 2 +- .../test_path_params/test_tutorial004.py | 6 ++-- .../test_path_params/test_tutorial005.py | 2 +- .../test_query_params/test_tutorial007.py | 6 ++-- .../test_tutorial001.py | 2 +- .../test_tutorial011.py | 6 ++-- .../test_tutorial012.py | 6 ++-- .../test_tutorial013.py | 6 ++-- .../test_request_files/test_tutorial001.py | 12 +++---- .../test_request_files/test_tutorial002.py | 12 +++---- .../test_request_forms/test_tutorial001.py | 4 +-- .../test_tutorial001.py | 12 +++---- .../test_tutorial001.py | 4 +-- .../test_response_cookies/test_tutorial001.py | 2 +- .../test_response_cookies/test_tutorial002.py | 2 +- .../test_response_headers/test_tutorial001.py | 2 +- .../test_response_headers/test_tutorial002.py | 2 +- .../test_response_model/test_tutorial003.py | 4 +-- .../test_response_model/test_tutorial004.py | 4 +-- .../test_response_model/test_tutorial005.py | 6 ++-- .../test_response_model/test_tutorial006.py | 6 ++-- .../test_security/test_tutorial001.py | 8 ++--- .../test_security/test_tutorial003.py | 18 +++++------ .../test_security/test_tutorial005.py | 32 +++++++++---------- .../test_security/test_tutorial006.py | 10 +++--- .../test_sql_databases/test_sql_databases.py | 20 ++++++------ .../test_sql_databases_middleware.py | 20 ++++++------ .../test_sql_databases_peewee.py | 22 ++++++------- .../test_sub_applications/test_tutorial001.py | 8 ++--- .../test_templates/test_tutorial001.py | 4 +-- tests/test_tutorial/test_testing/test_main.py | 2 +- .../test_testing/test_tutorial001.py | 2 +- .../test_tutorial001.py | 8 ++--- .../test_websockets/test_tutorial001.py | 2 +- .../test_websockets/test_tutorial002.py | 2 +- .../test_wsgi/test_tutorial001.py | 4 +-- tests/test_union_body.py | 6 ++-- tests/test_union_inherited_body.py | 6 ++-- tests/test_validate_response_recursive.py | 4 +-- 144 files changed, 434 insertions(+), 434 deletions(-) diff --git a/docs_src/sql_databases/sql_app/tests/test_sql_app.py b/docs_src/sql_databases/sql_app/tests/test_sql_app.py index 01c11a863ea60..c60c3356f85ed 100644 --- a/docs_src/sql_databases/sql_app/tests/test_sql_app.py +++ b/docs_src/sql_databases/sql_app/tests/test_sql_app.py @@ -34,14 +34,14 @@ def test_create_user(): "/users/", json={"email": "deadpool@example.com", "password": "chimichangas4life"}, ) - assert response.status_code == 200 + assert response.status_code == 200, response.text data = response.json() assert data["email"] == "deadpool@example.com" assert "id" in data user_id = data["id"] response = client.get(f"/users/{user_id}") - assert response.status_code == 200 + assert response.status_code == 200, response.text data = response.json() assert data["email"] == "deadpool@example.com" assert data["id"] == user_id diff --git a/tests/test_additional_properties.py b/tests/test_additional_properties.py index de38ce050538f..9e15e6ed06042 100644 --- a/tests/test_additional_properties.py +++ b/tests/test_additional_properties.py @@ -100,11 +100,11 @@ def foo(items: Items): def test_additional_properties_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_additional_properties_post(): response = client.post("/foo", json={"items": {"foo": 1, "bar": 2}}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"foo": 1, "bar": 2} diff --git a/tests/test_additional_response_extra.py b/tests/test_additional_response_extra.py index a20036cc7fd17..1df1891e059c2 100644 --- a/tests/test_additional_response_extra.py +++ b/tests/test_additional_response_extra.py @@ -42,11 +42,11 @@ def read_item(): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_path_operation(): response = client.get("/items/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"id": "foo"} diff --git a/tests/test_additional_responses_custom_validationerror.py b/tests/test_additional_responses_custom_validationerror.py index ce21bca24e597..811fe69221272 100644 --- a/tests/test_additional_responses_custom_validationerror.py +++ b/tests/test_additional_responses_custom_validationerror.py @@ -96,5 +96,5 @@ async def a(id): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_additional_responses_default_validationerror.py b/tests/test_additional_responses_default_validationerror.py index c7f56367b588e..6ea372ce8d696 100644 --- a/tests/test_additional_responses_default_validationerror.py +++ b/tests/test_additional_responses_default_validationerror.py @@ -81,5 +81,5 @@ async def a(id): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_additional_responses_response_class.py b/tests/test_additional_responses_response_class.py index 4d4e1db891197..aa549b1636ead 100644 --- a/tests/test_additional_responses_response_class.py +++ b/tests/test_additional_responses_response_class.py @@ -113,5 +113,5 @@ async def b(): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_additional_responses_router.py b/tests/test_additional_responses_router.py index bc5efeb035a36..d2b73058f7e92 100644 --- a/tests/test_additional_responses_router.py +++ b/tests/test_additional_responses_router.py @@ -89,23 +89,23 @@ async def c(): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_a(): response = client.get("/a") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == "a" def test_b(): response = client.get("/b") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == "b" def test_c(): response = client.get("/c") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == "c" diff --git a/tests/test_application.py b/tests/test_application.py index f4d294e1b7fb2..f6d77460a4032 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1128,7 +1128,7 @@ def test_get_path(path, expected_status, expected_response): def test_swagger_ui(): response = client.get("/docs") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.headers["content-type"] == "text/html; charset=utf-8" assert "swagger-ui-dist" in response.text assert ( @@ -1139,13 +1139,13 @@ def test_swagger_ui(): def test_swagger_ui_oauth2_redirect(): response = client.get("/docs/oauth2-redirect") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.headers["content-type"] == "text/html; charset=utf-8" assert "window.opener.swaggerUIRedirectOauth2" in response.text def test_redoc(): response = client.get("/redoc") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.headers["content-type"] == "text/html; charset=utf-8" assert "redoc@next" in response.text diff --git a/tests/test_custom_swagger_ui_redirect.py b/tests/test_custom_swagger_ui_redirect.py index ac724f8fb46ec..996734be219fe 100644 --- a/tests/test_custom_swagger_ui_redirect.py +++ b/tests/test_custom_swagger_ui_redirect.py @@ -16,7 +16,7 @@ async def read_items(): def test_swagger_ui(): response = client.get("/docs") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.headers["content-type"] == "text/html; charset=utf-8" assert "swagger-ui-dist" in response.text print(client.base_url) @@ -28,7 +28,7 @@ def test_swagger_ui(): def test_swagger_ui_oauth2_redirect(): response = client.get(swagger_ui_oauth2_redirect_url) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.headers["content-type"] == "text/html; charset=utf-8" assert "window.opener.swaggerUIRedirectOauth2" in response.text diff --git a/tests/test_dependency_cache.py b/tests/test_dependency_cache.py index dc7a14cd4b56c..65ed7f946459e 100644 --- a/tests/test_dependency_cache.py +++ b/tests/test_dependency_cache.py @@ -41,28 +41,28 @@ async def get_sub_counter_no_cache( def test_normal_counter(): counter_holder["counter"] = 0 response = client.get("/counter/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"counter": 1} response = client.get("/counter/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"counter": 2} def test_sub_counter(): counter_holder["counter"] = 0 response = client.get("/sub-counter/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"counter": 1, "subcounter": 1} response = client.get("/sub-counter/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"counter": 2, "subcounter": 2} def test_sub_counter_no_cache(): counter_holder["counter"] = 0 response = client.get("/sub-counter-no-cache/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"counter": 2, "subcounter": 1} response = client.get("/sub-counter-no-cache/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"counter": 4, "subcounter": 3} diff --git a/tests/test_dependency_class.py b/tests/test_dependency_class.py index 4b3bd724322b1..ba2e3cfcfe381 100644 --- a/tests/test_dependency_class.py +++ b/tests/test_dependency_class.py @@ -66,5 +66,5 @@ async def get_asynchronous_method_dependency( ) def test_class_dependency(route, value): response = client.get(route, params={"value": value}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == value diff --git a/tests/test_dependency_contextmanager.py b/tests/test_dependency_contextmanager.py index 5090e5459d117..847a41dce03ea 100644 --- a/tests/test_dependency_contextmanager.py +++ b/tests/test_dependency_contextmanager.py @@ -206,7 +206,7 @@ async def bg(state: dict): def test_async_state(): assert state["/async"] == f"asyncgen not started" response = client.get("/async") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == f"asyncgen started" assert state["/async"] == f"asyncgen completed" @@ -214,7 +214,7 @@ def test_async_state(): def test_sync_state(): assert state["/sync"] == f"generator not started" response = client.get("/sync") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == f"generator started" assert state["/sync"] == f"generator completed" @@ -237,7 +237,7 @@ def test_sync_raise_other(): def test_async_raise(): response = client.get("/async_raise") - assert response.status_code == 500 + assert response.status_code == 500, response.text assert state["/async_raise"] == "asyncgen raise finalized" assert "/async_raise" in errors errors.clear() @@ -272,7 +272,7 @@ def test_background_tasks(): def test_sync_raise(): response = client.get("/sync_raise") - assert response.status_code == 500 + assert response.status_code == 500, response.text assert state["/sync_raise"] == "generator raise finalized" assert "/sync_raise" in errors errors.clear() @@ -280,14 +280,14 @@ def test_sync_raise(): def test_sync_async_state(): response = client.get("/sync_async") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == f"asyncgen started" assert state["/async"] == f"asyncgen completed" def test_sync_sync_state(): response = client.get("/sync_sync") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == f"generator started" assert state["/sync"] == f"generator completed" @@ -308,7 +308,7 @@ def test_sync_sync_raise_other(): def test_sync_async_raise(): response = client.get("/sync_async_raise") - assert response.status_code == 500 + assert response.status_code == 500, response.text assert state["/async_raise"] == "asyncgen raise finalized" assert "/async_raise" in errors errors.clear() @@ -316,7 +316,7 @@ def test_sync_async_raise(): def test_sync_sync_raise(): response = client.get("/sync_sync_raise") - assert response.status_code == 500 + assert response.status_code == 500, response.text assert state["/sync_raise"] == "generator raise finalized" assert "/sync_raise" in errors errors.clear() diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py index 0462b43c848cd..5e15812b67042 100644 --- a/tests/test_dependency_duplicates.py +++ b/tests/test_dependency_duplicates.py @@ -190,13 +190,13 @@ async def no_duplicates_sub( def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_no_duplicates_invalid(): response = client.post("/no-duplicates", json={"item": {"data": "myitem"}}) - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == { "detail": [ { @@ -213,19 +213,19 @@ def test_no_duplicates(): "/no-duplicates", json={"item": {"data": "myitem"}, "item2": {"data": "myitem2"}}, ) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"data": "myitem"}, {"data": "myitem2"}] def test_duplicates(): response = client.post("/with-duplicates", json={"data": "myitem"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"data": "myitem"}, {"data": "myitem"}] def test_sub_duplicates(): response = client.post("/with-duplicates-sub", json={"data": "myitem"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [ {"data": "myitem"}, [{"data": "myitem"}, {"data": "myitem"}], diff --git a/tests/test_empty_router.py b/tests/test_empty_router.py index ccca5dbd34fce..186ceb347e048 100644 --- a/tests/test_empty_router.py +++ b/tests/test_empty_router.py @@ -21,11 +21,11 @@ def get_empty(): def test_use_empty(): with client: response = client.get("/prefix") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == ["OK"] response = client.get("/prefix/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == ["OK"] diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index 150ae1e078735..a2459b3588d27 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -314,47 +314,47 @@ def trace_item(item_id: str): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get_api_route(): response = client.get("/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": "foo"} def test_get_api_route_not_decorated(): response = client.get("/items-not-decorated/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": "foo"} def test_delete(): response = client.delete("/items/foo", json={"name": "Foo"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": "foo", "item": {"name": "Foo", "price": None}} def test_head(): response = client.head("/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.headers["x-fastapi-item-id"] == "foo" def test_options(): response = client.options("/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.headers["x-fastapi-item-id"] == "foo" def test_patch(): response = client.patch("/items/foo", json={"name": "Foo"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": "foo", "item": {"name": "Foo", "price": None}} def test_trace(): response = client.request("trace", "/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.headers["content-type"] == "message/http" diff --git a/tests/test_filter_pydantic_sub_model.py b/tests/test_filter_pydantic_sub_model.py index 9d631cc0215f7..9ce753d1e2179 100644 --- a/tests/test_filter_pydantic_sub_model.py +++ b/tests/test_filter_pydantic_sub_model.py @@ -127,13 +127,13 @@ async def get_model_a(name: str, model_c=Depends(get_model_c)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_filter_sub_model(): response = client.get("/model/modelA") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == { "name": "modelA", "description": "model-a-desc", diff --git a/tests/test_forms_from_non_typing_sequences.py b/tests/test_forms_from_non_typing_sequences.py index a34ec44c29612..be917eab7e2e0 100644 --- a/tests/test_forms_from_non_typing_sequences.py +++ b/tests/test_forms_from_non_typing_sequences.py @@ -26,7 +26,7 @@ def test_python_list_param_as_form(): response = client.post( "/form/python-list", data={"items": ["first", "second", "third"]} ) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == ["first", "second", "third"] @@ -34,7 +34,7 @@ def test_python_set_param_as_form(): response = client.post( "/form/python-set", data={"items": ["first", "second", "third"]} ) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert set(response.json()) == {"first", "second", "third"} @@ -42,5 +42,5 @@ def test_python_tuple_param_as_form(): response = client.post( "/form/python-tuple", data={"items": ["first", "second", "third"]} ) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == ["first", "second", "third"] diff --git a/tests/test_include_route.py b/tests/test_include_route.py index 61bf34f40400f..88696e2ba9800 100644 --- a/tests/test_include_route.py +++ b/tests/test_include_route.py @@ -18,5 +18,5 @@ def read_items(request: Request): def test_sub_router(): response = client.get("/items/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"hello": "world"} diff --git a/tests/test_infer_param_optionality.py b/tests/test_infer_param_optionality.py index 6d189989777f7..166c3fcc780b7 100644 --- a/tests/test_infer_param_optionality.py +++ b/tests/test_infer_param_optionality.py @@ -46,21 +46,21 @@ def get_item(item_id: str, user_id: str = None): def test_get_users(): """Check that /users returns expected data""" response = client.get("/users") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"user_id": "u1"}, {"user_id": "u2"}] def test_get_user(): """Check that /users/{user_id} returns expected data""" response = client.get("/users/abc123") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"user_id": "abc123"} def test_get_items_1(): """Check that /items returns expected data""" response = client.get("/items") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [ {"item_id": "i1", "user_id": "u1"}, {"item_id": "i2", "user_id": "u2"}, @@ -70,42 +70,42 @@ def test_get_items_1(): def test_get_items_2(): """Check that /items returns expected data with user_id specified""" response = client.get("/items?user_id=abc123") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"item_id": "i2", "user_id": "abc123"}] def test_get_item_1(): """Check that /items/{item_id} returns expected data""" response = client.get("/items/item01") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": "item01"} def test_get_item_2(): """Check that /items/{item_id} returns expected data with user_id specified""" response = client.get("/items/item01?user_id=abc123") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": "item01", "user_id": "abc123"} def test_get_users_items(): """Check that /users/{user_id}/items returns expected data""" response = client.get("/users/abc123/items") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"item_id": "i2", "user_id": "abc123"}] def test_get_users_item(): """Check that /users/{user_id}/items returns expected data""" response = client.get("/users/abc123/items/item01") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": "item01", "user_id": "abc123"} def test_schema_1(): """Check that the user_id is a required path parameter under /users""" response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text r = response.json() d = { @@ -122,7 +122,7 @@ def test_schema_1(): def test_schema_2(): """Check that the user_id is an optional query parameter under /items""" response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text r = response.json() d = { diff --git a/tests/test_modules_same_name_body/test_main.py b/tests/test_modules_same_name_body/test_main.py index e73b5c408b5ea..b0d3330c72b2c 100644 --- a/tests/test_modules_same_name_body/test_main.py +++ b/tests/test_modules_same_name_body/test_main.py @@ -125,31 +125,31 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_post_a(): data = {"a": 2, "b": "foo"} response = client.post("/a/compute", json=data) - assert response.status_code == 200 + assert response.status_code == 200, response.text data = response.json() def test_post_a_invalid(): data = {"a": "bar", "b": "foo"} response = client.post("/a/compute", json=data) - assert response.status_code == 422 + assert response.status_code == 422, response.text def test_post_b(): data = {"a": 2, "b": "foo"} response = client.post("/b/compute/", json=data) - assert response.status_code == 200 + assert response.status_code == 200, response.text data = response.json() def test_post_b_invalid(): data = {"a": "bar", "b": "foo"} response = client.post("/b/compute/", json=data) - assert response.status_code == 422 + assert response.status_code == 422, response.text diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 23e6b64d556ec..f198042619537 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -139,23 +139,23 @@ def save_item_no_body(item: List[Item]): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_put_correct_body(): response = client.post("/items/", json=[{"name": "Foo", "age": 5}]) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item": [{"name": "Foo", "age": 5}]} def test_jsonable_encoder_requiring_error(): response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}]) - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == single_error def test_put_incorrect_body_multiple(): response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}]) - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == multiple_errors diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index 467ed9f4fb6c1..69ea87a9b65cc 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -102,17 +102,17 @@ def read_items(q: List[int] = Query(None)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_multi_query(): response = client.get("/items/?q=5&q=6") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"q": [5, 6]} def test_multi_query_incorrect(): response = client.get("/items/?q=five&q=six") - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == multiple_errors diff --git a/tests/test_no_swagger_ui_redirect.py b/tests/test_no_swagger_ui_redirect.py index b403fa64c9400..0df9a46e57891 100644 --- a/tests/test_no_swagger_ui_redirect.py +++ b/tests/test_no_swagger_ui_redirect.py @@ -14,7 +14,7 @@ async def read_items(): def test_swagger_ui(): response = client.get("/docs") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.headers["content-type"] == "text/html; charset=utf-8" assert "swagger-ui-dist" in response.text print(client.base_url) @@ -23,7 +23,7 @@ def test_swagger_ui(): def test_swagger_ui_no_oauth2_redirect(): response = client.get("/docs/oauth2-redirect") - assert response.status_code == 404 + assert response.status_code == 404, response.text def test_response(): diff --git a/tests/test_param_class.py b/tests/test_param_class.py index 3066f22bfabba..bbb9eefcc3d7d 100644 --- a/tests/test_param_class.py +++ b/tests/test_param_class.py @@ -15,11 +15,11 @@ def read_items(q: str = Param(None)): def test_default_param_query_none(): response = client.get("/items/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"q": None} def test_default_param_query(): response = client.get("/items/?q=foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"q": "foo"} diff --git a/tests/test_param_in_path_and_dependency.py b/tests/test_param_in_path_and_dependency.py index 214d90174096f..0a94c2151316d 100644 --- a/tests/test_param_in_path_and_dependency.py +++ b/tests/test_param_in_path_and_dependency.py @@ -90,4 +90,4 @@ def test_reused_param(): def test_read_users(): response = client.get("/users/42") - assert response.status_code == 200 + assert response.status_code == 200, response.text diff --git a/tests/test_path.py b/tests/test_path.py index 26b65e695d220..d1a58bc66174d 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -8,13 +8,13 @@ def test_text_get(): response = client.get("/text") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == "Hello World" def test_nonexistent(): response = client.get("/nonexistent") - assert response.status_code == 404 + assert response.status_code == 404, response.text assert response.json() == {"detail": "Not Found"} diff --git a/tests/test_put_no_body.py b/tests/test_put_no_body.py index 95dbf03d2c8ec..1c2cfac891f52 100644 --- a/tests/test_put_no_body.py +++ b/tests/test_put_no_body.py @@ -81,17 +81,17 @@ def save_item_no_body(item_id: str): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_put_no_body(): response = client.put("/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": "foo"} def test_put_no_body_with_body(): response = client.put("/items/foo", json={"name": "Foo"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": "foo"} diff --git a/tests/test_request_body_parameters_media_type.py b/tests/test_request_body_parameters_media_type.py index 2eecd45eaa105..e3bd916729545 100644 --- a/tests/test_request_body_parameters_media_type.py +++ b/tests/test_request_body_parameters_media_type.py @@ -55,7 +55,7 @@ async def create_shop( def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text openapi_schema = response.json() assert ( openapi_schema["paths"]["/products"]["post"]["requestBody"] diff --git a/tests/test_response_change_status_code.py b/tests/test_response_change_status_code.py index d401b81b05cf7..377ce7897a630 100644 --- a/tests/test_response_change_status_code.py +++ b/tests/test_response_change_status_code.py @@ -22,5 +22,5 @@ async def get_main(): def test_dependency_set_status_code(): response = client.get("/") - assert response.status_code == 201 + assert response.status_code == 201, response.text assert response.json() == {"msg": "Hello World"} diff --git a/tests/test_response_class_no_mediatype.py b/tests/test_response_class_no_mediatype.py index ab869c3c5dbce..eb8500f3a872d 100644 --- a/tests/test_response_class_no_mediatype.py +++ b/tests/test_response_class_no_mediatype.py @@ -110,5 +110,5 @@ async def b(): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_response_code_no_body.py b/tests/test_response_code_no_body.py index 7d658d0cf7c47..45e2fabc7ef79 100644 --- a/tests/test_response_code_no_body.py +++ b/tests/test_response_code_no_body.py @@ -104,5 +104,5 @@ async def b(): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_response_model_sub_types.py b/tests/test_response_model_sub_types.py index 3f146481280bf..fd972e6a3d472 100644 --- a/tests/test_response_model_sub_types.py +++ b/tests/test_response_model_sub_types.py @@ -145,16 +145,16 @@ def valid4(): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_path_operations(): response = client.get("/valid1") - assert response.status_code == 200 + assert response.status_code == 200, response.text response = client.get("/valid2") - assert response.status_code == 200 + assert response.status_code == 200, response.text response = client.get("/valid3") - assert response.status_code == 200 + assert response.status_code == 200, response.text response = client.get("/valid4") - assert response.status_code == 200 + assert response.status_code == 200, response.text diff --git a/tests/test_router_events.py b/tests/test_router_events.py index 5aeb966f30478..5ff1fdf9f2059 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -77,7 +77,7 @@ def test_router_events(): assert state.router_shutdown is False assert state.sub_router_shutdown is False response = client.get("/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"message": "Hello World"} assert state.app_startup is True assert state.router_startup is True diff --git a/tests/test_router_prefix_with_template.py b/tests/test_router_prefix_with_template.py index b331c24bdf42f..163b213ff845c 100644 --- a/tests/test_router_prefix_with_template.py +++ b/tests/test_router_prefix_with_template.py @@ -19,5 +19,5 @@ def read_user(segment: str, id: str): def test_get(): response = client.get("/seg/users/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"segment": "seg", "id": "foo"} diff --git a/tests/test_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py index 6b9642988103e..a5b2e44f0ce50 100644 --- a/tests/test_security_api_key_cookie.py +++ b/tests/test_security_api_key_cookie.py @@ -52,17 +52,17 @@ def read_current_user(current_user: User = Depends(get_current_user)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_api_key(): response = client.get("/users/me", cookies={"key": "secret"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "secret"} def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403 + assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} diff --git a/tests/test_security_api_key_cookie_optional.py b/tests/test_security_api_key_cookie_optional.py index 0f3da9ad5edc0..96a64f09a6e7d 100644 --- a/tests/test_security_api_key_cookie_optional.py +++ b/tests/test_security_api_key_cookie_optional.py @@ -59,17 +59,17 @@ def read_current_user(current_user: User = Depends(get_current_user)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_api_key(): response = client.get("/users/me", cookies={"key": "secret"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "secret"} def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_security_api_key_header.py b/tests/test_security_api_key_header.py index a871a65fe419f..d53395f9991ac 100644 --- a/tests/test_security_api_key_header.py +++ b/tests/test_security_api_key_header.py @@ -52,17 +52,17 @@ def read_current_user(current_user: User = Depends(get_current_user)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_api_key(): response = client.get("/users/me", headers={"key": "secret"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "secret"} def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403 + assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} diff --git a/tests/test_security_api_key_header_optional.py b/tests/test_security_api_key_header_optional.py index f71bac9cf6ccf..4ab599c2ddd28 100644 --- a/tests/test_security_api_key_header_optional.py +++ b/tests/test_security_api_key_header_optional.py @@ -58,17 +58,17 @@ def read_current_user(current_user: Optional[User] = Depends(get_current_user)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_api_key(): response = client.get("/users/me", headers={"key": "secret"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "secret"} def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_security_api_key_query.py b/tests/test_security_api_key_query.py index 228f2a7a3baf9..4844c65e22ad4 100644 --- a/tests/test_security_api_key_query.py +++ b/tests/test_security_api_key_query.py @@ -52,17 +52,17 @@ def read_current_user(current_user: User = Depends(get_current_user)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_api_key(): response = client.get("/users/me?key=secret") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "secret"} def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403 + assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} diff --git a/tests/test_security_api_key_query_optional.py b/tests/test_security_api_key_query_optional.py index ddbdd55671edd..9339b7b3aa0c0 100644 --- a/tests/test_security_api_key_query_optional.py +++ b/tests/test_security_api_key_query_optional.py @@ -58,17 +58,17 @@ def read_current_user(current_user: Optional[User] = Depends(get_current_user)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_api_key(): response = client.get("/users/me?key=secret") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "secret"} def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index 7e2c191ef1037..89471627968fa 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -40,17 +40,17 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_http_base(): response = client.get("/users/me", headers={"Authorization": "Other foobar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"scheme": "Other", "credentials": "foobar"} def test_security_http_base_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403 + assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} diff --git a/tests/test_security_http_base_optional.py b/tests/test_security_http_base_optional.py index 4cc757dfb43bd..5a50f9b88c54d 100644 --- a/tests/test_security_http_base_optional.py +++ b/tests/test_security_http_base_optional.py @@ -46,17 +46,17 @@ def read_current_user( def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_http_base(): response = client.get("/users/me", headers={"Authorization": "Other foobar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"scheme": "Other", "credentials": "foobar"} def test_security_http_base_no_credentials(): response = client.get("/users/me") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py index b80fadff312e5..289bd5c74cfe5 100644 --- a/tests/test_security_http_basic_optional.py +++ b/tests/test_security_http_basic_optional.py @@ -46,20 +46,20 @@ def read_current_user(credentials: Optional[HTTPBasicCredentials] = Security(sec def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_http_basic(): auth = HTTPBasicAuth(username="john", password="secret") response = client.get("/users/me", auth=auth) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "john", "password": "secret"} def test_security_http_basic_no_credentials(): response = client.get("/users/me") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} @@ -67,7 +67,7 @@ def test_security_http_basic_invalid_credentials(): response = client.get( "/users/me", headers={"Authorization": "Basic notabase64token"} ) - assert response.status_code == 401 + assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" assert response.json() == {"detail": "Invalid authentication credentials"} @@ -76,6 +76,6 @@ def test_security_http_basic_non_basic_credentials(): payload = b64encode(b"johnsecret").decode("ascii") auth_header = f"Basic {payload}" response = client.get("/users/me", headers={"Authorization": auth_header}) - assert response.status_code == 401 + assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" assert response.json() == {"detail": "Invalid authentication credentials"} diff --git a/tests/test_security_http_basic_realm.py b/tests/test_security_http_basic_realm.py index 117c9ef0244d2..54867c2e01b57 100644 --- a/tests/test_security_http_basic_realm.py +++ b/tests/test_security_http_basic_realm.py @@ -43,21 +43,21 @@ def read_current_user(credentials: HTTPBasicCredentials = Security(security)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_http_basic(): auth = HTTPBasicAuth(username="john", password="secret") response = client.get("/users/me", auth=auth) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "john", "password": "secret"} def test_security_http_basic_no_credentials(): response = client.get("/users/me") assert response.json() == {"detail": "Not authenticated"} - assert response.status_code == 401 + assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' @@ -65,7 +65,7 @@ def test_security_http_basic_invalid_credentials(): response = client.get( "/users/me", headers={"Authorization": "Basic notabase64token"} ) - assert response.status_code == 401 + assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' assert response.json() == {"detail": "Invalid authentication credentials"} @@ -74,6 +74,6 @@ def test_security_http_basic_non_basic_credentials(): payload = b64encode(b"johnsecret").decode("ascii") auth_header = f"Basic {payload}" response = client.get("/users/me", headers={"Authorization": auth_header}) - assert response.status_code == 401 + assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' assert response.json() == {"detail": "Invalid authentication credentials"} diff --git a/tests/test_security_http_bearer.py b/tests/test_security_http_bearer.py index a05fbf4472b0d..39d8c84029d87 100644 --- a/tests/test_security_http_bearer.py +++ b/tests/test_security_http_bearer.py @@ -40,23 +40,23 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_http_bearer(): response = client.get("/users/me", headers={"Authorization": "Bearer foobar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"scheme": "Bearer", "credentials": "foobar"} def test_security_http_bearer_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403 + assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) - assert response.status_code == 403 + assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} diff --git a/tests/test_security_http_bearer_optional.py b/tests/test_security_http_bearer_optional.py index f1f202abb61dd..2e7dfb8a4e7c4 100644 --- a/tests/test_security_http_bearer_optional.py +++ b/tests/test_security_http_bearer_optional.py @@ -46,23 +46,23 @@ def read_current_user( def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_http_bearer(): response = client.get("/users/me", headers={"Authorization": "Bearer foobar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"scheme": "Bearer", "credentials": "foobar"} def test_security_http_bearer_no_credentials(): response = client.get("/users/me") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_security_http_digest.py b/tests/test_security_http_digest.py index e6b9919916bfc..8388824ff2bde 100644 --- a/tests/test_security_http_digest.py +++ b/tests/test_security_http_digest.py @@ -40,19 +40,19 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_http_digest(): response = client.get("/users/me", headers={"Authorization": "Digest foobar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"scheme": "Digest", "credentials": "foobar"} def test_security_http_digest_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403 + assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} @@ -60,5 +60,5 @@ def test_security_http_digest_incorrect_scheme_credentials(): response = client.get( "/users/me", headers={"Authorization": "Other invalidauthorization"} ) - assert response.status_code == 403 + assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} diff --git a/tests/test_security_http_digest_optional.py b/tests/test_security_http_digest_optional.py index 83c4539351fd0..2177b819f3b26 100644 --- a/tests/test_security_http_digest_optional.py +++ b/tests/test_security_http_digest_optional.py @@ -46,19 +46,19 @@ def read_current_user( def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_http_digest(): response = client.get("/users/me", headers={"Authorization": "Digest foobar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"scheme": "Digest", "credentials": "foobar"} def test_security_http_digest_no_credentials(): response = client.get("/users/me") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} @@ -66,5 +66,5 @@ def test_security_http_digest_incorrect_scheme_credentials(): response = client.get( "/users/me", headers={"Authorization": "Other invalidauthorization"} ) - assert response.status_code == 403 + assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 3f96846d48c67..266dab6e5a7b9 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -156,25 +156,25 @@ def read_current_user(current_user: "User" = Depends(get_current_user)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "Bearer footokenbar"} def test_security_oauth2_password_other_header(): response = client.get("/users/me", headers={"Authorization": "Other footokenbar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "Other footokenbar"} def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403 + assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} diff --git a/tests/test_security_oauth2_authorization_code_bearer.py b/tests/test_security_oauth2_authorization_code_bearer.py index ab088d67c7973..3e0155e1fa9f0 100644 --- a/tests/test_security_oauth2_authorization_code_bearer.py +++ b/tests/test_security_oauth2_authorization_code_bearer.py @@ -55,23 +55,23 @@ async def read_items(token: Optional[str] = Security(oauth2_scheme)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_no_token(): response = client.get("/items") - assert response.status_code == 401 + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} def test_incorrect_token(): response = client.get("/items", headers={"Authorization": "Non-existent testtoken"}) - assert response.status_code == 401 + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} def test_token(): response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"token": "testtoken"} diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index 8732dd03701dc..06967bd766081 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -160,25 +160,25 @@ def read_current_user(current_user: Optional[User] = Depends(get_current_user)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "Bearer footokenbar"} def test_security_oauth2_password_other_header(): response = client.get("/users/me", headers={"Authorization": "Other footokenbar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "Other footokenbar"} def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_security_oauth2_password_bearer_optional.py b/tests/test_security_oauth2_password_bearer_optional.py index 0c36796aef8af..3d6637d4a5a6d 100644 --- a/tests/test_security_oauth2_password_bearer_optional.py +++ b/tests/test_security_oauth2_password_bearer_optional.py @@ -49,23 +49,23 @@ async def read_items(token: Optional[str] = Security(oauth2_scheme)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_no_token(): response = client.get("/items") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} def test_token(): response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"token": "testtoken"} def test_incorrect_token(): response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_security_openid_connect.py b/tests/test_security_openid_connect.py index 07596ecfe0214..8203961bedb01 100644 --- a/tests/test_security_openid_connect.py +++ b/tests/test_security_openid_connect.py @@ -52,23 +52,23 @@ def read_current_user(current_user: User = Depends(get_current_user)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "Bearer footokenbar"} def test_security_oauth2_password_other_header(): response = client.get("/users/me", headers={"Authorization": "Other footokenbar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "Other footokenbar"} def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403 + assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} diff --git a/tests/test_security_openid_connect_optional.py b/tests/test_security_openid_connect_optional.py index 8aff16f282250..4577dfebb02f4 100644 --- a/tests/test_security_openid_connect_optional.py +++ b/tests/test_security_openid_connect_optional.py @@ -58,23 +58,23 @@ def read_current_user(current_user: Optional[User] = Depends(get_current_user)): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "Bearer footokenbar"} def test_security_oauth2_password_other_header(): response = client.get("/users/me", headers={"Authorization": "Other footokenbar"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"username": "Other footokenbar"} def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_starlette_exception.py b/tests/test_starlette_exception.py index 6cd9b8208b034..c83af6ca5b5f5 100644 --- a/tests/test_starlette_exception.py +++ b/tests/test_starlette_exception.py @@ -126,31 +126,31 @@ async def create_item(item_id: str): def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get_item(): response = client.get("/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item": "The Foo Wrestlers"} def test_get_item_not_found(): response = client.get("/items/bar") - assert response.status_code == 404 + assert response.status_code == 404, response.text assert response.headers.get("x-error") == "Some custom header" assert response.json() == {"detail": "Item not found"} def test_get_starlette_item(): response = client.get("/starlette-items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item": "The Foo Wrestlers"} def test_get_starlette_item_not_found(): response = client.get("/starlette-items/bar") - assert response.status_code == 404 + assert response.status_code == 404, response.text assert response.headers.get("x-error") is None assert response.json() == {"detail": "Item not found"} diff --git a/tests/test_starlette_urlconvertors.py b/tests/test_starlette_urlconvertors.py index 97fa41f2986c2..1ea22116c8702 100644 --- a/tests/test_starlette_urlconvertors.py +++ b/tests/test_starlette_urlconvertors.py @@ -25,7 +25,7 @@ def path_convertor(param: str = Path(...)): def test_route_converters_int(): # Test integer conversion response = client.get("/int/5") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"int": 5} assert app.url_path_for("int_convertor", param=5) == "/int/5" @@ -33,7 +33,7 @@ def test_route_converters_int(): def test_route_converters_float(): # Test float conversion response = client.get("/float/25.5") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"float": 25.5} assert app.url_path_for("float_convertor", param=25.5) == "/float/25.5" @@ -41,7 +41,7 @@ def test_route_converters_float(): def test_route_converters_path(): # Test path conversion response = client.get("/path/some/example") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"path": "some/example"} diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index c5a22e8b7c5ce..3de3cea9939d6 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -221,7 +221,7 @@ def test_get(): response = client.post( "/invoices/", json={"id": "fooinvoice", "customer": "John", "total": 5.3} ) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Invoice received"} diff --git a/tests/test_swagger_ui_init_oauth.py b/tests/test_swagger_ui_init_oauth.py index fc6b265dbcb3a..371cd72fb7529 100644 --- a/tests/test_swagger_ui_init_oauth.py +++ b/tests/test_swagger_ui_init_oauth.py @@ -16,7 +16,7 @@ async def read_items(): def test_swagger_ui(): response = client.get("/docs") - assert response.status_code == 200 + assert response.status_code == 200, response.text print(response.text) assert f"ui.initOAuth" in response.text assert f'"appName": "The Predendapp"' in response.text diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial001.py b/tests/test_tutorial/test_additional_responses/test_tutorial001.py index b6ba63f5ab6e8..82dec78adaa4a 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial001.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial001.py @@ -100,17 +100,17 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_path_operation(): response = client.get("/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"id": "foo", "value": "there goes my hero"} def test_path_operation_not_found(): response = client.get("/items/bar") - assert response.status_code == 404 + assert response.status_code == 404, response.text assert response.json() == {"message": "Item not found"} diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index 998070b3d66c4..274c95663f256 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -96,20 +96,20 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_path_operation(): response = client.get("/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"id": "foo", "value": "there goes my hero"} def test_path_operation_img(): shutil.copy("./docs/en/docs/img/favicon.png", "./image.png") response = client.get("/items/foo?img=1") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.headers["Content-Type"] == "image/png" assert len(response.content) os.remove("./image.png") diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial003.py b/tests/test_tutorial/test_additional_responses/test_tutorial003.py index 52144f7ae29ec..6787f9d29afff 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial003.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial003.py @@ -101,17 +101,17 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_path_operation(): response = client.get("/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"id": "foo", "value": "there goes my hero"} def test_path_operation_not_found(): response = client.get("/items/bar") - assert response.status_code == 404 + assert response.status_code == 404, response.text assert response.json() == {"message": "Item not found"} diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index 11f1227e45a68..d7456630ef477 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -99,20 +99,20 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_path_operation(): response = client.get("/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"id": "foo", "value": "there goes my hero"} def test_path_operation_img(): shutil.copy("./docs/en/docs/img/favicon.png", "./image.png") response = client.get("/items/foo?img=1") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.headers["Content-Type"] == "image/png" assert len(response.content) os.remove("./image.png") diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001.py index ccfe343dc46c1..e2131c71ba5c1 100644 --- a/tests/test_tutorial/test_additional_status_codes/test_tutorial001.py +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001.py @@ -7,11 +7,11 @@ def test_update(): response = client.put("/items/foo", json={"name": "Wrestlers"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"name": "Wrestlers", "size": None} def test_create(): response = client.put("/items/red", json={"name": "Chillies"}) - assert response.status_code == 201 + assert response.status_code == 201, response.text assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_advanced_middleware/test_tutorial001.py b/tests/test_tutorial/test_advanced_middleware/test_tutorial001.py index f49f50f0cd506..fb5058bcd3e4b 100644 --- a/tests/test_tutorial/test_advanced_middleware/test_tutorial001.py +++ b/tests/test_tutorial/test_advanced_middleware/test_tutorial001.py @@ -6,9 +6,9 @@ def test_middleware(): client = TestClient(app, base_url="https://testserver") response = client.get("/") - assert response.status_code == 200 + assert response.status_code == 200, response.text client = TestClient(app) response = client.get("/", allow_redirects=False) - assert response.status_code == 307 + assert response.status_code == 307, response.text assert response.headers["location"] == "https://testserver/" diff --git a/tests/test_tutorial/test_advanced_middleware/test_tutorial002.py b/tests/test_tutorial/test_advanced_middleware/test_tutorial002.py index da1d60ba8d95d..bac2dea8ba5a1 100644 --- a/tests/test_tutorial/test_advanced_middleware/test_tutorial002.py +++ b/tests/test_tutorial/test_advanced_middleware/test_tutorial002.py @@ -6,10 +6,10 @@ def test_middleware(): client = TestClient(app, base_url="http://example.com") response = client.get("/") - assert response.status_code == 200 + assert response.status_code == 200, response.text client = TestClient(app, base_url="http://subdomain.example.com") response = client.get("/") - assert response.status_code == 200 + assert response.status_code == 200, response.text client = TestClient(app, base_url="http://invalidhost") response = client.get("/") - assert response.status_code == 400 + assert response.status_code == 400, response.text diff --git a/tests/test_tutorial/test_advanced_middleware/test_tutorial003.py b/tests/test_tutorial/test_advanced_middleware/test_tutorial003.py index 0d482b81c2338..230a5742df342 100644 --- a/tests/test_tutorial/test_advanced_middleware/test_tutorial003.py +++ b/tests/test_tutorial/test_advanced_middleware/test_tutorial003.py @@ -14,9 +14,9 @@ async def large(): def test_middleware(): response = client.get("/large", headers={"accept-encoding": "gzip"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.text == "x" * 4000 assert response.headers["Content-Encoding"] == "gzip" assert int(response.headers["Content-Length"]) < 4000 response = client.get("/") - assert response.status_code == 200 + assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py index 553f42c2f4526..17214f874ca02 100644 --- a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py @@ -113,7 +113,7 @@ def test_openapi_schema(): with TestClient(app) as client: response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema @@ -121,11 +121,11 @@ def test_create_read(): with TestClient(app) as client: note = {"text": "Foo bar", "completed": False} response = client.post("/notes/", json=note) - assert response.status_code == 200 + assert response.status_code == 200, response.text data = response.json() assert data["text"] == note["text"] assert data["completed"] == note["completed"] assert "id" in data response = client.get(f"/notes/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert data in response.json() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial001.py b/tests/test_tutorial/test_background_tasks/test_tutorial001.py index a1381a9d5a9ef..8c8628982542a 100644 --- a/tests/test_tutorial/test_background_tasks/test_tutorial001.py +++ b/tests/test_tutorial/test_background_tasks/test_tutorial001.py @@ -13,7 +13,7 @@ def test(): if log.is_file(): os.remove(log) # pragma: no cover response = client.post("/send-notification/foo@example.com") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"message": "Notification sent in the background"} with open("./log.txt") as f: assert "notification for foo@example.com: some notification" in f.read() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002.py b/tests/test_tutorial/test_background_tasks/test_tutorial002.py index 861e83aef081b..d3ad37271000a 100644 --- a/tests/test_tutorial/test_background_tasks/test_tutorial002.py +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002.py @@ -13,7 +13,7 @@ def test(): if log.is_file(): os.remove(log) # pragma: no cover response = client.post("/send-notification/foo@example.com?q=some-query") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"message": "Message sent"} with open("./log.txt") as f: assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index b60bd6499c385..1a90ba05bb5de 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -270,7 +270,7 @@ def test_get_path(path, expected_status, expected_response, headers): def test_put_no_header(): response = client.put("/items/foo") - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == { "detail": [ { @@ -284,17 +284,17 @@ def test_put_no_header(): def test_put_invalid_header(): response = client.put("/items/foo", headers={"X-Token": "invalid"}) - assert response.status_code == 400 + assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} def test_put(): response = client.put("/items/foo", headers={"X-Token": "fake-super-secret-token"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": "foo", "name": "The Fighters"} def test_put_forbidden(): response = client.put("/items/bar", headers={"X-Token": "fake-super-secret-token"}) - assert response.status_code == 403 + assert response.status_code == 403, response.text assert response.json() == {"detail": "You can only update the item: foo"} diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index f3aded151eb3b..293981a09d8f4 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -85,7 +85,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema @@ -176,5 +176,5 @@ def test_post_body(path, body, expected_status, expected_response): def test_post_broken_body(): response = client.post("/items/", data={"name": "Foo", "price": 50.5}) - assert response.status_code == 400 + assert response.status_code == 400, response.text assert response.json() == {"detail": "There was an error parsing the body"} diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001.py b/tests/test_tutorial/test_body_fields/test_tutorial001.py index a437c62ec17b1..b5e95a3cf6edf 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -120,7 +120,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index 6241396f361c8..19972668dc74f 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -103,7 +103,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py index d93da39fce182..d228f9f67f702 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -114,7 +114,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py index 7ea7e5c74f63a..8a9f395331082 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -77,21 +77,21 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_post_body(): data = {"2": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == data def test_post_invalid_body(): data = {"foo": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == { "detail": [ { diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index d0777655601f7..b5f77af120184 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -133,13 +133,13 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get(): response = client.get("/items/baz") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == { "name": "Baz", "description": None, diff --git a/tests/test_tutorial/test_cors/test_tutorial001.py b/tests/test_tutorial/test_cors/test_tutorial001.py index dc3078e481417..e3f6b73501d4b 100644 --- a/tests/test_tutorial/test_cors/test_tutorial001.py +++ b/tests/test_tutorial/test_cors/test_tutorial001.py @@ -12,7 +12,7 @@ def test_cors(): "Access-Control-Request-Headers": "X-Example", } response = client.options("/", headers=headers) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.text == "OK" assert ( response.headers["access-control-allow-origin"] @@ -23,7 +23,7 @@ def test_cors(): # Test standard response headers = {"Origin": "https://localhost.tiangolo.com"} response = client.get("/", headers=headers) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"message": "Hello World"} assert ( response.headers["access-control-allow-origin"] @@ -32,6 +32,6 @@ def test_cors(): # Test non-CORS response response = client.get("/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"message": "Hello World"} assert "access-control-allow-origin" not in response.headers diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001b.py b/tests/test_tutorial/test_custom_response/test_tutorial001b.py index 08f372897d567..b061ef20bb950 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001b.py @@ -26,11 +26,11 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get_custom_response(): response = client.get("/items/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] diff --git a/tests/test_tutorial/test_custom_response/test_tutorial004.py b/tests/test_tutorial/test_custom_response/test_tutorial004.py index c9d120058699a..9cc27705f92d9 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial004.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial004.py @@ -37,11 +37,11 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get_custom_response(): response = client.get("/items/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.text == html_contents diff --git a/tests/test_tutorial/test_custom_response/test_tutorial005.py b/tests/test_tutorial/test_custom_response/test_tutorial005.py index 4755408e2ccf0..d5a74308c3682 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial005.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial005.py @@ -26,11 +26,11 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get(): response = client.get("/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.text == "Hello World" diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006.py b/tests/test_tutorial/test_custom_response/test_tutorial006.py index 2a90568c46f75..07daf2cf2b558 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006.py @@ -7,5 +7,5 @@ def test_get(): response = client.get("/typer", allow_redirects=False) - assert response.status_code == 307 + assert response.status_code == 307, response.text assert response.headers["location"] == "https://typer.tiangolo.com" diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial001.py index 353e5c8ab7b0d..73d65cf50c73b 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001.py @@ -128,7 +128,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial004.py index 7e41bbd3b503c..bf62e9cbc3687 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004.py @@ -86,7 +86,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py index 62f13f07a5c95..f3279bfe64e92 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -79,13 +79,13 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get_no_headers(): response = client.get("/items/") - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == { "detail": [ { @@ -104,7 +104,7 @@ def test_get_no_headers(): def test_get_invalid_one_header(): response = client.get("/items/", headers={"X-Token": "invalid"}) - assert response.status_code == 400 + assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} @@ -112,7 +112,7 @@ def test_get_invalid_second_header(): response = client.get( "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} ) - assert response.status_code == 400 + assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Key header invalid"} @@ -124,5 +124,5 @@ def test_get_valid_headers(): "X-Key": "fake-super-secret-key", }, ) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] diff --git a/tests/test_tutorial/test_events/test_tutorial001.py b/tests/test_tutorial/test_events/test_tutorial001.py index ebd3550af5bc7..2ab89b069e1cc 100644 --- a/tests/test_tutorial/test_events/test_tutorial001.py +++ b/tests/test_tutorial/test_events/test_tutorial001.py @@ -72,8 +72,8 @@ def test_events(): with TestClient(app) as client: response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema response = client.get("/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"name": "Fighters"} diff --git a/tests/test_tutorial/test_events/test_tutorial002.py b/tests/test_tutorial/test_events/test_tutorial002.py index bb18802473866..c06ea3f1cbb13 100644 --- a/tests/test_tutorial/test_events/test_tutorial002.py +++ b/tests/test_tutorial/test_events/test_tutorial002.py @@ -25,10 +25,10 @@ def test_events(): with TestClient(app) as client: response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema response = client.get("/items/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"name": "Foo"}] with open("log.txt") as log: assert "Application shutdown" in log.read() diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py index c0cb8ddaa6fbf..88997f59d6db6 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py @@ -31,14 +31,14 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test(): response = client.get("/items/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"name": "Foo"}] diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial002.py b/tests/test_tutorial/test_extending_openapi/test_tutorial002.py index 592fd84209016..06fb62f85249f 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial002.py +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial002.py @@ -19,24 +19,24 @@ def client(): def test_swagger_ui_html(client: TestClient): response = client.get("/docs") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert "/static/swagger-ui-bundle.js" in response.text assert "/static/swagger-ui.css" in response.text def test_swagger_ui_oauth2_redirect_html(client: TestClient): response = client.get("/docs/oauth2-redirect") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert "window.opener.swaggerUIRedirectOauth2" in response.text def test_redoc_html(client: TestClient): response = client.get("/redoc") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert "/static/redoc.standalone.js" in response.text def test_api(client: TestClient): response = client.get("/users/john") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json()["message"] == "Hello john" diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py index 9695bce26d7bc..72de6308bec5e 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -113,7 +113,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema @@ -134,5 +134,5 @@ def test_extra_types(): } ) response = client.put(f"/items/{item_id}", json=data) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == expected_response diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py index 2662a5710011f..150b899c6df37 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -102,13 +102,13 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get_car(): response = client.get("/items/item1") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == { "description": "All my friends drive a low rider", "type": "car", @@ -117,7 +117,7 @@ def test_get_car(): def test_get_plane(): response = client.get("/items/item2") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == { "description": "Music is my aeroplane, it's my aeroplane", "type": "plane", diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004.py b/tests/test_tutorial/test_extra_models/test_tutorial004.py index cb3bb33602ee3..08f859656cb83 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial004.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial004.py @@ -47,13 +47,13 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get_items(): response = client.get("/items/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [ {"name": "Foo", "description": "There comes my hero"}, {"name": "Red", "description": "It's my aeroplane"}, diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005.py b/tests/test_tutorial/test_extra_models/test_tutorial005.py index 413c147758cd7..bce5efecef293 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial005.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial005.py @@ -34,11 +34,11 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get_items(): response = client.get("/keyword-weights/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"foo": 2.3, "bar": 3.4} diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial001.py b/tests/test_tutorial/test_handling_errors/test_tutorial001.py index da8ee9864a3c4..5a80bc5f60787 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial001.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial001.py @@ -73,18 +73,18 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get_item(): response = client.get("/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item": "The Foo Wrestlers"} def test_get_item_not_found(): response = client.get("/items/bar") - assert response.status_code == 404 + assert response.status_code == 404, response.text assert response.headers.get("x-error") is None assert response.json() == {"detail": "Item not found"} diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial002.py b/tests/test_tutorial/test_handling_errors/test_tutorial002.py index f822ae0a1b707..4ffce6b2c1453 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial002.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial002.py @@ -73,18 +73,18 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get_item_header(): response = client.get("/items-header/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item": "The Foo Wrestlers"} def test_get_item_not_found_header(): response = client.get("/items-header/bar") - assert response.status_code == 404 + assert response.status_code == 404, response.text assert response.headers.get("x-error") == "There goes my error" assert response.json() == {"detail": "Item not found"} diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial003.py b/tests/test_tutorial/test_handling_errors/test_tutorial003.py index f63dc10d2bbd1..48da5db7cef58 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial003.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial003.py @@ -73,19 +73,19 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get(): response = client.get("/unicorns/shinny") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"unicorn_name": "shinny"} def test_get_exception(): response = client.get("/unicorns/yolo") - assert response.status_code == 418 + assert response.status_code == 418, response.text assert response.json() == { "message": "Oops! yolo did something. There goes a rainbow..." } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index eb8d629ece567..58b97c0034c4b 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -73,13 +73,13 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get_validation_error(): response = client.get("/items/foo") - assert response.status_code == 400 + assert response.status_code == 400, response.text validation_error_str_lines = [ b"1 validation error for Request", b"path -> item_id", @@ -90,11 +90,11 @@ def test_get_validation_error(): def test_get_http_error(): response = client.get("/items/3") - assert response.status_code == 418 + assert response.status_code == 418, response.text assert response.content == b"Nope! I don't like 3." def test_get(): response = client.get("/items/2") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": 2} diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py index f62c05ae5f233..f974466d15979 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py @@ -82,13 +82,13 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_post_validation_error(): response = client.post("/items/", json={"title": "towel", "size": "XL"}) - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == { "detail": [ { @@ -104,5 +104,5 @@ def test_post_validation_error(): def test_post(): data = {"title": "towel", "size": 5} response = client.post("/items/", json=data) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == data diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py index 87d9df4e17706..2b945fe908ac3 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial006.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial006.py @@ -73,13 +73,13 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get_validation_error(): response = client.get("/items/foo") - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == { "detail": [ { @@ -93,11 +93,11 @@ def test_get_validation_error(): def test_get_http_error(): response = client.get("/items/3") - assert response.status_code == 418 + assert response.status_code == 418, response.text assert response.json() == {"detail": "Nope! I don't like 3."} def test_get(): response = client.get("/items/2") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": 2} diff --git a/tests/test_tutorial/test_metadata/test_tutorial001.py b/tests/test_tutorial/test_metadata/test_tutorial001.py index 0cef510b30cb9..5f00d39570af0 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial001.py +++ b/tests/test_tutorial/test_metadata/test_tutorial001.py @@ -30,11 +30,11 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_items(): response = client.get("/items/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"name": "Foo"}] diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index 8340542f6fd52..337e703d20910 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -165,7 +165,7 @@ def test_get(): response = client.post( "/invoices/", json={"id": "fooinvoice", "customer": "John", "total": 5.3} ) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"msg": "Invoice received"} diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py index 9ec8bf2e9fc37..ca94ff1f14290 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py @@ -26,11 +26,11 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get(): response = client.get("/items/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py index 036ea138a7fec..1aa001ad4f4e9 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py @@ -26,11 +26,11 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get(): response = client.get("/items/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py index f5c26c767ab0b..16559a33e7cf2 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py @@ -13,11 +13,11 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_get(): response = client.get("/items/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index 099710e926015..f092703dfb783 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -96,13 +96,13 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_query_params_str_validations(): response = client.post("/items/", json={"name": "Foo", "price": 42}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == { "name": "Foo", "price": 42, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index b78263c8e6643..107fae09c29a7 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -96,13 +96,13 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_query_params_str_validations(): response = client.post("/items/", json={"name": "Foo", "price": 42}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == { "name": "Foo", "price": 42, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py index 5534cb5709b2c..bfcd4541183b9 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py @@ -55,7 +55,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_tutorial/test_path_params/test_tutorial004.py b/tests/test_tutorial/test_path_params/test_tutorial004.py index 52edbf1b57ea0..a3343744f524e 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial004.py +++ b/tests/test_tutorial/test_path_params/test_tutorial004.py @@ -73,19 +73,19 @@ def test_openapi(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_file_path(): response = client.get("/files/home/johndoe/myfile.txt") print(response.content) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"file_path": "home/johndoe/myfile.txt"} def test_root_file_path(): response = client.get("/files//home/johndoe/myfile.txt") print(response.content) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"file_path": "/home/johndoe/myfile.txt"} diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index b7eac1fd25ba3..bdf0a9e5977ee 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -78,7 +78,7 @@ def test_openapi(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_tutorial/test_query_params/test_tutorial007.py b/tests/test_tutorial/test_query_params/test_tutorial007.py index 3dc4f1803c5dc..335c103a15a15 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial007.py +++ b/tests/test_tutorial/test_query_params/test_tutorial007.py @@ -79,17 +79,17 @@ def test_openapi(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_read_item(): response = client.get("/items/foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": "foo", "limit": None} def test_read_item_query(): response = client.get("/items/foo?limit=5") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item_id": "foo", "limit": 5} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py index cce30d8a002fd..8bdba00b771fb 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py @@ -83,7 +83,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py index a498bca824a6a..fb37f4be211ca 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py @@ -77,19 +77,19 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_multi_query_values(): url = "/items/?q=foo&q=bar" response = client.get(url) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"q": ["foo", "bar"]} def test_query_no_values(): url = "/items/" response = client.get(url) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py index f7fcfe5e24cda..c05d8ac217082 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py @@ -78,19 +78,19 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_default_query_values(): url = "/items/" response = client.get(url) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"q": ["foo", "bar"]} def test_multi_query_values(): url = "/items/?q=baz&q=foobar" response = client.get(url) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"q": ["baz", "foobar"]} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py index d645c4a697aa2..7b1eab2b7def2 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py @@ -73,19 +73,19 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_multi_query_values(): url = "/items/?q=foo&q=bar" response = client.get(url) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"q": ["foo", "bar"]} def test_query_no_values(): url = "/items/" response = client.get(url) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index fdf4eb25455df..9aea331f32a37 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -125,7 +125,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema @@ -142,13 +142,13 @@ def test_openapi_schema(): def test_post_form_no_body(): response = client.post("/files/") - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == file_required def test_post_body_json(): response = client.post("/files/", json={"file": "Foo"}) - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == file_required @@ -159,7 +159,7 @@ def test_post_file(tmpdir): client = TestClient(app) response = client.post("/files/", files={"file": open(path, "rb")}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"file_size": 14} @@ -171,7 +171,7 @@ def test_post_large_file(tmpdir): client = TestClient(app) response = client.post("/files/", files={"file": open(path, "rb")}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"file_size": default_pydantic_max_size + 1} @@ -182,5 +182,5 @@ def test_post_upload_file(tmpdir): client = TestClient(app) response = client.post("/uploadfile/", files={"file": open(path, "rb")}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index 811e0f1c730e5..24e9eae2f5b9e 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -145,7 +145,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema @@ -162,13 +162,13 @@ def test_openapi_schema(): def test_post_form_no_body(): response = client.post("/files/") - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == file_required def test_post_body_json(): response = client.post("/files/", json={"file": "Foo"}) - assert response.status_code == 422 + assert response.status_code == 422, response.text assert response.json() == file_required @@ -188,7 +188,7 @@ def test_post_files(tmpdir): ("files", ("test2.txt", open(path2, "rb"))), ), ) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"file_sizes": [14, 15]} @@ -208,12 +208,12 @@ def test_post_upload_file(tmpdir): ("files", ("test2.txt", open(path2, "rb"))), ), ) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"filenames": ["test.txt", "test2.txt"]} def test_get_root(): client = TestClient(app) response = client.get("/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert b"Item ID: foo" in response.content response = client.get("/static/styles.css") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert b"color: green;" in response.content shutil.rmtree("./templates") shutil.rmtree("./static") diff --git a/tests/test_tutorial/test_testing/test_main.py b/tests/test_tutorial/test_testing/test_main.py index 36e8069a5263f..6ef800f6ed44e 100644 --- a/tests/test_tutorial/test_testing/test_main.py +++ b/tests/test_tutorial/test_testing/test_main.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_tutorial/test_testing/test_tutorial001.py b/tests/test_tutorial/test_testing/test_tutorial001.py index adc6928c63b4e..66863b94512f5 100644 --- a/tests/test_tutorial/test_testing/test_tutorial001.py +++ b/tests/test_tutorial/test_testing/test_tutorial001.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == openapi_schema diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001.py index 093c6499a8c0e..dccace562e2d0 100644 --- a/tests/test_tutorial/test_testing_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001.py @@ -21,7 +21,7 @@ def test_override_in_items_with_params_run(): def test_override_in_users(): response = client.get("/users/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == { "message": "Hello Users!", "params": {"q": None, "skip": 5, "limit": 10}, @@ -30,7 +30,7 @@ def test_override_in_users(): def test_override_in_users_with_q(): response = client.get("/users/?q=foo") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == { "message": "Hello Users!", "params": {"q": "foo", "skip": 5, "limit": 10}, @@ -39,7 +39,7 @@ def test_override_in_users_with_q(): def test_override_in_users_with_params(): response = client.get("/users/?q=foo&skip=100&limit=200") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == { "message": "Hello Users!", "params": {"q": "foo", "skip": 5, "limit": 10}, @@ -49,7 +49,7 @@ def test_override_in_users_with_params(): def test_normal_app(): app.dependency_overrides = None response = client.get("/items/?q=foo&skip=100&limit=200") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == { "message": "Hello Items!", "params": {"q": "foo", "skip": 100, "limit": 200}, diff --git a/tests/test_tutorial/test_websockets/test_tutorial001.py b/tests/test_tutorial/test_websockets/test_tutorial001.py index b30ecf477bead..6c715b19495a2 100644 --- a/tests/test_tutorial/test_websockets/test_tutorial001.py +++ b/tests/test_tutorial/test_websockets/test_tutorial001.py @@ -8,7 +8,7 @@ def test_main(): response = client.get("/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert b"" in response.content diff --git a/tests/test_tutorial/test_websockets/test_tutorial002.py b/tests/test_tutorial/test_websockets/test_tutorial002.py index 9e3e45edb4193..640691cd3e169 100644 --- a/tests/test_tutorial/test_websockets/test_tutorial002.py +++ b/tests/test_tutorial/test_websockets/test_tutorial002.py @@ -8,7 +8,7 @@ def test_main(): response = client.get("/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert b"" in response.content diff --git a/tests/test_tutorial/test_wsgi/test_tutorial001.py b/tests/test_tutorial/test_wsgi/test_tutorial001.py index e5390fced45b7..8eee4d465ae30 100644 --- a/tests/test_tutorial/test_wsgi/test_tutorial001.py +++ b/tests/test_tutorial/test_wsgi/test_tutorial001.py @@ -7,11 +7,11 @@ def test_flask(): response = client.get("/v1/") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.text == "Hello, World from Flask!" def test_app(): response = client.get("/v2") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"message": "Hello World"} diff --git a/tests/test_union_body.py b/tests/test_union_body.py index e043041708119..d1dfd5efbad2c 100644 --- a/tests/test_union_body.py +++ b/tests/test_union_body.py @@ -108,17 +108,17 @@ def save_union_body(item: Union[OtherItem, Item]): def test_item_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == item_openapi_schema def test_post_other_item(): response = client.post("/items/", json={"price": 100}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item": {"price": 100}} def test_post_item(): response = client.post("/items/", json={"name": "Foo"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item": {"name": "Foo"}} diff --git a/tests/test_union_inherited_body.py b/tests/test_union_inherited_body.py index b901f28e72fb6..a1a3f0ed5b5b7 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -121,19 +121,19 @@ def save_union_different_body(item: Union[ExtendedItem, Item]): @skip_py36 def test_inherited_item_openapi_schema(): response = client.get("/openapi.json") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == inherited_item_openapi_schema @skip_py36 def test_post_extended_item(): response = client.post("/items/", json={"name": "Foo", "age": 5}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item": {"name": "Foo", "age": 5}} @skip_py36 def test_post_item(): response = client.post("/items/", json={"name": "Foo"}) - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"item": {"name": "Foo"}} diff --git a/tests/test_validate_response_recursive.py b/tests/test_validate_response_recursive.py index 8b77ed14a9285..3a4b10e0ca9f4 100644 --- a/tests/test_validate_response_recursive.py +++ b/tests/test_validate_response_recursive.py @@ -56,14 +56,14 @@ def get_recursive_submodel(): def test_recursive(): response = client.get("/items/recursive") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == { "sub_items": [{"name": "subitem", "sub_items": []}], "name": "item", } response = client.get("/items/recursive-submodel") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == { "name": "item", "sub_items1": [ From a4405bbed27308b1e5aedd0477a38985fd4838c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Apr 2020 06:39:40 +0200 Subject: [PATCH 073/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2743f3f47df7a..432f68ca18415 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +* Improve test debugging by showing response text in failing tests. PR [#1222](https://github.com/tiangolo/fastapi/pull/1222) by [@samuelcolvin](https://github.com/samuelcolvin). + ## 0.54.0 * Fix grammatical mistakes in async docs. PR [#1188](https://github.com/tiangolo/fastapi/pull/1188) by [@mickeypash](https://github.com/mickeypash). From a46bbc54cd4565baa1e3a6ac5cc03fa09b6e3fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Apr 2020 07:41:53 +0200 Subject: [PATCH 074/153] :white_check_mark: Update database setup for tests (#1226) * :card_file_box: Update database setup for tests * :white_check_mark: Add pragmas and update db handling for tests --- .../test_sql_databases/test_sql_databases.py | 14 ++++++++++---- .../test_sql_databases_middleware.py | 15 +++++++++++---- .../test_testing_databases.py | 17 ++++++++++------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases.py b/tests/test_tutorial/test_sql_databases/test_sql_databases.py index d7b6e1b3afcb3..69092889ca32f 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases.py @@ -1,3 +1,4 @@ +import importlib from pathlib import Path import pytest @@ -283,13 +284,18 @@ @pytest.fixture(scope="module") def client(): + test_db = Path("./sql_app.db") + if test_db.is_file(): # pragma: nocover + test_db.unlink() # Import while creating the client to create the DB after starting the test session - from sql_databases.sql_app.main import app + from sql_databases.sql_app import main - test_db = Path("./sql_app.db") - with TestClient(app) as c: + # Ensure import side effects are re-executed + importlib.reload(main) + with TestClient(main.app) as c: yield c - test_db.unlink() + if test_db.is_file(): # pragma: nocover + test_db.unlink() def test_openapi_schema(client): diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py index d26a442b707b3..6b4ed567ed353 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py @@ -1,3 +1,4 @@ +import importlib from pathlib import Path import pytest @@ -283,13 +284,19 @@ @pytest.fixture(scope="module") def client(): + test_db = Path("./sql_app.db") + if test_db.is_file(): # pragma: nocover + test_db.unlink() # Import while creating the client to create the DB after starting the test session - from sql_databases.sql_app.alt_main import app + from sql_databases.sql_app import alt_main - test_db = Path("./sql_app.db") - with TestClient(app) as c: + # Ensure import side effects are re-executed + importlib.reload(alt_main) + + with TestClient(alt_main.app) as c: yield c - test_db.unlink() + if test_db.is_file(): # pragma: nocover + test_db.unlink() def test_openapi_schema(client): diff --git a/tests/test_tutorial/test_sql_databases/test_testing_databases.py b/tests/test_tutorial/test_sql_databases/test_testing_databases.py index fae66d3b87c1e..83a156b8509bd 100644 --- a/tests/test_tutorial/test_sql_databases/test_testing_databases.py +++ b/tests/test_tutorial/test_sql_databases/test_testing_databases.py @@ -1,13 +1,16 @@ +import importlib from pathlib import Path def test_testing_dbs(): + test_db = Path("./test.db") + if test_db.is_file(): # pragma: nocover + test_db.unlink() # Import while creating the client to create the DB after starting the test session - from sql_databases.sql_app.tests.test_sql_app import test_create_user + from sql_databases.sql_app.tests import test_sql_app - test_db = Path("./test.db") - app_db = Path("./sql_app.db") - test_create_user() - test_db.unlink() - if app_db.is_file(): # pragma: nocover - app_db.unlink() + # Ensure import side effects are re-executed + importlib.reload(test_sql_app) + test_sql_app.test_create_user() + if test_db.is_file(): # pragma: nocover + test_db.unlink() From 471d703611558d23049f9857bd7224c7cf20a476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Apr 2020 07:50:30 +0200 Subject: [PATCH 075/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 432f68ca18415..7247deb003899 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update database test setup. PR [#1226](https://github.com/tiangolo/fastapi/pull/1226). * Improve test debugging by showing response text in failing tests. PR [#1222](https://github.com/tiangolo/fastapi/pull/1222) by [@samuelcolvin](https://github.com/samuelcolvin). ## 0.54.0 From d4f3ca1c1b471a25634a7114f71a7cdd7a0ac18a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Apr 2020 07:51:26 +0200 Subject: [PATCH 076/153] :bookmark: Release 0.54.1 --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7247deb003899..70d524f0ed4b9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +## 0.54.1 + * Update database test setup. PR [#1226](https://github.com/tiangolo/fastapi/pull/1226). * Improve test debugging by showing response text in failing tests. PR [#1222](https://github.com/tiangolo/fastapi/pull/1222) by [@samuelcolvin](https://github.com/samuelcolvin). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index b2cfecfe73f82..2c2612f2d1417 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.54.0" +__version__ = "0.54.1" from starlette import status From a7b4c73663c6dc60b829cccb0fe0e2654184328f Mon Sep 17 00:00:00 2001 From: Camila Gutierrez Date: Sat, 11 Apr 2020 18:46:46 +0200 Subject: [PATCH 077/153] :memo: Add Spanish translation for the Features page (#1220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Spanish translation for the Features page * :pencil2: Fix small typos and wording nitpicks Co-authored-by: Sebastián Ramírez --- docs/es/docs/features.md | 202 +++++++++++++++++++++++++++++++++++++++ docs/es/mkdocs.yml | 1 + 2 files changed, 203 insertions(+) create mode 100644 docs/es/docs/features.md diff --git a/docs/es/docs/features.md b/docs/es/docs/features.md new file mode 100644 index 0000000000000..6207c46ca3157 --- /dev/null +++ b/docs/es/docs/features.md @@ -0,0 +1,202 @@ +# Características + +## Características de FastAPI + +**FastAPI** te provee lo siguiente: + +### Basado en estándares abiertos + +* OpenAPI para la creación de APIs, incluyendo declaraciones de path operations, parámetros, body requests, seguridad, etc. +* Documentación automática del modelo de datos con JSON Schema (dado que OpenAPI mismo está basado en JSON Schema). +* Diseñado alrededor de estos estándares después de un estudio meticuloso. En vez de ser una capa añadida a último momento. +* Esto también permite la **generación automática de código de cliente** para muchos lenguajes. + +### Documentación automática + +Documentación interactiva de la API e interfaces web de exploración. Hay múltiples opciones, dos incluídas por defecto, porque el framework está basado en OpenAPI. + +* Swagger UI, con exploración interactiva, llama y prueba tu API directamente desde tu navegador. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Documentación alternativa de la API con ReDoc. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Simplemente Python moderno + +Todo está basado en las declaraciones de tipo de **Python 3.6** estándar (gracias a Pydantic). No necesitas aprender una sintáxis nueva, solo Python moderno. + +Si necesitas un repaso de 2 minutos de cómo usar los tipos de Python (así no uses FastAPI) prueba el tutorial corto: [Python Types](python-types.md){.internal-link target=_blank}. + +Escribes Python estándar con tipos así: + +```Python +from typing import List, Dict +from datetime import date + +from pydantic import BaseModel + +# Declaras la variable como un str +# y obtienes soporte del editor dentro de la función +def main(user_id: str): + return user_id + + +# Un modelo de Pydantic +class User(BaseModel): + id: int + name: str + joined: date +``` + +Este puede ser usado como: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +!!! info + `**second_user_data` significa: + + Pasa las keys y los valores del dict `second_user_data` directamente como argumentos de key-value, equivalente a: `User(id=4, name="Mary", joined="2018-11-30")` + +### Soporte del editor + +El framework fue diseñado en su totalidad para ser fácil e intuitivo de usar. Todas las decisiones fueron probadas en múltiples editores antes de comenzar el desarrollo para asegurar la mejor experiencia de desarrollo. + +En la última encuesta a desarrolladores de Python fue claro que la característica más usada es el "autocompletado". + +El framework **FastAPI** está creado para satisfacer eso. El autocompletado funciona en todas partes. + +No vas a tener que volver a la documentación seguido. + +Así es como tu editor te puede ayudar: + +* en Visual Studio Code: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* en PyCharm: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +Obtendrás completado para tu código que podrías haber considerado imposible antes. Por ejemplo, el key `price` dentro del JSON body (que podría haber estado anidado) que viene de un request. + +Ya no pasará que escribas los nombres de key equivocados, o que tengas que revisar constantemente la documentación o desplazarte arriba y abajo para saber si usaste `username` o `user_name`. + +### Corto + +Tiene **configuraciones por defecto** razonables para todo, con configuraciones opcionales en todas partes. Todos los parámetros pueden ser ajustados para tus necesidades y las de tu API. + +Pero, todo **simplemente funciona** por defecto. + +### Validación + +* Validación para la mayoría (¿o todos?) los **tipos de datos** de Python incluyendo: + * Objetos JSON (`dict`). + * JSON array (`list`) definiendo tipos de ítem. + * Campos de texto (`str`) definiendo longitudes mínimas y máximas. + * Números (`int`, `float`) con valores mínimos y máximos, etc. + +* Validación para tipos más exóticos como: + * URL. + * Email. + * UUID. + * ...y otros. + +Toda la validación es manejada por **Pydantic**, que es robusto y sólidamente establecido. + +### Seguridad y autenticación + +La seguridad y la autenticación están integradas. Sin ningún compromiso con bases de datos ni modelos de datos. + +Todos los schemes de seguridad están definidos en OpenAPI incluyendo: + +* HTTP Basic. +* **OAuth2** (también con **JWT tokens**). Prueba el tutorial en [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* API keys en: + * Headers. + * Parámetros de Query. + * Cookies, etc. + +Más todas las características de seguridad de Starlette (incluyendo **session cookies**). + +Todo ha sido construido como herramientas y componentes reutilizables que son fácilmente integrados con tus sistemas, almacenamiento de datos, bases de datos relacionales y no relacionales, etc. + +### Dependency Injection + +FastAPI incluye un sistema de Dependency Injection extremadamente poderoso y fácil de usar. + +* Inclusive las dependencias pueden tener dependencias creando una jerarquía o un **"grafo" de dependencias**. +* Todas son **manejadas automáticamente** por el framework. +* Todas las dependencias pueden requerir datos de los requests y aumentar las restricciones del *path operation* y la documentación automática. +* **Validación automática** inclusive para parámetros del *path operation* definidos en las dependencias. +* Soporte para sistemas complejos de autenticación de usuarios, **conexiones con bases de datos**, etc. +* **Sin comprometerse** con bases de datos, frontends, etc. Pero permitiendo integración fácil con todos ellos. + +### "Plug-ins" ilimitados + +O dicho de otra manera, no hay necesidad para "plug-ins". Importa y usa el código que necesites. + +Cualquier integración está diseñada para que sea tan sencilla de usar (con dependencias) que puedas crear un "plug-in" para tu aplicación en dos líneas de código usando la misma estructura y sintáxis que usaste para tus *path operations*. + +### Probado + +* Cobertura de pruebas al 100%. +* Base de código 100% anotada con tipos. +* Usado en aplicaciones en producción. + +## Características de Starlette + +**FastAPI** está basado y es completamente compatible con Starlette. Tanto así, que cualquier código de Starlette que tengas también funcionará. + +`FastAPI` es realmente una sub-clase de `Starlette`. Así que, si ya conoces o usas Starlette, muchas de las características funcionarán de la misma manera. + +Con **FastAPI** obtienes todas las características de **Starlette** (porque FastAPI es simplemente Starlette en esteroides): + +* Desempeño realmente impresionante. Es uno de los frameworks de Python más rápidos, a la par con **NodeJS** y **Go**. +* Soporte para **WebSocket**. +* Soporte para **GraphQL**. +* Tareas en background. +* Eventos de startup y shutdown. +* Cliente de pruebas construido con `requests`. +* **CORS**, GZip, Static Files, Streaming responses. +* Soporte para **Session and Cookie**. +* Cobertura de pruebas al 100%. +* Base de código 100% anotada con tipos. + +## Características de Pydantic + +**FastAPI** está basado y es completamente compatible con Pydantic. Tanto así, que cualquier código de Pydantic que tengas también funcionará. + +Esto incluye a librerías externas basadas en Pydantic como ORMs y ODMs para bases de datos. + +Esto también significa que en muchos casos puedes pasar el mismo objeto que obtuviste de un request **directamente a la base de datos**, dado que todo es validado automáticamente. + +Lo mismo aplica para el sentido contrario. En muchos casos puedes pasarle el objeto que obtienes de la base de datos **directamente al cliente**. + +Con **FastAPI** obtienes todas las características de **Pydantic** (dado que FastAPI está basado en Pydantic para todo el manejo de datos): + +* **Sin dificultades para entender**: + * No necesitas aprender un nuevo micro-lenguaje de definición de schemas. + * Si sabes tipos de Python, sabes cómo usar Pydantic. +* Interactúa bien con tu **IDE/linter/cerebro**: + * Porque las estructuras de datos de Pydantic son solo instances de clases que tu defines, el auto-completado, el linting, mypy y tu intuición deberían funcionar bien con tus datos validados. +* **Rápido**: + * En benchmarks Pydantic es más rápido que todas las otras libraries probadas. +* Valida **estructuras complejas**: + * Usa modelos jerárquicos de modelos de Pydantic, `typing` de Python, `List` y `Dict`, etc. + * Los validadores también permiten que se definan fácil y claramente schemas complejos de datos. Estos son chequeados y documentados como JSON Schema. + * Puedes tener objetos de **JSON profundamente anidados** y que todos sean validados y anotados. +* **Extensible**: + * Pydantic permite que se definan tipos de datos a la medida o puedes extender la validación con métodos en un modelo decorado con el decorador de validación. +* Cobertura de pruebas al 100%. diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index e9fe5a3c8ae4f..832c6d61de25d 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -24,6 +24,7 @@ nav: - es: /es/ - pt: /pt/ - zh: /zh/ +- features.md markdown_extensions: - toc: permalink: true From 506d5dce39cab553390ddcca28eb6c96d74475b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 11 Apr 2020 18:49:52 +0200 Subject: [PATCH 078/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 70d524f0ed4b9..eabe015d3dfbb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +* Add Spanish translation for [Características (Features)](https://fastapi.tiangolo.com/es/features/). PR [#1220](https://github.com/tiangolo/fastapi/pull/1220) by [@mariacamilagl](https://github.com/mariacamilagl). + ## 0.54.1 * Update database test setup. PR [#1226](https://github.com/tiangolo/fastapi/pull/1226). From bd1e85a8d31d377bca57f2161f3282feb708fe63 Mon Sep 17 00:00:00 2001 From: Camila Gutierrez Date: Sat, 11 Apr 2020 19:20:32 +0200 Subject: [PATCH 079/153] :memo: Add Spanish translation for the Python Types Intro page (#1237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Spanish translation Python Types Intro page * :memo: Fix tuple docs in Types intro * :pencil2: Fix typos and wording nitpicks Co-authored-by: Sebastián Ramírez --- docs/en/docs/python-types.md | 2 +- docs/es/docs/python-types.md | 286 +++++++++++++++++++++++++++ docs/es/mkdocs.yml | 1 + docs_src/python_types/tutorial007.py | 2 +- 4 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 docs/es/docs/python-types.md diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index ce24281ba3ebf..46420362c8de4 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -194,7 +194,7 @@ You would do the same to declare `tuple`s and `set`s: This means: -* The variable `items_t` is a `tuple`, and each of its items is an `int`. +* The variable `items_t` is a `tuple` with 3 items, an `int`, another `int`, and a `str`. * The variable `items_s` is a `set`, and each of its items is of type `bytes`. #### Dicts diff --git a/docs/es/docs/python-types.md b/docs/es/docs/python-types.md new file mode 100644 index 0000000000000..84a92af959a64 --- /dev/null +++ b/docs/es/docs/python-types.md @@ -0,0 +1,286 @@ +# Introducción a los Tipos de Python + +**Python 3.6+** tiene soporte para "type hints" opcionales. + +Estos **type hints** son una nueva sintáxis, desde Python 3.6+, que permite declarar el tipo de una variable. + +Usando las declaraciones de tipos para tus variables, los editores y otras herramientas pueden proveerte un soporte mejor. + +Este es solo un **tutorial corto** sobre los Python type hints. Solo cubre lo mínimo necesario para usarlos con **FastAPI**... realmente es muy poco lo que necesitas. + +Todo **FastAPI** está basado en estos type hints, lo que le da muchas ventajas y beneficios. + +Pero, así nunca uses **FastAPI** te beneficiarás de aprender un poco sobre los type hints. + +!!! note "Nota" + Si eres un experto en Python y ya lo sabes todo sobre los type hints, salta al siguiente capítulo. + +## Motivación + +Comencemos con un ejemplo simple: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +Llamar este programa nos muestra el siguiente output: + +``` +John Doe +``` + +La función hace lo siguiente: + +* Toma un `first_name` y un `last_name`. +* Convierte la primera letra de cada uno en una letra mayúscula con `title()`. +* Las concatena con un espacio en la mitad. + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### Edítalo + +Es un programa muy simple. + +Ahora, imagina que lo estás escribiendo desde ceros. + +En algún punto habrías comenzado con la definición de la función, tenías los parámetros listos... + +Pero, luego tienes que llamar "ese método que convierte la primera letra en una mayúscula". + +Era `upper`? O era `uppercase`? `first_uppercase`? `capitalize`? + +Luego lo intentas con el viejo amigo de los programadores, el autocompletado del editor. + +Escribes el primer parámetro de la función `first_name`, luego un punto (`.`) y luego presionas `Ctrl+Space` para iniciar el autocompletado. + +Tristemente, no obtienes nada útil: + + + +### Añade tipos + +Vamos a modificar una única línea de la versión previa. + +Vamos a cambiar exactamente este fragmento, los parámetros de la función, de: + +```Python + first_name, last_name +``` + +a: + +```Python + first_name: str, last_name: str +``` + +Eso es todo. + +Esos son los "type hints": + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +No es lo mismo a declarar valores por defecto, como sería con: + +```Python + first_name="john", last_name="doe" +``` + +Es algo diferente. + +Estamos usando los dos puntos (`:`), no un símbolo de igual (`=`). + +Añadir los type hints normalmente no cambia lo que sucedería si ellos no estuviesen presentes. + +Pero ahora imagina que nuevamente estás creando la función, pero con los type hints. + +En el mismo punto intentas iniciar el autocompletado con `Ctrl+Space` y ves: + + + +Con esto puedes moverte hacia abajo viendo las opciones hasta que encuentras una que te suene: + + + +## Más motivación + +Mira esta función que ya tiene type hints: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +Como el editor conoce el tipo de las variables no solo obtienes autocompletado, si no que también obtienes chequeo de errores: + + + +Ahora que sabes que tienes que arreglarlo convierte `age` a un string con `str(age)`: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## Declarando tipos + +Acabas de ver el lugar principal para declarar los type hints. Como parámetros de las funciones. + +Este es también el lugar principal en que los usarías con **FastAPI**. + +### Tipos simples + +Puedes declarar todos los tipos estándar de Python, no solamente `str`. + +Por ejemplo, puedes usar: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### Tipos con sub-tipos + +Existen algunas estructuras de datos que pueden contener otros valores, como `dict`, `list`, `set` y `tuple`. Los valores internos pueden tener su propio tipo también. + +Para declarar esos tipos y sub-tipos puedes usar el módulo estándar de Python `typing`. + +Él existe específicamente para dar soporte a este tipo de type hints. + +#### Listas + +Por ejemplo, vamos a definir una variable para que sea una `list` compuesta de `str`. + +De `typing`, importa `List` (con una `L` mayúscula): + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial006.py!} +``` + +Declara la variable con la misma sintáxis de los dos puntos (`:`). + +Pon `List` como el tipo. + +Como la lista es un tipo que permite tener un "sub-tipo" pones el sub-tipo en corchetes `[]`: + +```Python hl_lines="4" +{!../../../docs_src/python_types/tutorial006.py!} +``` + +Esto significa: la variable `items` es una `list` y cada uno de los ítems en esta lista es un `str`. + +Con esta declaración tu editor puede proveerte soporte inclusive mientras está procesando ítems de la lista. + +Sin tipos el autocompletado en este tipo de estructura es casi imposible de lograr: + + + +Observa que la variable `item` es unos de los elementos en la lista `items`. + +El editor aún sabe que es un `str` y provee soporte para ello. + +#### Tuples y Sets + +Harías lo mismo para declarar `tuple`s y `set`s: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial007.py!} +``` + +Esto significa: + +* La variable `items_t` es un `tuple` con 3 ítems, un `int`, otro `int`, y un `str`. +* La variable `items_s` es un `set` y cada uno de sus ítems es de tipo `bytes`. + +#### Diccionarios (Dicts) + +Para definir un `dict` le pasas 2 sub-tipos separados por comas. + +El primer sub-tipo es para los keys del `dict`. + +El segundo sub-tipo es para los valores del `dict`: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial008.py!} +``` + +Esto significa: + +* La variable `prices` es un `dict`: + * Los keys de este `dict` son de tipo `str` (Digamos que son el nombre de cada ítem). + * Los valores de este `dict` son de tipo `float` (Digamos que son el precio de cada ítem). + +### Clases como tipos + +También puedes declarar una clase como el tipo de una variable. + +Digamos que tienes una clase `Person`con un nombre: + +```Python hl_lines="1 2 3" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +Entonces puedes declarar una variable que sea de tipo `Person`: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +Una vez más tendrás todo el soporte del editor: + + + +## Modelos de Pydantic + +Pydantic es una library de Python para llevar a cabo validación de datos. + +Tú declaras la "forma" de los datos mediante clases con atributos. + +Cada atributo tiene un tipo. + +Luego creas un instance de esa clase con algunos valores y Pydantic validará los valores, los convertirá al tipo apropiado (si ese es el caso) y te dará un objeto con todos los datos. + +Y obtienes todo el soporte del editor con el objeto resultante. + +Tomado de la documentación oficial de Pydantic: + +```Python +{!../../../docs_src/python_types/tutorial010.py!} +``` + +!!! info "Información" + Para aprender más sobre Pydantic mira su documentación. + +**FastAPI** está todo basado en Pydantic. + +Vas a ver mucho más de esto en práctica en el [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. + +## Type hints en **FastAPI** + +**FastAPI** aprovecha estos type hints para hacer varias cosas. + +Con **FastAPI** declaras los parámetros con type hints y obtienes: + +* **Soporte en el editor**. +* **Type checks**. + +...y **FastAPI** usa las mismas declaraciones para: + +* **Definir requerimientos**: desde request path parameters, query parameters, headers, bodies, dependencies, etc. +* **Convertir datos**: desde el request al tipo requerido. +* **Validar datos**: viniendo de cada request: + * Generando **errores automáticos** devueltos al cliente cuando los datos son inválidos. +* **Documentar** la API usando OpenAPI: + * que en su caso es usada por las interfaces de usuario de la documentación automática e interactiva. + +Puede que todo esto suene abstracto. Pero no te preocupes que todo lo verás en acción en el [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. + +Lo importante es que usando los tipos de Python estándar en un único lugar (en vez de añadir más clases, decorator, etc.) **FastAPI** hará mucho del trabajo por ti. + +!!! info "Información" + Si ya pasaste por todo el tutorial y volviste a la sección de los tipos, una buena referencia es la "cheat sheet" de `mypy`. diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 832c6d61de25d..6970862049b41 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -25,6 +25,7 @@ nav: - pt: /pt/ - zh: /zh/ - features.md +- python-types.md markdown_extensions: - toc: permalink: true diff --git a/docs_src/python_types/tutorial007.py b/docs_src/python_types/tutorial007.py index d2c6cb6e7f29a..5b13f15494a8d 100644 --- a/docs_src/python_types/tutorial007.py +++ b/docs_src/python_types/tutorial007.py @@ -1,5 +1,5 @@ from typing import Set, Tuple -def process_items(items_t: Tuple[int], items_s: Set[bytes]): +def process_items(items_t: Tuple[int, int, str], items_s: Set[bytes]): return items_t, items_s From 06e42a4e5de9facf6061cb47d5c34c36a58fa898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 11 Apr 2020 19:22:35 +0200 Subject: [PATCH 080/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index eabe015d3dfbb..1034b8dfdb167 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add Spanish translation for [Introducción a los Tipos de Python (Python Types Intro)](https://fastapi.tiangolo.com/es/python-types/). PR [#1237](https://github.com/tiangolo/fastapi/pull/1237) by [@mariacamilagl](https://github.com/mariacamilagl). * Add Spanish translation for [Características (Features)](https://fastapi.tiangolo.com/es/features/). PR [#1220](https://github.com/tiangolo/fastapi/pull/1220) by [@mariacamilagl](https://github.com/mariacamilagl). ## 0.54.1 From d03c197c80093e99605898b6b798c1b68204b159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Apr 2020 17:56:35 +0200 Subject: [PATCH 081/153] :memo: Update project generation docs (#1287) --- docs/en/docs/project-generation.md | 73 +++++++++--------------------- 1 file changed, 21 insertions(+), 52 deletions(-) diff --git a/docs/en/docs/project-generation.md b/docs/en/docs/project-generation.md index fd7a10fb7d109..2cc2159fc8126 100644 --- a/docs/en/docs/project-generation.md +++ b/docs/en/docs/project-generation.md @@ -1,16 +1,18 @@ # Project Generation - Template -There is a project generator that you can use to get started, with a lot of the initial set up, security, database and first API endpoints already done for you. +You can use a project generator to get started, as it includes a lot of the initial set up, security, database and first API endpoints already done for you. -## Full-Stack-FastAPI-PostgreSQL +A project generator will always have a very opinionated setup that you should update and adapt for your own needs, but it might be a good starting point for your project. + +## Full Stack FastAPI PostgreSQL GitHub: https://github.com/tiangolo/full-stack-fastapi-postgresql -### Full-Stack-FastAPI-PostgreSQL Features +### Full Stack FastAPI PostgreSQL - Features * Full **Docker** integration (Docker based). * Docker Swarm Mode deployment. -* **Docker Compose** integration and optimization for local development +* **Docker Compose** integration and optimization for local development. * **Production ready** Python web server using Uvicorn and Gunicorn. * Python **FastAPI** backend: * **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). @@ -19,14 +21,14 @@ GitHub: OpenAPI and JSON Schema. - * Many other features including automatic validation, serialization, interactive documentation, authentication with OAuth2 JWT tokens, etc. + * **Many other features** including automatic validation, serialization, interactive documentation, authentication with OAuth2 JWT tokens, etc. * **Secure password** hashing by default. * **JWT token** authentication. * **SQLAlchemy** models (independent of Flask extensions, so they can be used with Celery workers directly). * Basic starting models for users (modify and remove as you need). * **Alembic** migrations. * **CORS** (Cross Origin Resource Sharing). -* **Celery** worker that can import and use models and code from the rest of the backend selectively (you don't have to install the complete app in each worker). +* **Celery** worker that can import and use models and code from the rest of the backend selectively. * REST backend tests based on **Pytest**, integrated with Docker, so you can test the full API interaction, independent on the database. As it runs in Docker, it can build a new data store from scratch each time (so you can use ElasticSearch, MongoDB, CouchDB, or whatever you want, and just test that the API works). * Easy Python integration with **Jupyter Kernels** for remote or in-Docker development with extensions like Atom Hydrogen or Visual Studio Code Jupyter. * **Vue** frontend: @@ -50,53 +52,20 @@ GitHub: https://github.com/tiangolo/full-stack-fastapi-couchbase -### Full-Stack-FastAPI-Couchbase Features +⚠️ **WARNING** ⚠️ -* Full **Docker** integration (Docker based). -* Docker Swarm Mode deployment. -* **Docker Compose** integration and optimization for local development. -* **Production ready** Python web server using Uvicorn and Gunicorn. -* Python **FastAPI** backend: - * **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). - * **Intuitive**: Great editor support. Completion everywhere. Less time debugging. - * **Easy**: Designed to be easy to use and learn. Less time reading docs. - * **Short**: Minimize code duplication. Multiple features from each parameter declaration. - * **Robust**: Get production-ready code. With automatic interactive documentation. - * **Standards-based**: OpenAPI and JSON Schema. - * Many other features including automatic validation, serialization, interactive documentation, authentication with OAuth2 JWT tokens, etc. -* **Secure password** hashing by default. -* **JWT token** authentication. -* **CORS** (Cross Origin Resource Sharing). -* **Celery** worker that can import and use code from the rest of the backend selectively (you don't have to install the complete app in each worker). -* **NoSQL Couchbase** database that supports direct synchronization via Couchbase Sync Gateway for offline-first applications. -* **Full Text Search** integrated, using Couchbase. -* REST backend tests based on Pytest, integrated with Docker, so you can test the full API interaction, independent on the database. As it runs in Docker, it can build a new data store from scratch each time (so you can use ElasticSearch, MongoDB, or whatever you want, and just test that the API works). -* Easy Python integration with **Jupyter** Kernels for remote or in-Docker development with extensions like Atom Hydrogen or Visual Studio Code Jupyter. -* **Email notifications** for account creation and password recovery, compatible with: - * Mailgun - * SparkPost - * SendGrid - * ...any other provider that can generate standard SMTP credentials. -* **Vue** frontend: - * Generated with Vue CLI. - * **JWT Authentication** handling. - * Login view. - * After login, main dashboard view. - * Main dashboard with user creation and edition. - * Self user edition. - * **Vuex**. - * **Vue-router**. - * **Vuetify** for beautiful material design components. - * **TypeScript**. - * Docker server based on **Nginx** (configured to play nicely with Vue-router). - * Docker multi-stage building, so you don't need to save or commit compiled code. - * Frontend tests ran at build time (can be disabled too). - * Made as modular as possible, so it works out of the box, but you can re-generate with Vue CLI or create it as you need, and re-use what you want. -* **Flower** for Celery jobs monitoring. -* Load balancing between frontend and backend with **Traefik**, so you can have both under the same domain, separated by path, but served by different containers. -* Traefik integration, including Let's Encrypt **HTTPS** certificates automatic generation. -* GitLab **CI** (continuous integration), including frontend and backend testing. +If you are starting a new project from scratch, check the alternatives here. + +For example, the project generator Full Stack FastAPI PostgreSQL might be a better alternative, as it is actively maintained and used. And it includes all the new features and improvements. + +You are still free to use the Couchbase-based generator if you want to, it should probably still work fine, and if you already have a project generated with it that's fine as well (and you probably already updated it to suit your needs). + +You can read more about it in the docs for the repo. + +## Full Stack FastAPI MongoDB + +...might come later, depending on my time availability and other factors. 😅 🎉 From 4e77737a3f7bf2608132ea170e9ff013b5af6732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Apr 2020 17:57:50 +0200 Subject: [PATCH 082/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1034b8dfdb167..7384d6d381acb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update docs for project generation. PR [#1287](https://github.com/tiangolo/fastapi/pull/1287). * Add Spanish translation for [Introducción a los Tipos de Python (Python Types Intro)](https://fastapi.tiangolo.com/es/python-types/). PR [#1237](https://github.com/tiangolo/fastapi/pull/1237) by [@mariacamilagl](https://github.com/mariacamilagl). * Add Spanish translation for [Características (Features)](https://fastapi.tiangolo.com/es/features/). PR [#1220](https://github.com/tiangolo/fastapi/pull/1220) by [@mariacamilagl](https://github.com/mariacamilagl). From bfa78db458c3d712ce26aa846e8f734af9ec8b2b Mon Sep 17 00:00:00 2001 From: Xie Wei <39515546+waynerv@users.noreply.github.com> Date: Sat, 16 May 2020 17:58:10 +0800 Subject: [PATCH 083/153] :globe_with_meridians: Add Chinese translation for index docs (#1191) * Add Chinese tranlation for docs/index.md * Fix syntax issue * Update resource address of zh docs * Optimize typography in zh docs * improve translations Co-authored-by: Waynerv --- docs/zh/docs/index.md | 327 +++++++++++++++++++++--------------------- 1 file changed, 162 insertions(+), 165 deletions(-) diff --git a/docs/zh/docs/index.md b/docs/zh/docs/index.md index 138d8bd607ab7..897fd45fbac2c 100644 --- a/docs/zh/docs/index.md +++ b/docs/zh/docs/index.md @@ -1,7 +1,3 @@ - -{!../../../docs/missing-translation.md!} - -

    FastAPI

    @@ -25,80 +21,80 @@ --- -**Documentation**: https://fastapi.tiangolo.com +**文档**: https://fastapi.tiangolo.com -**Source Code**: https://github.com/tiangolo/fastapi +**源码**: https://github.com/tiangolo/fastapi --- -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. +FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用基于类型提示的 Python 3.6 及更高版本。 -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). +* **快速**:可与 **NodeJS** 和 **Go** 比肩的极高性能(归功于 Starlette 和 Pydantic)。[最快的 Python web 框架之一](#_11)。 -* **Fast to code**: Increase the speed to develop features by about 200% to 300% *. -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. +* **高效编码**:提高功能开发速度约 200% 至 300%。* +* **更少 bug**:减少约 40% 的人为(开发者)导致错误。* +* **智能**:极佳的编辑器支持。处处皆可自动补全,减少调试时间。 +* **简单**:设计的易于使用和学习,减少阅读文档时间。 +* **简短**:减少代码重复。通过不同的参数声明实现丰富功能。bug 更少。 +* **健壮**:生产可用级别的代码。以及自动生成的交互式文档。 +* **标准化**:基于 API 的相关开放标准并完全兼容:OpenAPI (以前被称为 Swagger) 和 JSON Schema。 -* estimation based on tests on an internal development team, building production applications. +* 根据对某个构建线上应用的内部开发团队所进行的测试估算得出。 -## 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.*" +"*[...] 最近我一直在使用 **FastAPI**。[...] 实际上我正在计划将其用于我所在的微软团队的所有**机器学习服务**。其中一些服务正被集成进 **Windows** 核心产品和一些 **Office** 产品。*" -
    Kabir Khan - Microsoft (ref)
    +
    Kabir Khan - 微软 (ref)
    --- -"*I’m over the moon excited about **FastAPI**. It’s so fun!*" +"***FastAPI** 让我兴奋的欣喜若狂。它太棒了!*" -
    Brian Okken - Python Bytes podcast host (ref)
    +
    Brian Okken - Python Bytes 播客主持人 (ref)
    --- -"*Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that.*" +"*老实说,你的作品看起来非常可靠和优美。在很多方面,这就是我想让 **Hug** 成为的样子 - 看到有人实现了它真的很鼓舞人心。*" -
    Timothy Crosley - Hug creator (ref)
    +
    Timothy Crosley - Hug 作者 (ref)
    --- -"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*" +"*如果你正打算学习一个**现代框架**用来构建 REST API,来看下 **FastAPI** [...] 它快速、易用且易于学习 [...]*" -"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*" +"*我们已经将 **API** 服务切换到了 **FastAPI** [...] 我认为你会喜欢它的 [...]*" -
    Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
    +
    Ines Montani - Matthew Honnibal - Explosion AI 创始人 - spaCy 作者 (ref) - (ref)
    --- -"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*" +"*我们采用了 **FastAPI** 来创建用于获取**预测结果**的 **REST** 服务。[用于 Ludwig]*" -
    Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
    +
    Piero Molino, Yaroslav Dudin 和 Sai Sumanth Miryala - Uber (ref)
    --- -## **Typer**, the FastAPI of CLIs +## **Typer**,命令行中的 Fast API -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. +如果你正在开发一个在终端中运行的命令行应用而不是 web API,不妨试下 **Typer**。 -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 +**Typer** 是 FastAPI 的小伙伴。它打算成为**命令行中的 FastAPI**。 ⌨️ 🚀 -## Requirements +## 依赖 -Python 3.6+ +Python 3.6 及更高版本 -FastAPI stands on the shoulders of giants: +FastAPI 站在以下巨人的肩膀之上: -* Starlette for the web parts. -* Pydantic for the data parts. +* Starlette负责 web 部分。 +* Pydantic负责数据部分。 -## Installation +## 安装
    @@ -110,7 +106,7 @@ $ pip install fastapi
    -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. +你还需要一个 ASGI 服务器,生产环境可以使用 Uvicorn 或者 Hypercorn
    @@ -122,11 +118,11 @@ $ pip install uvicorn
    -## Example +## 示例 -### Create it +### 创建 -* Create a file `main.py` with: +* 创建一个 `main.py` 文件并写入以下内容: ```Python from fastapi import FastAPI @@ -145,9 +141,9 @@ def read_item(item_id: int, q: str = None): ```
    -Or use async def... +或者使用 async def... -If your code uses `async` / `await`, use `async def`: +如果你的代码里会出现 `async` / `await`,应使用 `async def`: ```Python hl_lines="7 12" from fastapi import FastAPI @@ -167,13 +163,13 @@ async def read_item(item_id: int, q: str = None): **Note**: -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. +如果你不知道是否会用到,可以查看文档的 _"In a hurry?"_ 章节中 关于 `async` 和 `await` 的部分
    -### Run it +### 运行 -Run the server with: +通过以下命令运行服务器:
    @@ -190,54 +186,54 @@ $ uvicorn main:app --reload
    -About the command uvicorn main:app --reload... +关于 uvicorn main:app --reload 命令...... -The command `uvicorn main:app` refers to: + `uvicorn main:app` 命令含义如下: -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. +* `main`:`main.py` 文件(一个 Python "模块")。 +* `app`:在 `main.py` 文件中通过 `app = FastAPI()` 创建的对象。 +* `--reload`:让服务器在更新代码后重新启动。仅在开发时使用该选项。
    -### Check it +### 检查 -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. +使用浏览器访问 http://127.0.0.1:8000/items/5?q=somequery。 -You will see the JSON response as: +你将会看到如下 JSON 响应: ```JSON {"item_id": 5, "q": "somequery"} ``` -You already created an API that: +你已经创建了一个具有以下功能的 API: -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. +* 通过 _路径_ `/` 和 `/items/{item_id}` 接受 HTTP 请求。 +* 以上 _路径_ 都接受 `GET` 操作(也被称为 HTTP _方法_)。 +* `/items/{item_id}` _路径_ 有一个 _路径参数_ `item_id` 并且应该为 `int` 类型。 +* `/items/{item_id}` _路径_ 有一个可选的 `str` 类型的 _查询参数_ `q`。 -### Interactive API docs +### 交互式 API 文档 -Now go to http://127.0.0.1:8000/docs. +现在访问 http://127.0.0.1:8000/docs。 -You will see the automatic interactive API documentation (provided by Swagger UI): +你会看到自动生成的交互式 API 文档(由 Swagger UI生成): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternative API docs +### 备选 API 文档 -And now, go to http://127.0.0.1:8000/redoc. +访问 http://127.0.0.1:8000/redoc。 -You will see the alternative automatic documentation (provided by ReDoc): +你会看到另一个自动生成的文档(由 ReDoc)生成: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Example upgrade +## 升级示例 -Now modify the file `main.py` to receive a body from a `PUT` request. +修改 `main.py` 文件来从 `PUT` 请求中接收请求体。 -Declare the body using standard Python types, thanks to Pydantic. +我们借助 Pydantic 来使用标准的 Python 类型声明请求体。 ```Python hl_lines="2 7 8 9 10 23 24 25" from fastapi import FastAPI @@ -267,175 +263,176 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). +服务器将会自动重载(因为在上面的步骤中你向 `uvicorn` 命令添加了 `--reload` 选项)。 -### Interactive API docs upgrade +### 升级交互式 API 文档 -Now go to http://127.0.0.1:8000/docs. +访问 http://127.0.0.1:8000/docs。 -* The interactive API documentation will be automatically updated, including the new body: +* 交互式 API 文档将会自动更新,并加入新的请求体: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: +* 点击 "Try it out" 按钮,之后你可以填写参数并直接调用 API: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: +* 然后点击 "Execute" 按钮,用户界面将会和 API 进行通信,发送参数,获取结果并在屏幕上展示: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Alternative API docs upgrade +### 升级备选文档 -And now, go to http://127.0.0.1:8000/redoc. +访问 http://127.0.0.1:8000/redoc。 -* The alternative documentation will also reflect the new query parameter and body: +* 备选文档同样会体现新加入的请求参数和请求体: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Recap +### 回顾 -In summary, you declare **once** the types of parameters, body, etc. as function parameters. +总的来说,你就像声明函数的参数类型一样只声明了**一次**请求参数、请求体等的类型。 -You do that with standard modern Python types. +你使用了标准的现代 Python 类型来完成声明。 -You don't have to learn a new syntax, the methods or classes of a specific library, etc. +你不需要去学习新的语法、了解特定库的方法或类,等等。 -Just standard **Python 3.6+**. +只需要使用标准的 **Python 3.6 及更高版本**。 -For example, for an `int`: +举个例子,比如声明 `int` 类型: ```Python item_id: int ``` -or for a more complex `Item` model: +或者一个更复杂的 `Item` 模型: ```Python item: Item ``` -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. +......在进行一次声明之后,你将获得: + +* 编辑器支持,包括: + * 自动补全 + * 类型检查 +* 数据校验: + * 在校验失败时自动生成清晰的错误信息 + * 对多层嵌套的 JSON 对象依然执行校验 +* 转换 来自网络请求的输入数据为 Python 数据类型。包括以下数据: + * JSON + * 路径参数 + * 查询参数 + * Cookies + * 请求头 + * 表单 + * 文件 +* 转换 输出的数据:转换 Python 数据类型为供网络传输的 JSON 数据: + * 转换 Python 基础类型 (`str`、 `int`、 `float`、 `bool`、 `list` 等) + * `datetime` 对象 + * `UUID` 对象 + * 数据库模型 + * ......以及更多其他类型 +* 自动生成的交互式 API 文档,包括两种可选的用户界面: + * Swagger UI + * ReDoc --- -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. +回到前面的代码示例,**FastAPI** 将会: + +* 校验 `GET` 和 `PUT` 请求的路径中是否含有 `item_id`。 +* 校验 `GET` 和 `PUT` 请求中的 `item_id` 是否为 `int` 类型。 + * 如果不是,客户端将会收到清晰有用的错误信息。 +* 检查 `GET` 请求中是否有命名为 `q` 的可选查询参数(比如 `http://127.0.0.1:8000/items/foo?q=somequery`)。 + * 因为 `q` 被声明为 `= None`,所以它是可选的。 + * 如果没有 `None` 它将会是必需的 (如 `PUT` 例子中的请求体)。 +* 对于访问 `/items/{item_id}` 的 `PUT` 请求,将请求体读取为 JSON 并: + * 检查是否有必需属性 `name` 并且值为 `str` 类型 。 + * 检查是否有必需属性 `price` 并且值为 `float` 类型。 + * 检查是否有可选属性 `is_offer`, 如果有的话值应该为 `bool` 类型。 + * 以上过程对于多层嵌套的 JSON 对象同样也会执行 +* 自动对 JSON 进行转换或转换成 JSON。 +* 通过 OpenAPI 文档来记录所有内容,可被用于: + * 交互式文档系统 + * 许多编程语言的客户端代码自动生成系统 +* 直接提供 2 种交互式文档 web 界面。 --- -We just scratched the surface, but you already get the idea of how it all works. +虽然我们才刚刚开始,但其实你已经了解了这一切是如何工作的。 -Try changing the line with: +尝试更改下面这行代码: ```Python return {"item_name": item.name, "item_id": item_id} ``` -...from: +......从: ```Python ... "item_name": item.name ... ``` -...to: +......改为: ```Python ... "item_price": item.price ... ``` -...and see how your editor will auto-complete the attributes and know their types: +......注意观察编辑器是如何自动补全属性并且还知道它们的类型: ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -For a more complete example including more features, see the Tutorial - User Guide. +教程 - 用户指南 中有包含更多特性的更完整示例。 -**Spoiler alert**: the tutorial - user guide includes: +**剧透警告**: 教程 - 用户指南中的内容有: -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: +* 对来自不同地方的参数进行声明,如:**请求头**、**cookies**、**form 表单**以及**上传的文件**。 +* 如何设置**校验约束**如 `maximum_length` 或者 `regex`。 +* 一个强大并易于使用的 **依赖注入** 系统。 +* 安全性和身份验证,包括通过 **JWT 令牌**和 **HTTP 基本身份认证**来支持 **OAuth2**。 +* 更进阶(但同样简单)的技巧来声明 **多层嵌套 JSON 模型** (借助 Pydantic)。 +* 许多额外功能(归功于 Starlette)比如: * **WebSockets** * **GraphQL** - * extremely easy tests based on `requests` and `pytest` + * 基于 `requests` 和 `pytest` 的极其简单的测试 * **CORS** * **Cookie Sessions** - * ...and more. + * ......以及更多 + +## 性能 -## Performance -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) +独立机构 TechEmpower 所作的基准测试结果显示,基于 Uvicorn 运行的 **FastAPI** 程序是 最快的 Python web 框架之一,仅次于 Starlette 和 Uvicorn 本身(FastAPI 内部使用了它们)。(*) -To understand more about it, see the section Benchmarks. +想了解更多,请查阅 基准测试 章节。 -## Optional Dependencies +## 可选依赖 -Used by Pydantic: +用于 Pydantic: -* ujson - for faster JSON "parsing". -* email_validator - for email validation. +* ujson - 更快的 JSON "解析"。 +* email_validator - 用于 email 校验。 -Used by Starlette: +用于 Starlette: -* requests - Required if you want to use the `TestClient`. -* aiofiles - Required if you want to use `FileResponse` or `StaticFiles`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. +* requests - 使用 `TestClient` 时安装。 +* aiofiles - 使用 `FileResponse` 或 `StaticFiles` 时安装。 +* jinja2 - 使用默认模板配置时安装。 +* python-multipart - 需要通过 `request.form()` 对表单进行"解析"时安装。 +* itsdangerous - 提供 `SessionMiddleware` 支持。 +* pyyaml - 使用 Starlette 提供的 `SchemaGenerator` 时安装(有 FastAPI 你可能并不需要它)。 +* graphene - 需要 `GraphQLApp` 支持时安装。 +* ujson - 使用 `UJSONResponse` 时安装。 -Used by FastAPI / Starlette: +用于 FastAPI / Starlette: -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. +* uvicorn - 用于加载和服务你的应用程序的服务器。 +* orjson - 使用 `ORJSONResponse` 时安装。 -You can install all of these with `pip install fastapi[all]`. +你可以通过 `pip install fastapi[all]` 命令来安装以上所有依赖。 -## License +## 许可协议 -This project is licensed under the terms of the MIT license. +该项目遵循 MIT 许可协议。 From 44bd64d797ffe196d858b67f6954a1400277fd4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 12:04:32 +0200 Subject: [PATCH 084/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7384d6d381acb..32d30b1a29e86 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation for [main page to Chinese](https://fastapi.tiangolo.com/zh/) PR [#1191](https://github.com/tiangolo/fastapi/pull/1191) by [@waynerv](https://github.com/waynerv). * Update docs for project generation. PR [#1287](https://github.com/tiangolo/fastapi/pull/1287). * Add Spanish translation for [Introducción a los Tipos de Python (Python Types Intro)](https://fastapi.tiangolo.com/es/python-types/). PR [#1237](https://github.com/tiangolo/fastapi/pull/1237) by [@mariacamilagl](https://github.com/mariacamilagl). * Add Spanish translation for [Características (Features)](https://fastapi.tiangolo.com/es/features/). PR [#1220](https://github.com/tiangolo/fastapi/pull/1220) by [@mariacamilagl](https://github.com/mariacamilagl). From a0cdbe449b98e6d10194448383b2d2f0b37fba7a Mon Sep 17 00:00:00 2001 From: Dustyposa Date: Sat, 16 May 2020 18:18:04 +0800 Subject: [PATCH 085/153] :globe_with_meridians: Add translation of features.md to Chinese (#1192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * translation features.md to Chinese * update review data * :DOCS: update with review * :fire: Remove double link in build mkdocs.yml for other languages Co-authored-by: Sebastián Ramírez --- docs/zh/docs/features.md | 206 +++++++++++++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 207 insertions(+) create mode 100644 docs/zh/docs/features.md diff --git a/docs/zh/docs/features.md b/docs/zh/docs/features.md new file mode 100644 index 0000000000000..964a2fd7ece60 --- /dev/null +++ b/docs/zh/docs/features.md @@ -0,0 +1,206 @@ +# 特性 + +## FastAPI 特性 + +**FastAPI** 提供了以下内容: + +### 基于开放标准 + + +* 用于创建 API 的 OpenAPI 包含了路径操作,请求参数,请求体,安全性等的声明。 +* 使用 JSON Schema (因为 OpenAPI 本身就是基于 JSON Schema 的)自动生成数据模型文档。 +* 经过了缜密的研究后围绕这些标准而设计。并非狗尾续貂。 +* 这也允许了在很多语言中自动**生成客户端代码**。 + +### 自动生成文档 + +交互式 API 文档以及具探索性 web 界面。因为该框架是基于 OpenAPI,所以有很多可选项,FastAPI 默认自带两个交互式 API 文档。 + +* Swagger UI,可交互式操作,能在浏览器中直接调用和测试你的 API 。 + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* 另外的 API 文档:ReDoc + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### 更主流的 Python + +全部都基于标准的 **Python 3.6 类型**声明(感谢 Pydantic )。没有新的语法需要学习。只需要标准的 Python 。 + +如果你需要2分钟来学习如何使用 Python 类型(即使你不使用 FastAPI ),看看这个简短的教程:[Python Types](python-types.md){.internal-link target=_blank}。 + +编写带有类型标注的标准 Python: + +```Python +from typing import List, Dict +from datetime import date + +from pydantic import BaseModel + +# Declare a variable as a str +# and get editor support inside the function +def main(user_id: str): + return user_id + + +# A Pydantic model +class User(BaseModel): + id: int + name: str + joined: date +``` + +可以像这样来使用: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + + +!!! info + `**second_user_data` 意思是: + + 直接将`second_user_data`字典的键和值直接作为key-value参数传递,等同于:`User(id=4, name="Mary", joined="2018-11-30")` + +### 编辑器支持 + +整个框架都被设计得易于使用且直观,所有的决定都在开发之前就在多个编辑器上进行了测试,来确保最佳的开发体验。 + +在最近的 Python 开发者调查中,我们能看到 被使用最多的功能是"自动补全"。 + +整个 **FastAPI** 框架就是基于这一点的。任何地方都可以进行自动补全。 + +你几乎不需要经常回来看文档。 + +在这里,你的编辑器可能会这样帮助你: + +* Visual Studio Code 中: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* PyCharm 中: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +你将能进行代码补全,这是在之前你可能曾认为不可能的事。例如,在来自请求 JSON 体(可能是嵌套的)中的键 `price`。 + +不会再输错键名,来回翻看文档,或者来回滚动寻找你最后使用的 `username` 或者 `user_name` 。 + + + +### 简洁 + +任何类型都有合理的**默认值**,任何和地方都有可选配置。所有的参数被微调,来满足你的需求,定义成你需要的 API。 + +但是默认情况下,一切都能**“顺利工作”**。 + +### 验证 + +* 校验大部分(甚至所有?)的 Python **数据类型**,包括: + * JSON 对象 (`dict`). + * JSON 数组 (`list`) 定义成员类型。 + * 字符串 (`str`) 字段, 定义最小或最大长度。 + * 数字 (`int`, `float`) 有最大值和最小值, 等等。 + +* 校验外来类型, 比如: + * URL. + * Email. + * UUID. + * ...及其他. + +所有的校验都由完善且强大的 **Pydantic** 处理。 + +### 安全性及身份验证 + +集成了安全性和身份认证。杜绝数据库或者数据模型的渗透风险。 + +OpenAPI 中定义的安全模式,包括: + +* HTTP 基本认证。 +* **OAuth2** (也使用 **JWT tokens**)。在 [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}查看教程。 +* API 密钥,在: + * 请求头。 + * 查询参数。 + * Cookies, 等等。 + +加上来自 Starlette(包括 **session cookie**)的所有安全特性。 + +所有的这些都是可复用的工具和组件,可以轻松与你的系统,数据仓库,关系型以及 NoSQL 数据库等等集成。 + + + +### 依赖注入 + +FastAPI 有一个使用非常简单,但是非常强大的依赖注入系统。 + +* 甚至依赖也可以有依赖,创建一个层级或者**“图”依赖**。 +* 所有**自动化处理**都由框架完成。 +* 所有的依赖关系都可以从请求中获取数据,并且**增加了路径操作**约束和自动文档生成。 +* 即使在依赖项中被定义的*路径操作* 也会**自动验证**。 +* 支持复杂的用户身份认证系统,**数据库连接**等等。 +* **不依赖**数据库,前端等。 但是和它们集成很简单。 + +### 无限制"插件" + +或者说,导入并使用你需要的代码,而不需要它们。 + +任何集成都被设计得被易于使用(用依赖关系),你可以用和*路径操作*相同的结构和语法,在两行代码中为你的应用创建一个“插件”。 + +### 测试 + +* 100% 测试覆盖。 +* 代码库100% 类型注释。 +* 用于生产应用。 + +## Starlette 特性 + +**FastAPI** 和 Starlette 完全兼容(并基于)。所以,你有的其他的 Starlette 代码也能正常工作。`FastAPI` 实际上是 `Starlette`的一个子类。所以,如果你已经知道或者使用 Starlette,大部分的功能会以相同的方式工作。 + +通过 **FastAPI** 你可以获得所有 **Starlette** 的特性 ( FastAPI 就像加强版的 Starlette ): + +* 令人惊叹的性能。它是 Python 可用的最快的框架之一,和 **NodeJS** 及 **Go** 相当。 +* **支持 WebSocket** 。 +* **支持 GraphQL** 。 +* 后台任务处理。 +* Startup 和 shutdown 事件。 +* 测试客户端基于 `requests`。 +* **CORS**, GZip, 静态文件, 流响应。 +* 支持 **Session 和 Cookie** 。 +* 100% 测试覆盖率。 +* 代码库 100% 类型注释。 + +## Pydantic 特性 + +**FastAPI** 和 Pydantic 完全兼容(并基于)。所以,你有的其他的 Pydantic 代码也能正常工作。 + +兼容包括基于 Pydantic 的外部库, 例如用与数据库的 ORMs, ODMs。 + +这也意味着在很多情况下,你可以将从请求中获得的相同对象**直接传到数据库**,因为所有的验证都是自动的。 + +反之亦然,在很多情况下,你也可以将从数据库中获取的对象**直接传到客户端**。 + +通过 **FastAPI** 你可以获得所有 **Pydantic** (FastAPI 基于 Pydantic 做了所有的数据处理): + +* **更简单**: + * 没有新的模式定义 micro-language 需要学习。 + * 如果你知道 Python types,你就知道如何使用 Pydantic。 +* 和你 **IDE/linter/brain** 适配: + * 因为 pydantic 数据结构仅仅是你定义的类的实例;自动补全,linting,mypy 以及你的直觉应该可以和你验证的数据一起正常工作。 +* **更快**: + * 在 基准测试 中,Pydantic 比其他被测试的库都要快。 +* 验证**复杂结构**: + * 使用分层的 Pydantic 模型, Python `typing`的 `List` 和 `Dict` 等等。 + * 验证器使我们能够简单清楚的将复杂的数据模式定义、检查并记录为 JSON Schema。 + * 你可以拥有深度**嵌套的 JSON** 对象并对它们进行验证和注释。 +* **可扩展**: + * Pydantic 允许定义自定义数据类型或者你可以用验证器装饰器对被装饰的模型上的方法扩展验证。 +* 100% 测试覆盖率。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 6a95e605d640b..61e46bb1bf2ba 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -24,6 +24,7 @@ nav: - es: /es/ - pt: /pt/ - zh: /zh/ +- features.md markdown_extensions: - toc: permalink: true From 1d0f909ca586df89aacd07a694c4746768369286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 12:21:06 +0200 Subject: [PATCH 086/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 32d30b1a29e86..4340b419de461 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation for [Features - 特性](https://fastapi.tiangolo.com/zh/features/). PR [#1192](https://github.com/tiangolo/fastapi/pull/1192) by [@Dustyposa](https://github.com/Dustyposa). * Add translation for [main page to Chinese](https://fastapi.tiangolo.com/zh/) PR [#1191](https://github.com/tiangolo/fastapi/pull/1191) by [@waynerv](https://github.com/waynerv). * Update docs for project generation. PR [#1287](https://github.com/tiangolo/fastapi/pull/1287). * Add Spanish translation for [Introducción a los Tipos de Python (Python Types Intro)](https://fastapi.tiangolo.com/es/python-types/). PR [#1237](https://github.com/tiangolo/fastapi/pull/1237) by [@mariacamilagl](https://github.com/mariacamilagl). From fc7b4ab8803a96694ca36dde602f1be4071add8f Mon Sep 17 00:00:00 2001 From: Xie Wei <39515546+waynerv@users.noreply.github.com> Date: Sat, 16 May 2020 18:32:31 +0800 Subject: [PATCH 087/153] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20translat?= =?UTF-8?q?ion=20for=20tutorial=20intro=20doc=20(#1202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/zh/docs/tutorial/index.md | 80 ++++++++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 2 + 2 files changed, 82 insertions(+) create mode 100644 docs/zh/docs/tutorial/index.md diff --git a/docs/zh/docs/tutorial/index.md b/docs/zh/docs/tutorial/index.md new file mode 100644 index 0000000000000..fbc488202f25f --- /dev/null +++ b/docs/zh/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# 教程 - 用户指南 - 简介 + +本教程将一步步向你展示如何使用 **FastAPI** 的绝大部分特性。 + +各个章节的内容循序渐进,但是又围绕着单独的主题,所以你可以直接跳转到某个章节以解决你的特定需求。 + +本教程同样可以作为将来的参考手册。 + +你可以随时回到本教程并查阅你需要的内容。 + +## 运行代码 + +所有代码片段都可以复制后直接使用(它们实际上是经过测试的 Python 文件)。 + +要运行任何示例,请将代码复制到 `main.py` 文件中,然后使用以下命令启动 `uvicorn`: + +
    + +```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. +``` + +
    + +强烈建议你在本地编写或复制代码,对其进行编辑并运行。 + +在编辑器中使用 FastAPI 会真正地展现出它的优势:只需要编写很少的代码,所有的类型检查,代码补全等等。 + +--- + +## 安装 FastAPI + +第一个步骤是安装 FastAPI。 + +为了使用本教程,你可能需要安装所有的可选依赖及对应功能: + +
    + +```console +$ pip install fastapi[all] + +---> 100% +``` + +
    + +......以上安装还包括了 `uvicorn`,你可以将其用作运行代码的服务器。 + +!!! note + 你也可以分开来安装。 + + 假如你想将应用程序部署到生产环境,你可能要执行以下操作: + + ``` + pip install fastapi + ``` + + 并且安装`uvicorn`来作为服务器: + + ``` + pip install uvicorn + ``` + + 然后对你想使用的每个可选依赖项也执行相同的操作。 + +## 进阶用户指南 + +在本**教程-用户指南**之后,你可以阅读**进阶用户指南**。 + +**进阶用户指南**以本教程为基础,使用相同的概念,并教授一些额外的特性。 + +但是你应该先阅读**教程-用户指南**(即你现在正在阅读的内容)。 + +教程经过精心设计,使你可以仅通过**教程-用户指南**来开发一个完整的应用程序,然后根据你的需要,使用**进阶用户指南**中的一些其他概念,以不同的方式来扩展它。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 61e46bb1bf2ba..12cd53dc481d8 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -25,6 +25,8 @@ nav: - pt: /pt/ - zh: /zh/ - features.md +- 教程 - 用户指南: + - tutorial/index.md markdown_extensions: - toc: permalink: true From dff644abe0a6e4ba902ea06b0dbf59aa41aed4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 12:38:13 +0200 Subject: [PATCH 088/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4340b419de461..86d4c31ff6a8f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation for [Tutorial - User Guide - Intro - 教程 - 用户指南 - 简介](https://fastapi.tiangolo.com/zh/tutorial/). PR [#1202](https://github.com/tiangolo/fastapi/pull/1202) by [@waynerv](https://github.com/waynerv). * Add translation for [Features - 特性](https://fastapi.tiangolo.com/zh/features/). PR [#1192](https://github.com/tiangolo/fastapi/pull/1192) by [@Dustyposa](https://github.com/Dustyposa). * Add translation for [main page to Chinese](https://fastapi.tiangolo.com/zh/) PR [#1191](https://github.com/tiangolo/fastapi/pull/1191) by [@waynerv](https://github.com/waynerv). * Update docs for project generation. PR [#1287](https://github.com/tiangolo/fastapi/pull/1287). From f67bc3ffe8ee72ceb2039ab9c4ce3863d14df426 Mon Sep 17 00:00:00 2001 From: Ikkyu <31848542+RunningIkkyu@users.noreply.github.com> Date: Sat, 16 May 2020 18:45:04 +0800 Subject: [PATCH 089/153] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20translat?= =?UTF-8?q?ions=20for=20docs:=20deployment.md=20(#1203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add new language of docs: zh * Add deployment.md Chinese trans * add "or" * rm index.md * updates Chinese translations of deployement.md * update translations of deployment.md Co-authored-by: Sebastián Ramírez --- docs/zh/docs/deployment.md | 390 +++++++++++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 391 insertions(+) create mode 100644 docs/zh/docs/deployment.md diff --git a/docs/zh/docs/deployment.md b/docs/zh/docs/deployment.md new file mode 100644 index 0000000000000..6eb25824a7c42 --- /dev/null +++ b/docs/zh/docs/deployment.md @@ -0,0 +1,390 @@ +# 部署 + +部署 **FastAPI** 应用相对比较简单。 + +根据特定使用情况和使用工具有几种不同的部署方式。 + +接下来的章节,你将了解到一些关于部署方式的内容。 + +## FastAPI 版本 + +许多应用和系统已经在生产环境使用 **FastAPI**。其测试覆盖率保持在 100%。但该项目仍在快速开发。 + +我们会经常加入新的功能,定期错误修复,同时也在不断的优化项目代码。 + +这也是为什么当前版本仍然是 `0.x.x`,我们以此表明每个版本都可能有重大改变。 + +现在就可以使用 **FastAPI** 创建生产应用(你可能已经使用一段时间了)。你只需要确保使用的版本和代码其他部分能够正常兼容。 + +### 指定你的 `FastAPI` 版本 + +你应该做的第一件事情,是为你正在使用的 **FastAPI** 指定一个能够正确运行你的应用的最新版本。 + +例如,假设你的应用中正在使用版本 `0.45.0`。 + +如果你使用 `requirements.txt` 文件,你可以这样指定版本: + +```txt +fastapi==0.45.0 +``` + +这表明你将使用 `0.45.0` 版本的 `FastAPI`。 + +或者你也可以这样指定: + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +这表明你将使用 `0.45.0` 及以上,但低于 `0.46.0` 的版本,例如,`0.45.2` 依然可以接受。 + +如果使用其他工具管理你的安装,比如 Poetry,Pipenv,或者其他工具,它们都有各自指定包的版本的方式。 + +### 可用版本 + +你可以在 [发行说明](release-notes.md){.internal-link target=_blank} 中查看可用的版本(比如:检查最新版本是什么)。 + + +### 关于版本 + +FastAPI 遵循语义版本控制约定,`1.0.0` 以下的任何版本都可能加入重大变更。 + +FastAPI 也遵循这样的约定:任何 ”PATCH“ 版本变更都是用来修复 bug 和向下兼容的变更。 + +!!! tip + "PATCH" 是指版本号的最后一个数字,例如,在 `0.2.3` 中,PATCH 版本是 `3`。 + +所以,你应该像这样指定版本: + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +不兼容变更和新特性在 "MINOR" 版本中添加。 + +!!! tip + "MINOR" 是版本号中间的数字,例如,在 `0.2.3` 中,MINOR 版本是 `2`。 + +### 更新 FaseAPI 版本 + +你应该为你的应用添加测试。 + +使用 **FastAPI** 测试应用非常容易(归功于 Starlette),查看文档:[测试](tutorial/testing.md){.internal-link target=_blank} + +有了测试之后,就可以将 **FastAPI** 更新到最近的一个的版本,然后通过运行测试来确定你所有代码都可以正确工作。 + +如果一切正常,或者做了必要的修改之后,所有的测试都通过了,就可以把 `FastAPI` 版本指定为那个比较新的版本了。 + +### 关于 Starlette + +不要指定 `starlette` 的版本。 + +不同版本的 **FastAPI** 会使用特定版本的 Starlette。 + +所以你只要让 **FastAPI** 自行选择正确的 Starlette 版本。 + +### 关于 Pydantic + +Pydantic 自身的测试中已经包含了 **FastAPI** 的测试,所以最新版本的 Pydantic (`1.0.0` 以上版本)总是兼容 **FastAPI**。 + +你可以指定 Pydantic 为任意一个高于 `1.0.0` 且低于的 `2.0.0` 的版本。 + +例如: + +```txt +pydantic>=1.2.0,<2.0.0 +``` + +## Docker + +这部分,你将通过指引和链接了解到: + +* 如何将你的 **FastAPI** 应用制作成最高性能的 **Docker** 映像/容器。约需五分钟。 +* (可选)理解作为一个开发者需要知道的 HTTPS 相关知识。 +* 使用自动化 HTTPS 设置一个 Docker Swarm 模式的集群,即使是在一个简单的 $5 USD/month 的服务器上。约需要 20 分钟。 +* 使用 Docker Swarm 集群以及 HTTP 等等,生成和部署一个完整的 **FastAPI** 应用。约需 10 分钟。 + +可以使用 **Docker** 进行部署。它具有安全性、可复制性、开发简单性等优点。 + +如果你正在使用 Docker,你可以使用官方 Docker 镜像: + +### tiangolo/uvicorn-gunicorn-fastapi + +该映像包含一个「自动调优」的机制,这样就可以仅仅添加代码就能自动获得超高性能,而不用做出牺牲。 + +不过你仍然可以使用环境变量或配置文件更改和更新所有配置。 + +!!! tip + 查看全部配置和选项,请移步 Docker 镜像页面:tiangolo/uvicorn-gunicorn-fastapi。 + +### 创建 `Dockerfile` + +* 进入你的项目目录。 +* 使用如下命令创建一个 `Dockerfile`: + +```Dockerfile +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 + +COPY ./app /app +``` + +#### 大型应用 + +如果遵循创建 [多文件大型应用](tutorial/bigger-applications.md){.internal-link target=_blank} 的章节,你的 Dockerfile 可能看起来是这样: + +```Dockerfile +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 + +COPY ./app /app/app +``` + +#### 树莓派以及其他架构 + +如果你在树莓派或者任何其他架构中运行 Docker,可以基于 Python 基础镜像(它是多架构的)从头创建一个 `Dockerfile` 并单独使用 Uvicorn。 + +这种情况下,你的 `Dockerfile` 可能是这样的: + +```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"] +``` + +### 创建 **FastAPI** 代码 + +* 创建一个 `app` 目录并进入该目录。 +* 创建一个 `main.py` 文件,内容如下: + +```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} +``` + +* 现在目录结构如下: + +``` +. +├── app +│ └── main.py +└── Dockerfile +``` + +### 构建 Docker 镜像 + +* 进入项目目录(在 `Dockerfile` 所在的位置,包含 `app` 目录) +* 构建 **FastAPI** 镜像 + +
    + +```console +$ docker build -t myimage . + +---> 100% +``` + +
    + +### 启动 Docker 容器 + +* 运行基于你的镜像容器: + +
    + +```console +$ docker run -d --name mycontainer -p 80:80 myimage +``` + +
    + +现在你在 Docker 容器中有了一个根据当前服务器(和CPU核心的数量)自动优化好的 FastAPI 服务器。 + +### 检查一下 + +你应该能够在 Docker 容器的 URL 中检查它。例如:http://192.168.99.100/items/5?q=somequery 或者 http://127.0.0.1/items/5?q=somequery (或者类似的,使用Docker主机)。 + +得到类似的输出: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +### 交互式 API 文档 + +现在可以访问 http://192.168.99.100/docs 或者 http://127.0.0.1/docs (或者类似的,使用Docker主机)。 + +你会看到一个交互式的 API 文档 (由 Swagger UI 提供): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### 可选的 API 文档 + +你也可以访问 http://192.168.99.100/redoc 或者 http://127.0.0.1/redoc (或者类似的,使用Docker主机)。 + +你将看到一个可选的自动化文档(由 ReDoc 提供) + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## HTTPS + +### 关于 HTTPS + +我们当然可以假设 HTTPS 只是某种「启用」或「不启用」的东西。 + +但是事实比这要复杂的多。 + +!!! tip + 如果你着急或者不关心这部分内容,请继续按照下一章节的步骤进行配置。 + +要从用户的角度学习 HTTPS 的基础,请移步 https://howhttps.works/。 + +从开发人员的角度来看,在考虑 HTTPS 时有以下几点需要注意: + +* 对 HTTPS 来说,服务端需要有第三方生成的「证书」。 + * 实际上这些证书是从第三方获取的,而非「生成」的。 +* 证书有生命周期。 + * 证书会过期。 + * 证书过期之后需要更新,重新从第三方获取。 +* 连接的加密发生在 TCP 层。 + * TCP 层在 HTTP 之下一层。 + * 因此,证书和加密处理在 HTTP 之前完成。 +* TCP 不知道「域名」,只知道 IP 地址。 + * 指定域名的请求信息在 HTTP 的数据中。 +* HTTPS 证书「认证」某个特定域名,但是协议和加密在知道要处理哪个域名之前就已经在 TCP 层发生了。 +* 默认情况下,一个 IP 地址仅有一个 HTTPS 证书。 + * 无论你的服务器大小,都是如此。 + * 但是对此有解决办法。 +* TSL 协议(在 TCP 层处理加密的协议,发生在 HTTP 之前)有一个扩展,叫 SNI。 + * SNI 扩展允许一个服务器(一个 IP 地址)有多个 HTTPS 证书,为多个 HTTPS 域名/应用 提供服务。 + * 要使其工作,服务器运行的单一组件(程序)监听公网 IP 地址,所有 HTTPS 证书必须都在该服务器上。 +* 在获得一个安全连接之后,通讯协议仍然是 HTTP。 + * HTTP 内容是加密的,即使这些内容使用 HTTP 协议传输。 + +常见的做法是在服务器(机器,主机等等)上运行一个程序或 HTTP 服务来管理所有的 HTTPS 部分:将解密后的 HTTP 请求发送给在同一服务器运行的真实 HTTP 应用(在这里是 **FastAPI** 应用),从应用获得 HTTP 响应,使用适当的证书加密响应然后使用 HTTPS 将其发回客户端。这个服务器常被称作 TLS 终止代理。 + + +### Let's Encrypt + +在 Let's Encrypt 出现之前,这些 HTTPS 证书由受信任的第三方出售。 + +获取这些证书的过程曾经非常繁琐,需要大量的文书工作,而且证书的价格也相当昂贵。 + +但是紧接着 Let's Encrypt 被创造了。 + +这是一个来自 Linux 基金会的项目。它以自动化的方式免费提供 HTTPS 证书。这些证书使用所有的标准加密措施,且证书生命周期很短(大约 3 个月),正是由于它们生命周期的减短,所以实际上安全性更高。 + +对域名进行安全验证并自动生成证书。同时也允许自动更新这些证书。 + +其想法是自动获取和更新这些证书,这样就可以一直免费获得安全的 HTTPS。 + +### Traefik + +Traefik 是一个高性能的反向代理/负载均衡器。它能够完成「TLS 终止代理」的工作(其他特性除外)。 + +Traefik 集成了 Let's Encrypt,所以能够处理全部 HTTPS 的部分,包括证书获取与更新。 + +Traefik 也集成了 Docker,所以你也可以在每个应用的配置中声明你的域名并可以让它读取这些配置,生成 HTTPS 证书并自动将 HTTPS 提供给你的应用程序,而你不需要对其配置进行任何更改。 + +--- + +有了这些信息和工具,就可以进入下一节把所有内容结合到一起。 + +## 通过 Traefik 和 HTTPS 搭建 Docker Swarm mode 集群 + +通过一个主要的 Traefik 来处理 HTTPS (包括证书获取和更新),大约 20 分钟就可以搭建好一个 Docker Swarm mode 集群。 + +借助 Docker Swarm mode,你可以从单个机器的集群开始(甚至可以是 $5 /月的服务器),然后你可以根据需要添加更多的服务器来进行扩展。 + +要使用 Traefik 和 HTTPS 处理来构建 Docker Swarm Mode 集群,请遵循以下指南: + +### Docker Swarm Mode 和 Traefik 用于 HTTPS 集群 + +### 部署一个 FastAPI 应用 + +部署的最简单方式就是使用 [**FastAPI** 项目生成器](project-generation.md){.internal-link target=_blank}。 + +它被设计成与上述带有 Traefik 和 HTTPS 的 Docker Swarm 集群整合到一起。 + +你可以在大概两分钟内生成一个项目。 + +生成的项目有部署说明,需要再花两分钟部署项目。 + +## 或者,不用 Docker 部署 **FastAPI** + +你也可以不用 Docker 直接部署 **FastAPI**。 + +只需要安装一个兼容 ASGI 的服务器: + +* Uvicorn,一个轻量快速的 ASGI 服务器,基于 uvloop 和 httptools 构建。 + +
    + +```console +$ pip install uvicorn + +---> 100% +``` + +
    + +* Hypercorn,一个也兼容 HTTP/2 的 ASGI 服务器。 + +
    + +```console +$ pip install hypercorn + +---> 100% +``` + +
    + +...或者任何其他的 ASGI 服务器。 + +然后使用教程中同样的方式来运行你的应用,但是不要加 `--reload` 选项,比如: + +
    + +```console +$ uvicorn main:app --host 0.0.0.0 --port 80 + +INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) +``` + +
    + +或者使用 Hypercorn: + +
    + +```console +$ hypercorn main:app --bind 0.0.0.0:80 + +Running on 0.0.0.0:8080 over http (CTRL + C to quit) +``` + +
    + +也许你想编写一些工具来确保它停止时会自动重启。 + +或者安装 Gunicorn将其作为 Uvicorn 的管理器,或者使用多职程(worker)的 Hypercorn。 + +或者保证精确调整职程的数量等等。 + +但是如果你正做这些,你可能只需要使用 Docker 镜像就能够自动做到这些了。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 12cd53dc481d8..4f541075090e1 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -27,6 +27,7 @@ nav: - features.md - 教程 - 用户指南: - tutorial/index.md +- deployment.md markdown_extensions: - toc: permalink: true From f7a87cd6baaabb900a204c214d12b88acef42c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 12:47:44 +0200 Subject: [PATCH 090/153] :memo: Update release notes --- docs/en/docs/release-notes.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 86d4c31ff6a8f..1a5adbebc6916 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,8 +2,9 @@ ## Latest changes -* Add translation for [Tutorial - User Guide - Intro - 教程 - 用户指南 - 简介](https://fastapi.tiangolo.com/zh/tutorial/). PR [#1202](https://github.com/tiangolo/fastapi/pull/1202) by [@waynerv](https://github.com/waynerv). -* Add translation for [Features - 特性](https://fastapi.tiangolo.com/zh/features/). PR [#1192](https://github.com/tiangolo/fastapi/pull/1192) by [@Dustyposa](https://github.com/Dustyposa). +* Add translation to Chinese for [Deployment - 部署](https://fastapi.tiangolo.com/zh/deployment/). PR [#1203](https://github.com/tiangolo/fastapi/pull/1203) by [@RunningIkkyu](https://github.com/RunningIkkyu). +* Add translation to Chinese for [Tutorial - User Guide - Intro - 教程 - 用户指南 - 简介](https://fastapi.tiangolo.com/zh/tutorial/). PR [#1202](https://github.com/tiangolo/fastapi/pull/1202) by [@waynerv](https://github.com/waynerv). +* Add translation to Chinese for [Features - 特性](https://fastapi.tiangolo.com/zh/features/). PR [#1192](https://github.com/tiangolo/fastapi/pull/1192) by [@Dustyposa](https://github.com/Dustyposa). * Add translation for [main page to Chinese](https://fastapi.tiangolo.com/zh/) PR [#1191](https://github.com/tiangolo/fastapi/pull/1191) by [@waynerv](https://github.com/waynerv). * Update docs for project generation. PR [#1287](https://github.com/tiangolo/fastapi/pull/1287). * Add Spanish translation for [Introducción a los Tipos de Python (Python Types Intro)](https://fastapi.tiangolo.com/es/python-types/). PR [#1237](https://github.com/tiangolo/fastapi/pull/1237) by [@mariacamilagl](https://github.com/mariacamilagl). From 9812684178eaaf54e6fa20f85b7f024ca4a457a7 Mon Sep 17 00:00:00 2001 From: MartinEliasQ Date: Sat, 16 May 2020 06:02:20 -0500 Subject: [PATCH 091/153] =?UTF-8?q?=F0=9F=8C=90=20Add=20Spanish=20translat?= =?UTF-8?q?ion=20for=20the=20tutorial-user-guide=20index=20page=20(#1244)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Spanish translation for the tutorial-user-guide index page * Improve some parts of the text in terms of writing * Change the wording to keep the documentation consistent. * :memo: Add small wording and consistency changes * :art: Apply the same consistency changes to EN 🤷 Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/index.md | 4 +- docs/es/docs/tutorial/index.md | 80 ++++++++++++++++++++++++++++++++++ docs/es/mkdocs.yml | 2 + 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 docs/es/docs/tutorial/index.md diff --git a/docs/en/docs/tutorial/index.md b/docs/en/docs/tutorial/index.md index 8fb93ab2b67c8..107961d3b2de3 100644 --- a/docs/en/docs/tutorial/index.md +++ b/docs/en/docs/tutorial/index.md @@ -75,6 +75,6 @@ There is also an **Advanced User Guide** that you can read later after this **Tu The **Advanced User Guide**, builds on this, uses the same concepts, and teaches you some extra features. -But you should first read the **Tutorial - User guide** (what you are reading right now). +But you should first read the **Tutorial - User Guide** (what you are reading right now). -It's designed so that you can build a complete application with just the **Tutorial - User guide**, and then extend it in different ways, depending on your needs, using some of the additional ideas from the **Advanced User Guide**. +It's designed so that you can build a complete application with just the **Tutorial - User Guide**, and then extend it in different ways, depending on your needs, using some of the additional ideas from the **Advanced User Guide**. diff --git a/docs/es/docs/tutorial/index.md b/docs/es/docs/tutorial/index.md new file mode 100644 index 0000000000000..a4dc5fe145bcf --- /dev/null +++ b/docs/es/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Tutorial - Guía de Usuario - Introducción + +Este tutorial te muestra cómo usar **FastAPI** con la mayoría de sus características paso a paso. + +Cada sección se basa gradualmente en las anteriores, pero está estructurada en temas separados, así puedes ir directamente a cualquier tema en concreto para resolver tus necesidades específicas sobre la API. + +También está diseñado para funcionar como una referencia futura. + +Para que puedas volver y ver exactamente lo que necesitas. + +## Ejecuta el código + +Todos los bloques de código se pueden copiar y usar directamente (en realidad son archivos Python probados). + +Para ejecutar cualquiera de los ejemplos, copia el código en un archivo llamado `main.py`, y ejecuta `uvicorn` de la siguiente manera en tu terminal: + +
    + +```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. +``` + +
    + +Se **RECOMIENDA** que escribas o copies el código, lo edites y lo ejecutes localmente. + +Usarlo en tu editor de código es lo que realmente te muestra los beneficios de FastAPI, al ver la poca cantidad de código que tienes que escribir, todas las verificaciones de tipo, autocompletado, etc. + +--- + +## Instala FastAPI + +El primer paso es instalar FastAPI. + +Para el tutorial, es posible que quieras instalarlo con todas las dependencias y características opcionales: + +
    + +```console +$ pip install fastapi[all] + +---> 100% +``` + +
    + +...eso también incluye `uvicorn` que puedes usar como el servidor que ejecuta tu código. + +!!! nota + También puedes instalarlo parte por parte. + + Esto es lo que probablemente harías una vez que desees implementar tu aplicación en producción: + + ``` + pip install fastapi + ``` + + También debes instalar `uvicorn` para que funcione como tu servidor: + + ``` + pip install uvicorn + ``` + + Y lo mismo para cada una de las dependencias opcionales que quieras utilizar. + +## Guía Avanzada de Usuario + +También hay una **Guía Avanzada de Usuario** que puedes leer luego de este **Tutorial - Guía de Usuario**. + +La **Guía Avanzada de Usuario**, se basa en este tutorial, utiliza los mismos conceptos y enseña algunas características adicionales. + +Pero primero deberías leer el **Tutorial - Guía de Gsuario** (lo que estas leyendo ahora mismo). + +La guía esa diseñada para que puedas crear una aplicación completa con solo el **Tutorial - Guía de Usuario**, y luego extenderlo de diferentes maneras, según tus necesidades, utilizando algunas de las ideas adicionales de la **Guía Avanzada de Usuario**. diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 6970862049b41..50edbcf95156d 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -26,6 +26,8 @@ nav: - zh: /zh/ - features.md - python-types.md +- "Tutorial - Gu\xEDa de usuario": + - tutorial/index.md markdown_extensions: - toc: permalink: true From 761e5ff01dacd285ab9159ebecaa03eade1c982f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 13:05:21 +0200 Subject: [PATCH 092/153] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Spanish=20MkDocs?= =?UTF-8?q?=20title?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/es/mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 50edbcf95156d..1d2756bd1dd25 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -26,7 +26,7 @@ nav: - zh: /zh/ - features.md - python-types.md -- "Tutorial - Gu\xEDa de usuario": +- Tutorial - Guía de Usuario: - tutorial/index.md markdown_extensions: - toc: From 2d013b8340e7b63a83ba9dcc099a191bd471d545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 13:06:28 +0200 Subject: [PATCH 093/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1a5adbebc6916..5d6b2320b12de 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Spanish for [Tutorial - User Guide - Intro - Tutorial - Guía de Usuario - Introducción](https://fastapi.tiangolo.com/es/tutorial/). PR [#1244](https://github.com/tiangolo/fastapi/pull/1244) by [@MartinEliasQ](https://github.com/MartinEliasQ). * Add translation to Chinese for [Deployment - 部署](https://fastapi.tiangolo.com/zh/deployment/). PR [#1203](https://github.com/tiangolo/fastapi/pull/1203) by [@RunningIkkyu](https://github.com/RunningIkkyu). * Add translation to Chinese for [Tutorial - User Guide - Intro - 教程 - 用户指南 - 简介](https://fastapi.tiangolo.com/zh/tutorial/). PR [#1202](https://github.com/tiangolo/fastapi/pull/1202) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Features - 特性](https://fastapi.tiangolo.com/zh/features/). PR [#1192](https://github.com/tiangolo/fastapi/pull/1192) by [@Dustyposa](https://github.com/Dustyposa). From 121e87b3e0d3f2e786505d0d8f1f9eb16b1b1f7d Mon Sep 17 00:00:00 2001 From: Marcos Monteiro Date: Sat, 16 May 2020 09:37:17 -0300 Subject: [PATCH 094/153] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20trans?= =?UTF-8?q?lation=20for=20Features=20(#1248)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translate features.md file to Portuguese * Changes word of features.md translation to Portuguese * Fixing typos and bad wording Thanks @Serrones for the kind review --- docs/pt/docs/features.md | 202 +++++++++++++++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 203 insertions(+) create mode 100644 docs/pt/docs/features.md diff --git a/docs/pt/docs/features.md b/docs/pt/docs/features.md new file mode 100644 index 0000000000000..221ebfbf14861 --- /dev/null +++ b/docs/pt/docs/features.md @@ -0,0 +1,202 @@ +# Recursos + +## Recursos do FastAPI + +**FastAPI** te oferece o seguinte: + +### Baseado em padrões abertos + +* OpenAPI para criação de APIs, incluindo declarações de operações de caminho, parâmetros, requisições de corpo, segurança etc. +* Modelo de documentação automática com JSON Schema (já que o OpenAPI em si é baseado no JSON Schema). +* Projetado em cima desses padrões após um estudo meticuloso, em vez de uma reflexão breve. +* Isso também permite o uso de **geração de código do cliente** automaticamente em muitas linguagens. + +### Documentação automática + +Documentação interativa da API e navegação _web_ da interface de usuário. Como o _framework_ é baseado no OpenAPI, há várias opções, 2 incluídas por padrão. + +* Swagger UI, com navegação interativa, chame e teste sua API diretamente do navegador. + +![Interação Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Documentação alternativa da API com ReDoc. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Apenas Python moderno + +Tudo é baseado no padrão das declarações de **tipos do Python 3.6** (graças ao Pydantic). Nenhuma sintaxe nova para aprender. Apenas o padrão moderno do Python. + +Se você precisa refrescar a memória rapidamente sobre como usar tipos do Python (mesmo que você não use o FastAPI), confira esse rápido tutorial: [Tipos do Python](python-types.md){.internal-link target=_blank}. + +Você escreve Python padrão com tipos: + +```Python +from typing import List, Dict +from datetime import date + +from pydantic import BaseModel + +# Declare uma variável como str +# e obtenha suporte do editor dentro da função +def main(user_id: str): + return user_id + + +# Um modelo do Pydantic +class User(BaseModel): + id: int + name: str + joined: date +``` + +Que então pode ser usado como: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +!!! info + `**second_user_data` quer dizer: + + Passe as chaves e valores do dicionário `second_user_data` diretamente como argumentos chave-valor, equivalente a: `User(id=4, name="Mary", joined="2018-11-30")` + +### Suporte de editores + +Todo o _framework_ foi projetado para ser fácil e intuitivo de usar, todas as decisões foram testadas em vários editores antes do início do desenvolvimento, para garantir a melhor experiência de desenvolvimento. + +Na última pesquisa do desenvolvedor Python ficou claro que o recurso mais utilizado é o "auto completar". + +Todo o _framework_ **FastAPI** é feito para satisfazer isso. Auto completação funciona em todos os lugares. + +Você raramente precisará voltar à documentação. + +Aqui está como o editor poderá te ajudar: + +* no Visual Studio Code: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* no PyCharm: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +Você terá completação do seu código que você poderia considerar impossível antes. Como por exemplo, a chave `price` dentro do corpo JSON (que poderia ter sido aninhado) que vem de uma requisição. + +Sem a necessidade de digitar nomes de chaves erroneamente, ir e voltar entre documentações, ou rolar pela página para descobrir se você utilizou `username` or `user_name`. + +### Breve + +Há **padrões** sensíveis para tudo, com configurações adicionais em todos os lugares. Todos os parâmetros podem ser regulados para fazer o que você precisa e para definir a API que você necessita. + +Por padrão, tudo **"simplesmente funciona"**. + +### Validação + +* Validação para a maioria dos (ou todos?) **tipos de dados** do Python, incluindo: + * objetos JSON (`dict`). + * arrays JSON (`list`), definindo tipos dos itens. + * campos String (`str`), definindo tamanho mínimo e máximo. + * Numbers (`int`, `float`) com valores mínimos e máximos, etc. + +* Validação de tipos mais exóticos, como: + * URL. + * Email. + * UUID. + * ...e outros. + +Toda a validação é controlada pelo robusto e bem estabelecido **Pydantic**. + +### Segurança e autenticação + +Segurança e autenticação integradas. Sem nenhum compromisso com bancos de dados ou modelos de dados. + +Todos os esquemas de seguranças definidos no OpenAPI, incluindo: + +* HTTP Basic. +* **OAuth2** (também com **tokens JWT**). Confira o tutorial em [OAuth2 com JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* Chaves de API em: + * Headers. + * parâmetros da Query. + * Cookies etc. + +Além disso, todos os recursos de seguranças do Starlette (incluindo **cookies de sessão**). + +Tudo construído como ferramentas e componentes reutilizáveis que são fáceis de integrar com seus sistemas, armazenamento de dados, banco de dados relacionais e não-relacionais etc. + +### Injeção de dependência + +FastAPI inclui um sistema de injeção de dependência extremamente fácil de usar, mas extremamente poderoso. + +* Mesmo dependências podem ter dependências, criando uma hierarquia ou **"grafo" de dependências**. +* Tudo **automaticamente controlado** pelo _framework_. +* Todas as dependências podem pedir dados das requisições e **ampliar** as restrições e documentação automática da **operação de caminho**. +* **Validação automática** mesmo para parâmetros da *operação de caminho* definidos em dependências. +* Suporte para sistemas de autenticação complexos, **conexões com banco de dados** etc. +* **Sem comprometer** os bancos de dados, _frontends_ etc. Mas fácil integração com todos eles. + +### "Plug-ins" ilimitados + +Ou, de outra forma, sem a necessidade deles, importe e use o código que precisar. + +Qualquer integração é projetada para ser tão simples de usar (com dependências) que você pode criar um "plug-in" para suas aplicações com 2 linhas de código usando a mesma estrutura e sintaxe para as suas *operações de caminho*. + +### Testado + +* 100% de cobertura de testes. +* 100% do código utiliza type annotations. +* Usado para aplicações em produção. + +## Recursos do Starlette + +**FastAPI** é totalmente compatível com (e baseado no) Starlette. Então, qualquer código adicional Starlette que você tiver, também funcionará. + +`FastAPI` é na verdade uma sub-classe do `Starlette`. Então, se você já conhece ou usa Starlette, a maioria das funcionalidades se comportará da mesma forma. + +Com **FastAPI**, você terá todos os recursos do **Starlette** (já que FastAPI é apenas um Starlette com esteróides): + +* Desempenho realmente impressionante. É um dos _frameworks_ Python disponíveis mais rápidos, a par com o **NodeJS** e **Go**. +* Suporte a **WebSocket**. +* Suporte a **GraphQL**. +* Tarefas em processo _background_. +* Eventos na inicialização e encerramento. +* Cliente de testes construído sobre `requests`. +* Respostas em **CORS**, GZip, Static Files, Streaming. +* Suporte a **Session e Cookie**. +* 100% de cobertura de testes. +* 100% do código utilizando _type annotations_. + +## Recursos do Pydantic + +**FastAPI** é totalmente compatível com (e baseado no) Pydantic. Então, qualquer código Pydantic adicional que você tiver, também funcionará. + +Incluindo bibliotecas externas também baseadas no Pydantic, como ORMs e ODMs para bancos de dados. + +Isso também significa que em muitos casos você poderá passar o mesmo objeto que você receber de uma requisição **diretamente para o banco de dados**, já que tudo é validado automaticamente. + +O mesmo se aplica no sentido inverso, em muitos casos você poderá simplesmente passar o objeto que você recebeu do banco de dados **diretamente para o cliente**. + +Com **FastAPI** você terá todos os recursos do **Pydantic** (já que FastAPI utiliza o Pydantic para todo o controle dos dados): + +* **Sem pegadinhas**: + * Sem novas definições de esquema de micro-linguagem para aprender. + * Se você conhece os tipos do Python, você sabe como usar o Pydantic. +* Vai bem com o/a seu/sua **IDE/linter/cérebro**: + * Como as estruturas de dados do Pydantic são apenas instâncias de classes que você define, a auto completação, _linting_, _mypy_ e a sua intuição devem funcionar corretamente com seus dados validados. +* **Rápido**: + * em _benchmarks_, o Pydantic é mais rápido que todas as outras bibliotecas testadas. +* Valida **estruturas complexas**: + * Use modelos hierárquicos do Pydantic, `List` e `Dict` do `typing` do Python, etc. + * Validadores permitem que esquemas de dados complexos sejam limpos e facilmente definidos, conferidos e documentados como JSON Schema. + * Você pode ter **JSONs aninhados** profundamente e tê-los todos validados e anotados. +* **Extensível**: + * Pydantic permite que tipos de dados personalizados sejam definidos ou você pode estender a validação com métodos em um modelo decorado com seu decorador de validador. +* 100% de cobertura de testes. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 70e8dd106d47f..18c900d2d1f30 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -24,6 +24,7 @@ nav: - es: /es/ - pt: /pt/ - zh: /zh/ +- features.md markdown_extensions: - toc: permalink: true From f71ba8885e8544606a6116e1b966762dd23d3101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 14:39:54 +0200 Subject: [PATCH 095/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5d6b2320b12de..ab335538d4ebd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Portuguese for [Features - Recursos](https://fastapi.tiangolo.com/pt/features/). PR [#1248](https://github.com/tiangolo/fastapi/pull/1248) by [@marcosmmb](https://github.com/marcosmmb). * Add translation to Spanish for [Tutorial - User Guide - Intro - Tutorial - Guía de Usuario - Introducción](https://fastapi.tiangolo.com/es/tutorial/). PR [#1244](https://github.com/tiangolo/fastapi/pull/1244) by [@MartinEliasQ](https://github.com/MartinEliasQ). * Add translation to Chinese for [Deployment - 部署](https://fastapi.tiangolo.com/zh/deployment/). PR [#1203](https://github.com/tiangolo/fastapi/pull/1203) by [@RunningIkkyu](https://github.com/RunningIkkyu). * Add translation to Chinese for [Tutorial - User Guide - Intro - 教程 - 用户指南 - 简介](https://fastapi.tiangolo.com/zh/tutorial/). PR [#1202](https://github.com/tiangolo/fastapi/pull/1202) by [@waynerv](https://github.com/waynerv). From 406b3ac8051ce4dfe64eae8b3916d897eafc4021 Mon Sep 17 00:00:00 2001 From: Marcos Monteiro Date: Sat, 16 May 2020 09:46:49 -0300 Subject: [PATCH 096/153] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20trans?= =?UTF-8?q?lation=20for=20the=20history-design-future=20page=20(#1249)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translate history-design-future.md to Portuguese * Update docs/pt/docs/history-design-future.md Co-Authored-By: Cássio Botaro * 📝 Capitalize title Co-authored-by: Cássio Botaro Co-authored-by: Sebastián Ramírez --- docs/pt/docs/history-design-future.md | 79 +++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 80 insertions(+) create mode 100644 docs/pt/docs/history-design-future.md diff --git a/docs/pt/docs/history-design-future.md b/docs/pt/docs/history-design-future.md new file mode 100644 index 0000000000000..45427ec630735 --- /dev/null +++ b/docs/pt/docs/history-design-future.md @@ -0,0 +1,79 @@ +# História, Design e Futuro + +Há algum tempo, um usuário **FastAPI** perguntou: + +> Qual é a história desse projeto? Parece que surgiu do nada e se tornou incrível em poucas semanas [...] + +Aqui está um pouco dessa história. + +## Alternativas + +Eu tenho criado APIs com requisitos complexos por vários anos (Aprendizado de Máquina, sistemas distribuídos, tarefas assíncronas, banco de dados NoSQL etc.), liderando vários times de desenvolvedores. + +Como parte disso, eu precisava investigar, testar e usar muitas alternativas. + +A história do **FastAPI** é, em grande parte, a história de seus predecessores. + +Como dito na seção [Alternativas](alternatives.md){.internal-link target=_blank}: + +
    + +**FastAPI** não existiria se não pelo trabalho anterior de outros. + +Há muitas ferramentas criadas antes que ajudaram a inspirar sua criação. + +Eu estive evitando a criação de um novo _framework_ por vários anos. Primeiro tentei resolver todas as funcionalidades cobertas por **FastAPI** usando muitos _frameworks_, _plug-ins_ e ferramentas diferentes. + +Mas em algum ponto, não havia outra opção senão criar algo que oferecia todas as funcionalidades, aproveitando as melhores ideias de ferramentas anteriores, e combinando-as da melhor maneira possível, usando funcionalidades da linguagem que nem estavam disponíveis antes (_type hints_ do Python 3.6+). + +
    + +## Investigação + +Ao usar todas as alternativas anteriores, eu tive a chance de aprender com todas elas, aproveitar ideias e combiná-las da melhor maneira que encontrei para mim e para os times de desenvolvedores com os quais trabalhava. + +Por exemplo, estava claro que idealmente ele deveria ser baseado nos _type hints_ padrões do Python. + +Também, a melhor abordagem era usar padrões já existentes. + +Então, antes mesmo de começar a codificar o **FastAPI**, eu investi vários meses estudando as especificações do OpenAPI, JSON Schema, OAuth2 etc. Entendendo suas relações, sobreposições e diferenças. + +## Design + +Eu então dediquei algum tempo projetando a "API" de desenvolvimento que eu queria como usuário (como um desenvolvedor usando o FastAPI). + +Eu testei várias ideias nos editores Python mais populares: PyCharm, VS Code, e editores baseados no Jedi. + +Pela última Pesquisa do Desenvolvedor Python, isso cobre cerca de 80% dos usuários. + +Isso significa que o **FastAPI** foi testado especificamente com os editores usados por 80% dos desenvolvedores Python. Como a maioria dos outros editores tendem a funcionar de forma similar, todos os seus benefícios devem funcionar para virtualmente todos os editores. + +Dessa forma eu pude encontrar a melhor maneira de reduzir duplicação de código o máximo possível, ter completação de texto em todos os lugares, conferência de tipos e erros etc. + +Tudo de uma forma que oferecesse a melhor experiência de desenvolvimento para todos os desenvolvedores. + +## Requisitos + +Após testar várias alternativas, eu decidi que usaria o **Pydantic** por suas vantagens. + +Então eu contribuí com ele, para deixá-lo completamente de acordo com o JSON Schema, para dar suporte a diferentes maneiras de definir declarações de restrições, e melhorar o suporte a editores (conferências de tipos, auto completações) baseado nos testes em vários editores. + +Durante o desenvolvimento, eu também contribuí com o **Starlette**, outro requisito chave. + +## Desenvolvimento + +Quando comecei a criar o **FastAPI** de fato, a maior parte das peças já estavam encaixadas, o design estava definido, os requisitos e ferramentas já estavam prontos, e o conhecimento sobre os padrões e especificações estavam claros e frescos. + +## Futuro + +Nesse ponto, já está claro que o **FastAPI** com suas ideias está sendo útil para muitas pessoas. + +Ele foi escolhido sobre outras alternativas anteriores por se adequar melhor em muitos casos. + +Muitos desenvolvedores e times já dependem do **FastAPI** para seus projetos (incluindo eu e meu time). + +Mas ainda há muitas melhorias e funcionalidades a vir. + +**FastAPI** tem um grande futuro à frente. + +E [sua ajuda](help-fastapi.md){.internal-link target=_blank} é muito bem-vinda. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 18c900d2d1f30..4b6337eb4e1f6 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -25,6 +25,7 @@ nav: - pt: /pt/ - zh: /zh/ - features.md +- history-design-future.md markdown_extensions: - toc: permalink: true From 89f36371b96e236d7f71ae07ee607e6d80f062de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 14:48:56 +0200 Subject: [PATCH 097/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ab335538d4ebd..439b284fc1035 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Portuguese for [History, Design and Future - História, Design e Futuro](https://fastapi.tiangolo.com/pt/history-design-future/). PR [#1249](https://github.com/tiangolo/fastapi/pull/1249) by [@marcosmmb](https://github.com/marcosmmb). * Add translation to Portuguese for [Features - Recursos](https://fastapi.tiangolo.com/pt/features/). PR [#1248](https://github.com/tiangolo/fastapi/pull/1248) by [@marcosmmb](https://github.com/marcosmmb). * Add translation to Spanish for [Tutorial - User Guide - Intro - Tutorial - Guía de Usuario - Introducción](https://fastapi.tiangolo.com/es/tutorial/). PR [#1244](https://github.com/tiangolo/fastapi/pull/1244) by [@MartinEliasQ](https://github.com/MartinEliasQ). * Add translation to Chinese for [Deployment - 部署](https://fastapi.tiangolo.com/zh/deployment/). PR [#1203](https://github.com/tiangolo/fastapi/pull/1203) by [@RunningIkkyu](https://github.com/RunningIkkyu). From 046d6b7fa0131abab1711b67397ed07ce1ba816a Mon Sep 17 00:00:00 2001 From: Juan Funez Date: Sat, 16 May 2020 14:58:00 +0200 Subject: [PATCH 098/153] =?UTF-8?q?=F0=9F=8C=90=20Add=20Spanish=20translat?= =?UTF-8?q?ion=20for=20advanced/index.md=20(#1250)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * spanish translation for advanced/index.md * Ajustes sugeridos * ✏️ Capitalize docs title * 🔧 Add config to MkDocs for spanish Co-authored-by: Sebastián Ramírez --- docs/es/docs/advanced/index.md | 18 ++++++++++++++++++ docs/es/mkdocs.yml | 2 ++ 2 files changed, 20 insertions(+) create mode 100644 docs/es/docs/advanced/index.md diff --git a/docs/es/docs/advanced/index.md b/docs/es/docs/advanced/index.md new file mode 100644 index 0000000000000..1bee540f2bdc7 --- /dev/null +++ b/docs/es/docs/advanced/index.md @@ -0,0 +1,18 @@ +# Guía de Usuario Avanzada - Introducción + +## Características Adicionales + +El [Tutorial - Guía de Usuario](../tutorial/){.internal-link target=_blank} principal debe ser suficiente para darte un paseo por todas las características principales de **FastAPI** + +En las secciones siguientes verás otras opciones, configuraciones, y características adicionales. + +!!! tip + Las próximas secciones **no son necesariamente "avanzadas"**. + + Y es posible que para tu caso, la solución se encuentre en una de estas. + +## Lee primero el Tutorial + +Puedes continuar usando la mayoría de las características de **FastAPI** con el conocimiento del [Tutorial - Guía de Usuario](../tutorial/){.internal-link target=_blank} principal. + +En las siguientes secciones se asume que lo has leído y conoces esas ideas principales. diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 1d2756bd1dd25..8832f53f4bdb8 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -28,6 +28,8 @@ nav: - python-types.md - Tutorial - Guía de Usuario: - tutorial/index.md +- Guía de Usuario Avanzada: + - advanced/index.md markdown_extensions: - toc: permalink: true From 22e858f65c06ae7ac21456b0e3ab5fb90d051b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 15:01:10 +0200 Subject: [PATCH 099/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 439b284fc1035..4584d677e57d2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Spanish for [Advanced User Guide - Intro - Guía de Usuario Avanzada - Introducción](https://fastapi.tiangolo.com/es/advanced/). PR [#1250](https://github.com/tiangolo/fastapi/pull/1250) by [@jfunez](https://github.com/jfunez). * Add translation to Portuguese for [History, Design and Future - História, Design e Futuro](https://fastapi.tiangolo.com/pt/history-design-future/). PR [#1249](https://github.com/tiangolo/fastapi/pull/1249) by [@marcosmmb](https://github.com/marcosmmb). * Add translation to Portuguese for [Features - Recursos](https://fastapi.tiangolo.com/pt/features/). PR [#1248](https://github.com/tiangolo/fastapi/pull/1248) by [@marcosmmb](https://github.com/marcosmmb). * Add translation to Spanish for [Tutorial - User Guide - Intro - Tutorial - Guía de Usuario - Introducción](https://fastapi.tiangolo.com/es/tutorial/). PR [#1244](https://github.com/tiangolo/fastapi/pull/1244) by [@MartinEliasQ](https://github.com/MartinEliasQ). From e4f09478217697742163df5a61b45abf6e855853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 15:17:24 +0200 Subject: [PATCH 100/153] =?UTF-8?q?=E2=9C=A8=20Allow=20Unicode=20in=20MkDo?= =?UTF-8?q?cs=20for=20translations=20instead=20of=20escaped=20chars=20(#14?= =?UTF-8?q?19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/docs.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/docs.py b/scripts/docs.py index 73f21371b80ed..33297dd8f7a81 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -93,7 +93,8 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): new_config = get_base_lang_config(lang) new_config_path: Path = Path(new_path) / mkdocs_name new_config_path.write_text( - yaml.dump(new_config, sort_keys=False, width=200), encoding="utf-8" + yaml.dump(new_config, sort_keys=False, width=200, allow_unicode=True), + encoding="utf-8", ) new_config_docs_path: Path = new_path / "docs" new_config_docs_path.mkdir() @@ -177,7 +178,8 @@ def build_lang( lang_config["nav"] = export_lang_nav build_lang_config_path: Path = build_lang_path / mkdocs_name build_lang_config_path.write_text( - yaml.dump(lang_config, sort_keys=False, width=200), encoding="utf-8" + yaml.dump(lang_config, sort_keys=False, width=200, allow_unicode=True), + encoding="utf-8", ) current_dir = os.getcwd() os.chdir(build_lang_path) @@ -295,7 +297,8 @@ def update_config(lang: str): languages.append({name: f"/{name}/"}) config["nav"][1] = {"Languages": languages} config_path.write_text( - yaml.dump(config, sort_keys=False, width=200), encoding="utf-8" + yaml.dump(config, sort_keys=False, width=200, allow_unicode=True), + encoding="utf-8", ) From 4c1b54e2096ac1dac079a391d60468bae91092f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 15:18:18 +0200 Subject: [PATCH 101/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4584d677e57d2..bed60cfa369fe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Allow using Unicode in MkDocs for translations. PR [#1419](https://github.com/tiangolo/fastapi/pull/1419). * Add translation to Spanish for [Advanced User Guide - Intro - Guía de Usuario Avanzada - Introducción](https://fastapi.tiangolo.com/es/advanced/). PR [#1250](https://github.com/tiangolo/fastapi/pull/1250) by [@jfunez](https://github.com/jfunez). * Add translation to Portuguese for [History, Design and Future - História, Design e Futuro](https://fastapi.tiangolo.com/pt/history-design-future/). PR [#1249](https://github.com/tiangolo/fastapi/pull/1249) by [@marcosmmb](https://github.com/marcosmmb). * Add translation to Portuguese for [Features - Recursos](https://fastapi.tiangolo.com/pt/features/). PR [#1248](https://github.com/tiangolo/fastapi/pull/1248) by [@marcosmmb](https://github.com/marcosmmb). From caed37a08fb3efcc1551e30dbe8dbf24a63a4b17 Mon Sep 17 00:00:00 2001 From: Marcos Monteiro Date: Sat, 16 May 2020 10:28:26 -0300 Subject: [PATCH 102/153] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20trans?= =?UTF-8?q?lation=20for=20the=20tutorial/index=20page=20(#1259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translate tutorial/index.md to Portuguese * ✏️ Update capitalization * 🔧 Update docs section title in Portuguese Co-authored-by: Sebastián Ramírez --- docs/pt/docs/tutorial/index.md | 80 ++++++++++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 2 + 2 files changed, 82 insertions(+) create mode 100644 docs/pt/docs/tutorial/index.md diff --git a/docs/pt/docs/tutorial/index.md b/docs/pt/docs/tutorial/index.md new file mode 100644 index 0000000000000..8470927a7ffd2 --- /dev/null +++ b/docs/pt/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Tutorial - Guia de Usuário - Introdução + +Esse tutorial mostra como usar o **FastAPI** com a maior parte de seus recursos, passo a passo. + +Cada seção constrói, gradualmente, sobre as anteriores, mas sua estrutura são tópicos separados, para que você possa ir a qualquer um específico e resolver suas necessidades específicas de API. + +Ele também foi feito como referência futura. + +Então você poderá voltar e ver exatamente o que precisar. + +## Rode o código + +Todos os blocos de código podem ser copiados e utilizados diretamente (eles são, na verdade, arquivos Python testados). + +Para rodar qualquer um dos exemplos, copie o codigo para um arquivo `main.py`, e inicie o `uvivorn` com: + +
    + +```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. +``` + +
    + +É **ALTAMENTE recomendado** que você escreva ou copie o código, edite-o e rode-o localmente. + +Usá-lo em seu editor é o que realmente te mostra os benefícios do FastAPI, ver quão pouco código você tem que escrever, todas as conferências de tipo, auto completações etc. + +--- + +## Instale o FastAPI + +O primeiro passo é instalar o FastAPI. + +Para o tutorial, você deve querer instalá-lo com todas as dependências e recursos opicionais. + +
    + +```console +$ pip install fastapi[all] + +---> 100% +``` + +
    + +...isso também inclui o `uvicorn`, que você pode usar como o servidor que rodará seu código. + +!!! nota + Você também pode instalar parte por parte. + + Isso é provavelmente o que você faria quando você quisesse lançar sua aplicação em produção: + + ``` + pip install fastapi + ``` + + Também instale o `uvicorn` para funcionar como servidor: + + ``` + pip install uvicorn + ``` + + E o mesmo para cada dependência opcional que você quiser usar. + +## Guia Avançado de Usuário + +Há também um **Guia Avançado de Usuário** que você pode ler após esse **Tutorial - Guia de Usuário**. + +O **Guia Avançado de Usuário** constrói sobre esse, usa os mesmos conceitos e te ensina alguns recursos extras. + +Mas você deveria ler primeiro o **Tutorial - Guia de Usuário** (que você está lendo agora). + +Ele foi projetado para que você possa construir uma aplicação completa com apenas o **Tutorial - Guia de Usuário**, e então estendê-la de diferentes formas, dependendo das suas necessidades, usando algumas ideias adicionais do **Guia Avançado de Usuário**. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 4b6337eb4e1f6..1c385ccc53427 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -25,6 +25,8 @@ nav: - pt: /pt/ - zh: /zh/ - features.md +- Tutorial - Guia de Usuário: + - tutorial/index.md - history-design-future.md markdown_extensions: - toc: From cfd2c3017f25134361f334caaf646d9591b1fcc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 15:29:08 +0200 Subject: [PATCH 103/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bed60cfa369fe..953afcad8ac14 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Portuguese for [Tutorial - User Guide - Intro - Tutorial - Guia de Usuário - Introdução](https://fastapi.tiangolo.com/pt/tutorial/). PR [#1259](https://github.com/tiangolo/fastapi/pull/1259) by [@marcosmmb](https://github.com/marcosmmb). * Allow using Unicode in MkDocs for translations. PR [#1419](https://github.com/tiangolo/fastapi/pull/1419). * Add translation to Spanish for [Advanced User Guide - Intro - Guía de Usuario Avanzada - Introducción](https://fastapi.tiangolo.com/es/advanced/). PR [#1250](https://github.com/tiangolo/fastapi/pull/1250) by [@jfunez](https://github.com/jfunez). * Add translation to Portuguese for [History, Design and Future - História, Design e Futuro](https://fastapi.tiangolo.com/pt/history-design-future/). PR [#1249](https://github.com/tiangolo/fastapi/pull/1249) by [@marcosmmb](https://github.com/marcosmmb). From 778822bd9ad16a6f278ddaeeba84247ea1d07765 Mon Sep 17 00:00:00 2001 From: Fabio Serrao Date: Sat, 16 May 2020 10:37:57 -0300 Subject: [PATCH 104/153] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20trans?= =?UTF-8?q?lation=20for=20benchmarks=20(#1274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translation benchmarks * Update docs/pt/docs/benchmarks.md Co-Authored-By: Marcos Monteiro * Update docs/pt/docs/benchmarks.md Co-Authored-By: Marcos Monteiro * Update docs/pt/docs/benchmarks.md Co-Authored-By: Marcos Monteiro * Update docs/pt/docs/benchmarks.md Co-Authored-By: Marcos Monteiro * Update docs/pt/docs/benchmarks.md Co-Authored-By: Marcos Monteiro * Update docs/pt/docs/benchmarks.md Co-Authored-By: Marcos Monteiro * Update docs/pt/docs/benchmarks.md Co-Authored-By: Marcos Monteiro * Update docs/pt/docs/benchmarks.md Co-Authored-By: Marcos Monteiro * Update docs/pt/docs/benchmarks.md Co-Authored-By: Marcos Monteiro * 🔧 Include benchmark translation in MkDocs for Portuguese Co-authored-by: Marcos Monteiro Co-authored-by: Sebastián Ramírez --- docs/pt/docs/benchmarks.md | 34 ++++++++++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 35 insertions(+) create mode 100644 docs/pt/docs/benchmarks.md diff --git a/docs/pt/docs/benchmarks.md b/docs/pt/docs/benchmarks.md new file mode 100644 index 0000000000000..7f7c95ba1c65e --- /dev/null +++ b/docs/pt/docs/benchmarks.md @@ -0,0 +1,34 @@ +# Comparações + +As comparações independentes da TechEmpower mostram as aplicações **FastAPI** rodando com Uvicorn como um dos _frameworks_ Python mais rápidos disponíveis, somente atrás dos próprios Starlette e Uvicorn (utilizados internamente pelo FastAPI). (*) + +Mas quando se checa _benchmarks_ e comparações você deveria ter o seguinte em mente. + +## Comparações e velocidade + +Ao verificar os _benchmarks_, é comum observar algumas ferramentas de diferentes tipos comparadas como equivalentes. + +Especificamente, observa-se Uvicorn, Starlette e FastAPI comparados juntos (entre muitas outras ferramentas). + +Quanto mais simples o problema resolvido pela ferramenta, melhor a performance que ela terá. E a maioria dos _benchmarks_ não testam as características adicionais fornecidas pela ferramenta. + +A hierarquia segue assim: + +* **Uvicorn**: um servidor ASGI + * **Starlette**: (utiliza Uvicorn) um _microframework web_ + * **FastAPI**: (utiliza Starlette) um _microframework_ de API com vários recursos adicionais para construção de APIs, com validação de dados, etc. + +* **Uvicorn**: + * Terá a melhor performance, já que ele não tem muito código extra além do servidor em si. + * Você não conseguiria escrever uma aplicação em Uvicorn diretamente. Isso significa que seu código deveria conter, mais ou menos, todo o código fornecido pelo Starlette (ou **FastAPI**). E se você fizesse isso, sua aplicação final poderia ter a mesma sobrecarga que utilizar um _framework_ que minimiza o código e _bugs_ da sua aplicação. + * Se você quer fazer comparações com o Uvicorn, compare com Daphne, Hypercorn, uWSGI, etc. Servidores de Aplicação. +* **Starlette**: + * Terá a melhor performance, depois do Uvicorn. De fato, Starlette utiliza Uvicorn para rodar. Então, ele provavelmente será "mais lento" que Uvicorn por ter que executar mais código. + * Mas ele fornece a você as ferramentas para construir aplicações _web_ simples, com roteamento baseado em caminhos, etc. + * Se você quer fazer comparações com o Starlette, compare com Sanic, Flask, Django, etc. _Frameworks Web_ (ou _microframeworks_). +* **FastAPI**: + * Do mesmo modo que Starlette utiliza Uvicorn e não pode ser mais rápido que ele, **FastAPI** utiliza o Starlette, então não tem como ser mais rápido do que o Starlette. + * FastAPI fornece mais recursos acima do Starlette. Recursos que você quase sempre precisará quando construir APIs, como validação de dados e serialização. E utilizando eles, você terá uma documentação automática de graça (a documentação automática nem sequer adiciona peso para rodar as aplicações, ela é gerada na inicialização). + * Se você nunca utilizou FastAPI mas utilizou diretamente o Starlette (ou outra ferramenta, como Sanic, Flask, Responder, etc) você teria que implementar toda validação de dados e serialização por conta. Então, sua aplicação final poderia ainda ter a mesma sobrecarga como se fosse desenvolvida com FastAPI. Em muitos casos, a validação de dados e serialização é o maior pedaço de código escrito em aplicações. + * Então, ao utilizar o FastAPI você estará economizando tempo de desenvolvimento, evitará _bugs_, linhas de código, e você provavelmente terá a mesma performance (ou melhor) do que não utilizá-lo (já que você teria que implementar tudo isso em seu código). + * Se você quer fazer comparações com o FastAPI, compare com um _framework_ (ou conjunto de ferramentas) para aplicações _web_ que forneça validação de dados, serialização e documentação, como Flask-apispec, NestJS, Molten, etc. _Frameworks_ com validação de dados automática, serialização e documentação integradas. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 1c385ccc53427..64bdeb9e085ab 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -28,6 +28,7 @@ nav: - Tutorial - Guia de Usuário: - tutorial/index.md - history-design-future.md +- benchmarks.md markdown_extensions: - toc: permalink: true From cfb72eec5ae75371188212fe4f58df4ad3e1e375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 15:41:21 +0200 Subject: [PATCH 105/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 953afcad8ac14..aa0e587a36706 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Portuguese for [Benchmarks - Comparações](https://fastapi.tiangolo.com/pt/benchmarks/). PR [#1274](https://github.com/tiangolo/fastapi/pull/1274) by [@Serrones](https://github.com/Serrones). * Add translation to Portuguese for [Tutorial - User Guide - Intro - Tutorial - Guia de Usuário - Introdução](https://fastapi.tiangolo.com/pt/tutorial/). PR [#1259](https://github.com/tiangolo/fastapi/pull/1259) by [@marcosmmb](https://github.com/marcosmmb). * Allow using Unicode in MkDocs for translations. PR [#1419](https://github.com/tiangolo/fastapi/pull/1419). * Add translation to Spanish for [Advanced User Guide - Intro - Guía de Usuario Avanzada - Introducción](https://fastapi.tiangolo.com/es/advanced/). PR [#1250](https://github.com/tiangolo/fastapi/pull/1250) by [@jfunez](https://github.com/jfunez). From 409264960ecdfb2e96a14f16183734f155e57aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 17:45:12 +0200 Subject: [PATCH 106/153] =?UTF-8?q?=E2=9C=A8=20Allow=20disabling=20docs=20?= =?UTF-8?q?UIs=20by=20disabling=20OpenAPI=20(#1421)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Allow disabling docs UIs by disabling openapi_url * 📝 Add docs for disabling OpenAPI and docs in prod or other environments * ✅ Add tests for disabling OpenAPI and docs --- docs/en/docs/advanced/conditional-openapi.md | 58 +++++++++++++++++++ docs/en/docs/tutorial/metadata.md | 2 +- docs/en/mkdocs.yml | 1 + docs_src/conditional_openapi/tutorial001.py | 16 +++++ fastapi/applications.py | 3 - .../test_conditional_openapi/__init__.py | 0 .../test_tutorial001.py | 46 +++++++++++++++ 7 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 docs/en/docs/advanced/conditional-openapi.md create mode 100644 docs_src/conditional_openapi/tutorial001.py create mode 100644 tests/test_tutorial/test_conditional_openapi/__init__.py create mode 100644 tests/test_tutorial/test_conditional_openapi/test_tutorial001.py diff --git a/docs/en/docs/advanced/conditional-openapi.md b/docs/en/docs/advanced/conditional-openapi.md new file mode 100644 index 0000000000000..add16fbec519e --- /dev/null +++ b/docs/en/docs/advanced/conditional-openapi.md @@ -0,0 +1,58 @@ +# 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 Security through obscurity. + +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: + +
    + +```console +$ OPENAPI_URL= uvicorn main:app + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
    + +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" +} +``` diff --git a/docs/en/docs/tutorial/metadata.md b/docs/en/docs/tutorial/metadata.md index 59e3f5b5ab55d..666fa7648b1c3 100644 --- a/docs/en/docs/tutorial/metadata.md +++ b/docs/en/docs/tutorial/metadata.md @@ -33,7 +33,7 @@ For example, to set it to be served at `/api/v1/openapi.json`: {!../../../docs_src/metadata/tutorial002.py!} ``` -If you want to disable the OpenAPI schema completely you can set `openapi_url=None`. +If you want to disable the OpenAPI schema completely you can set `openapi_url=None`, that will also disable the documentation user interfaces that use it. ## Docs URLs diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 9e13e68c5e063..1dac0fde477fe 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -103,6 +103,7 @@ nav: - advanced/testing-dependencies.md - advanced/testing-database.md - advanced/settings.md + - advanced/conditional-openapi.md - advanced/extending-openapi.md - advanced/openapi-callbacks.md - advanced/wsgi.md diff --git a/docs_src/conditional_openapi/tutorial001.py b/docs_src/conditional_openapi/tutorial001.py new file mode 100644 index 0000000000000..717e723e83a89 --- /dev/null +++ b/docs_src/conditional_openapi/tutorial001.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI +from pydantic import BaseSettings + + +class Settings(BaseSettings): + openapi_url: str = "/openapi.json" + + +settings = Settings() + +app = FastAPI(openapi_url=settings.openapi_url) + + +@app.get("/") +def root(): + return {"message": "Hello World"} diff --git a/fastapi/applications.py b/fastapi/applications.py index 84a1b6de290b3..a5dfa4fdf3236 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -81,9 +81,6 @@ def __init__( if self.openapi_url: assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'" assert self.version, "A version must be provided for OpenAPI, e.g.: '2.1.0'" - - if self.docs_url or self.redoc_url: - assert self.openapi_url, "The openapi_url is required for the docs" self.openapi_schema: Optional[Dict[str, Any]] = None self.setup() diff --git a/tests/test_tutorial/test_conditional_openapi/__init__.py b/tests/test_tutorial/test_conditional_openapi/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py new file mode 100644 index 0000000000000..1914b7c2ce0e0 --- /dev/null +++ b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py @@ -0,0 +1,46 @@ +import importlib + +from fastapi.testclient import TestClient + +from conditional_openapi import tutorial001 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, +} + + +def test_default_openapi(): + client = TestClient(tutorial001.app) + response = client.get("/openapi.json") + assert response.json() == openapi_schema + response = client.get("/docs") + assert response.status_code == 200, response.text + response = client.get("/redoc") + assert response.status_code == 200, response.text + + +def test_disable_openapi(monkeypatch): + monkeypatch.setenv("OPENAPI_URL", "") + importlib.reload(tutorial001) + client = TestClient(tutorial001.app) + response = client.get("/openapi.json") + assert response.status_code == 404, response.text + response = client.get("/docs") + assert response.status_code == 404, response.text + response = client.get("/redoc") + assert response.status_code == 404, response.text From 897b7d1b992184ef5d78c95fb50deb108937b281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 17:47:26 +0200 Subject: [PATCH 107/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index aa0e587a36706..f7f8a5529bf19 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Allow disabling docs UIs by just disabling OpenAPI with `openapi_url=None`. New example in docs: [Advanced: Conditional OpenAPI](https://fastapi.tiangolo.com/advanced/conditional-openapi/). PR [#1421](https://github.com/tiangolo/fastapi/pull/1421). * Add translation to Portuguese for [Benchmarks - Comparações](https://fastapi.tiangolo.com/pt/benchmarks/). PR [#1274](https://github.com/tiangolo/fastapi/pull/1274) by [@Serrones](https://github.com/Serrones). * Add translation to Portuguese for [Tutorial - User Guide - Intro - Tutorial - Guia de Usuário - Introdução](https://fastapi.tiangolo.com/pt/tutorial/). PR [#1259](https://github.com/tiangolo/fastapi/pull/1259) by [@marcosmmb](https://github.com/marcosmmb). * Allow using Unicode in MkDocs for translations. PR [#1419](https://github.com/tiangolo/fastapi/pull/1419). From c5807fdaa4e4be239e1e0040d06bc3357bff8b82 Mon Sep 17 00:00:00 2001 From: Donghui Wang <977675308@qq.com> Date: Sun, 17 May 2020 00:02:05 +0800 Subject: [PATCH 108/153] =?UTF-8?q?=F0=9F=94=A5=20Remove=20vote=20link=20(?= =?UTF-8?q?#1289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remove 'Vote to include FastAPI in awesome-python', because the PR was closed --- docs/en/docs/help-fastapi.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index f6965658302bd..9c55e5c305d30 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -69,7 +69,6 @@ You can let me know: ## Vote for FastAPI -* Vote to include **FastAPI** in `awesome-python`. * Vote for **FastAPI** in Slant. ## Help others with issues in GitHub From 16b3669adf308b412ba8fd6b2273af06d15dba84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 18:03:17 +0200 Subject: [PATCH 109/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f7f8a5529bf19..c4173e414b821 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Remove obsolete vote link. PR [#1289](https://github.com/tiangolo/fastapi/pull/1289) by [@donhui](https://github.com/donhui). * Allow disabling docs UIs by just disabling OpenAPI with `openapi_url=None`. New example in docs: [Advanced: Conditional OpenAPI](https://fastapi.tiangolo.com/advanced/conditional-openapi/). PR [#1421](https://github.com/tiangolo/fastapi/pull/1421). * Add translation to Portuguese for [Benchmarks - Comparações](https://fastapi.tiangolo.com/pt/benchmarks/). PR [#1274](https://github.com/tiangolo/fastapi/pull/1274) by [@Serrones](https://github.com/Serrones). * Add translation to Portuguese for [Tutorial - User Guide - Intro - Tutorial - Guia de Usuário - Introdução](https://fastapi.tiangolo.com/pt/tutorial/). PR [#1259](https://github.com/tiangolo/fastapi/pull/1259) by [@marcosmmb](https://github.com/marcosmmb). From 741de7f92745d9549207f16e2ae64c8949027fe1 Mon Sep 17 00:00:00 2001 From: Alvaro Pernas Date: Sat, 16 May 2020 20:53:40 +0200 Subject: [PATCH 110/153] =?UTF-8?q?=F0=9F=8C=90=20Add=20Spanish=20translat?= =?UTF-8?q?ion=20for=20Concurrency=20and=20async=20/=20await=20(#1290)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * final touches to async section ES translation * minor fixes * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * Update docs/es/docs/async.md Co-Authored-By: Camila Gutierrez * 📝 Update async/await docs in EN with emojis * 📝 Update wording, format, and emojis for async/await in ES * 🔧 Add async.md to MkDocs for Spanish Co-authored-by: Camila Gutierrez Co-authored-by: Sebastián Ramírez --- docs/en/docs/async.md | 126 +++++++------- docs/es/docs/async.md | 394 ++++++++++++++++++++++++++++++++++++++++++ docs/es/mkdocs.yml | 1 + 3 files changed, 459 insertions(+), 62 deletions(-) create mode 100644 docs/es/docs/async.md diff --git a/docs/en/docs/async.md b/docs/en/docs/async.md index 2d0a7311e4d5b..44169c61f370e 100644 --- a/docs/en/docs/async.md +++ b/docs/en/docs/async.md @@ -55,7 +55,7 @@ But by following the steps above, it will be able to do some performance optimiz 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, below: +Let's see that phrase by parts in the sections below: * **Asynchronous Code** * **`async` and `await`** @@ -94,7 +94,7 @@ For "synchronous" (contrary to "asynchronous") they commonly also use the term " This idea of **asynchronous** code described above is also sometimes called **"concurrency"**. It is different from **"parallelism"**. -**Concurrency** and **parallelism** both relate to "different things happening more or less at the same time". +**Concurrency** and **parallelism** both relate to "different things happening more or less at the same time". But the details between *concurrency* and *parallelism* are quite different. @@ -102,107 +102,109 @@ To see the difference, imagine the following story about 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 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 ✨😍✨. -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, 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 (not preparing them), 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 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 😍. -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. +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. -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 -You go with your crush to get parallel fast food. +Now let's imagine these aren't "Concurrent Burgers", but "Parallel Burgers". -You stand in line while several (let's say 8) cashiers take the orders from the people in front of you. +You go with your crush 😍 to get parallel fast food 🍔. -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. +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. -Then it's finally your turn, you place your order of 2 very fancy burgers for your crush and 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 prepares the burger right away before getting the next order. -You pay. +Then it's finally your turn, you place your order of 2 very fancy burgers 🍔 for your crush 😍 and you. -The cashier goes to the kitchen. +You pay 💸. -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. +The cashier goes to the kitchen 👨‍🍳. -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. +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. -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. +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 😞. -Then your cashier/cook finally comes back with your burgers, after a long time waiting there in front of the counter. +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. -You take your burgers and go to the table with your crush. +Then your cashier/cook 👨‍🍳 finally comes back with your burgers 🍔, after a long time waiting 🕙 there in front of the counter. -You just eat them, and you are done. +You take your burgers 🍔 and go to the table with your crush 😍. -There was not much talk or flirting as most of the time was spent waiting in front of the counter. +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 😞. --- -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. -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 -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. -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. @@ -210,7 +212,7 @@ Even though the main specification for asynchronous web Python (ASGI) was develo 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 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 (all thanks to Starlette). @@ -228,15 +230,15 @@ 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. -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. And as most of the execution time is taken by actual work (instead of waiting), and the work in a computer is done by a CPU, they call these problems "CPU bound". @@ -246,8 +248,8 @@ Common examples of CPU bound operations are things that require complex math pro For example: -* **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) +* **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. * **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. @@ -271,7 +273,7 @@ When there is an operation that will require waiting before giving the results a 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`: @@ -290,7 +292,7 @@ def get_sequential_burgers(number: int): 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: @@ -338,7 +340,7 @@ In previous versions of NodeJS / Browser JavaScript, you would have used "callba ## 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". @@ -348,7 +350,7 @@ 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. -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. @@ -356,16 +358,16 @@ All that is what powers FastAPI (through Starlette) and what makes it have such !!! warning You can probably skip this. - + These are very technical details of how **FastAPI** works underneath. - + If you have quite some technical knowledge (co-routines, threads, blocking, etc) and are curious about how FastAPI handles `async def` vs normal `def`, go ahead. ### Path operation functions 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 IO. +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 I/O. 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. @@ -375,7 +377,7 @@ The same applies for dependencies. If a dependency is a standard `def` function ### 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 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 (from the threadpool) instead of being "awaited". ### Other utility functions @@ -383,7 +385,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. -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. --- diff --git a/docs/es/docs/async.md b/docs/es/docs/async.md new file mode 100644 index 0000000000000..609fc4866f9b6 --- /dev/null +++ b/docs/es/docs/async.md @@ -0,0 +1,394 @@ +# Concurrencia y async / await + +Detalles sobre la sintaxis `async def` para *path operation functions* y un poco de información sobre código asíncrono, concurrencia y paralelismo. + +## ¿Tienes prisa? + +TL;DR: + +Si estás utilizando libraries de terceros que te dicen que las llames con `await`, del tipo: + +```Python +results = await some_library() +``` + +Entonces declara tus *path operation functions* con `async def` de la siguiente manera: + +```Python hl_lines="2" +@app.get('/') +async def read_results(): + results = await some_library() + return results +``` + +!!! note "Nota" + Solo puedes usar `await` dentro de funciones creadas con `async def`. + +--- + +Si estás utilizando libraries de terceros que se comunican con algo (una base de datos, una API, el sistema de archivos, etc.) y no tienes soporte para `await` (este es el caso para la mayoría de las libraries de bases de datos), declara tus *path operation functions* de forma habitual, con solo `def`, de la siguiente manera: + +```Python hl_lines="2" +@app.get('/') +def results(): + results = some_library() + return results +``` + +--- + +Si tu aplicación (de alguna manera) no tiene que comunicarse con nada más y en consecuencia esperar a que responda, usa `async def`. + +--- + +Si simplemente no lo sabes, usa `def` normal. + +--- + +**Nota**: puedes mezclar `def` y `async def` en tus *path operation functions* tanto como lo necesites y definir cada una utilizando la mejor opción para ti. FastAPI hará lo correcto con ellos. + +De todos modos, en cualquiera de los casos anteriores, FastAPI seguirá funcionando de forma asíncrona y será extremadamente rápido. + +Pero siguiendo los pasos anteriores, FastAPI podrá hacer algunas optimizaciones de rendimiento. + +## Detalles Técnicos + +Las versiones modernas de Python tienen soporte para **"código asíncrono"** usando algo llamado **"coroutines"**, usando la sintaxis **`async` y `await`**. + +Veamos esa frase por partes en las secciones siguientes: + +* **Código Asíncrono** +* **`async` y `await`** +* **Coroutines** + +## Código Asíncrono + +El código asíncrono sólo significa que el lenguaje 💬 tiene una manera de decirle al sistema / programa 🤖 que, en algún momento del código, 🤖 tendrá que esperar a que *algo más* termine en otro sitio. Digamos que ese *algo más* se llama, por ejemplo, "archivo lento" 📝. + +Durante ese tiempo, el sistema puede hacer otras cosas, mientras "archivo lento" 📝 termina. + +Entonces el sistema / programa 🤖 volverá cada vez que pueda, sea porque está esperando otra vez, porque 🤖 ha terminado todo el trabajo que tenía en ese momento. Y 🤖 verá si alguna de las tareas por las que estaba esperando ha terminado, haciendo lo que tenía que hacer. + +Luego, 🤖 cogerá la primera tarea finalizada (digamos, nuestro "archivo lento" 📝) y continuará con lo que tenía que hacer con esa tarea. + +Esa "espera de otra cosa" normalmente se refiere a operaciones I/O que son relativamente "lentas" (en relación a la velocidad del procesador y memoria RAM), como por ejemplo esperar por: + +* los datos de cliente que se envían a través de la red +* los datos enviados por tu programa para ser recibidos por el cliente a través de la red +* el contenido de un archivo en disco para ser leído por el sistema y entregado al programa +* los contenidos que tu programa da al sistema para ser escritos en disco +* una operación relacionada con una API remota +* una operación de base de datos +* el retorno de resultados de una consulta de base de datos +* etc. + +Como el tiempo de ejecución se consume principalmente al esperar a operaciones de I/O, las llaman operaciones "I/O bound". + +Se llama "asíncrono" porque el sistema / programa no tiene que estar "sincronizado" con la tarea lenta, esperando el momento exacto en que finaliza la tarea, sin hacer nada, para poder recoger el resultado de la tarea y continuar el trabajo. + +En lugar de eso, al ser un sistema "asíncrono", una vez finalizada, la tarea puede esperar un poco en la cola (algunos microsegundos) para que la computadora / programa termine lo que estaba haciendo, y luego vuelva para recoger los resultados y seguir trabajando con ellos. + +Por "síncrono" (contrario a "asíncrono") también se usa habitualmente el término "secuencial", porque el sistema / programa sigue todos los pasos secuencialmente antes de cambiar a una tarea diferente, incluso si esos pasos implican esperas. + +### Concurrencia y Hamburguesas + +El concepto de código **asíncrono** descrito anteriormente a veces también se llama **"concurrencia"**. Es diferente del **"paralelismo"**. + +**Concurrencia** y **paralelismo** ambos se relacionan con "cosas diferentes que suceden más o menos al mismo tiempo". + +Pero los detalles entre *concurrencia* y *paralelismo* son bastante diferentes. + +Para entender las diferencias, imagina la siguiente historia sobre hamburguesas: + +### Hamburguesas Concurrentes + +Vas con la persona que te gusta 😍 a pedir comida rápida 🍔, haces cola mientras el cajero 💁 recoge los pedidos de las personas de delante tuyo. + +Llega tu turno, haces tu pedido de 2 hamburguesas impresionantes para esa persona 😍 y para ti. + +Pagas 💸. + +El cajero 💁 le dice algo al chico de la cocina 👨‍🍳 para que sepa que tiene que preparar tus hamburguesas 🍔 (a pesar de que actualmente está preparando las de los clientes anteriores). + +El cajero 💁 te da el número de tu turno. + +Mientras esperas, vas con esa persona 😍 y eliges una mesa, se sientan y hablan durante un rato largo (ya que las hamburguesas son muy impresionantes y necesitan un rato para prepararse ✨🍔✨). + +Mientras te sientas en la mesa con esa persona 😍, esperando las hamburguesas 🍔, puedes disfrutar ese tiempo admirando lo increíble, inteligente, y bien que se ve ✨😍✨. + +Mientras esperas y hablas con esa persona 😍, de vez en cuando, verificas el número del mostrador para ver si ya es tu turno. + +Al final, en algún momento, llega tu turno. Vas al mostrador, coges tus hamburguesas 🍔 y vuelves a la mesa. + +Tú y esa persona 😍 se comen las hamburguesas 🍔 y la pasan genial ✨. + +--- + +Imagina que eres el sistema / programa 🤖 en esa historia. + +Mientras estás en la cola, estás quieto 😴, esperando tu turno, sin hacer nada muy "productivo". Pero la línea va rápida porque el cajero 💁 solo recibe los pedidos (no los prepara), así que está bien. + +Luego, cuando llega tu turno, haces un trabajo "productivo" real 🤓, procesas el menú, decides lo que quieres, lo que quiere esa persona 😍, pagas 💸, verificas que das el billete o tarjeta correctos, verificas que te cobren correctamente, que el pedido tiene los artículos correctos, etc. + +Pero entonces, aunque aún no tienes tus hamburguesas 🍔, el trabajo hecho con el cajero 💁 está "en pausa" ⏸, porque debes esperar 🕙 a que tus hamburguesas estén listas. + +Pero como te alejas del mostrador y te sientas en la mesa con un número para tu turno, puedes cambiar tu atención 🔀 a esa persona 😍 y "trabajar" ⏯ 🤓 en eso. Entonces nuevamente estás haciendo algo muy "productivo" 🤓, como coquetear con esa persona 😍. + +Después, el 💁 cajero dice "he terminado de hacer las hamburguesas" 🍔 poniendo tu número en la pantalla del mostrador, pero no saltas al momento que el número que se muestra es el tuyo. Sabes que nadie robará tus hamburguesas 🍔 porque tienes el número de tu turno y ellos tienen el suyo. + +Así que esperas a que esa persona 😍 termine la historia (terminas el trabajo actual ⏯ / tarea actual que se está procesando 🤓), sonríes gentilmente y le dices que vas por las hamburguesas ⏸. + +Luego vas al mostrador 🔀, a la tarea inicial que ya está terminada ⏯, recoges las hamburguesas 🍔, les dices gracias y las llevas a la mesa. Eso termina esa fase / tarea de interacción con el mostrador ⏹. Eso a su vez, crea una nueva tarea, "comer hamburguesas" 🔀 ⏯, pero la anterior de "conseguir hamburguesas" está terminada ⏹. + +### Hamburguesas Paralelas + +Ahora imagina que estas no son "Hamburguesas Concurrentes" sino "Hamburguesas Paralelas". + +Vas con la persona que te gusta 😍 por comida rápida paralela 🍔. + +Haces la cola mientras varios cajeros (digamos 8) que a la vez son cocineros 👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳 toman los pedidos de las personas que están delante de ti. + +Todos los que están antes de ti están esperando 🕙 que sus hamburguesas 🍔 estén listas antes de dejar el mostrador porque cada uno de los 8 cajeros prepara la hamburguesa de inmediato antes de recibir el siguiente pedido. + +Entonces finalmente es tu turno, haces tu pedido de 2 hamburguesas 🍔 impresionantes para esa persona 😍 y para ti. + +Pagas 💸. + +El cajero va a la cocina 👨‍🍳. + +Esperas, de pie frente al mostrador 🕙, para que nadie más recoja tus hamburguesas 🍔, ya que no hay números para los turnos. + +Como tu y esa persona 😍 están ocupados en impedir que alguien se ponga delante y recoja tus hamburguesas apenas llegan 🕙, tampoco puedes prestarle atención a esa persona 😞. + +Este es un trabajo "síncrono", estás "sincronizado" con el cajero / cocinero 👨‍🍳. Tienes que esperar y estar allí en el momento exacto en que el cajero / cocinero 👨‍🍳 termina las hamburguesas 🍔 y te las da, o de lo contrario, alguien más podría cogerlas. + +Luego, el cajero / cocinero 👨‍🍳 finalmente regresa con tus hamburguesas 🍔, después de mucho tiempo esperando 🕙 frente al mostrador. + +Cojes tus hamburguesas 🍔 y vas a la mesa con esa persona 😍. + +Sólo las comes y listo 🍔 ⏹. + +No has hablado ni coqueteado mucho, ya que has pasado la mayor parte del tiempo esperando 🕙 frente al mostrador 😞. + +--- + +En este escenario de las hamburguesas paralelas, tú eres un sistema / programa 🤖 con dos procesadores (tú y la persona que te gusta 😍), ambos esperando 🕙 y dedicando su atención ⏯ a estar "esperando en el mostrador" 🕙 durante mucho tiempo. + +La tienda de comida rápida tiene 8 procesadores (cajeros / cocineros) 👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳. Mientras que la tienda de hamburguesas concurrentes podría haber tenido solo 2 (un cajero y un cocinero) 💁 👨‍🍳. + +Pero aún así, la experiencia final no es la mejor 😞. + +--- + +Esta sería la historia paralela equivalente de las hamburguesas 🍔. + +Para un ejemplo más "real" de ésto, imagina un banco. + +Hasta hace poco, la mayoría de los bancos tenían varios cajeros 👨‍💼👨‍💼👨‍💼👨‍💼 y una gran línea 🕙🕙🕙🕙🕙🕙🕙🕙. + +Todos los cajeros haciendo todo el trabajo con un cliente tras otro 👨‍💼⏯. + +Y tienes que esperar 🕙 en la fila durante mucho tiempo o perderás tu turno. + +Probablemente no querrás llevar contigo a la persona que te gusta 😍 a hacer encargos al banco 🏦. + +### Conclusión de las Hamburguesa + +En este escenario de "hamburguesas de comida rápida con tu pareja", debido a que hay mucha espera 🕙, tiene mucho más sentido tener un sistema con concurrencia ⏸🔀⏯. + +Este es el caso de la mayoría de las aplicaciones web. + +Muchos, muchos usuarios, pero el servidor está esperando 🕙 el envío de las peticiones ya que su conexión no es buena. + +Y luego esperando 🕙 nuevamente a que las respuestas retornen. + +Esta "espera" 🕙 se mide en microsegundos, pero aun así, sumando todo, al final es mucha espera. + +Es por eso que tiene mucho sentido usar código asíncrono ⏸🔀⏯ para las API web. + +La mayoría de los framework populares de Python existentes (incluidos Flask y Django) se crearon antes de que existieran las nuevas funciones asíncronas en Python. Por lo tanto, las formas en que pueden implementarse admiten la ejecución paralela y una forma más antigua de ejecución asíncrona que no es tan potente como la actual. + +A pesar de que la especificación principal para Python web asíncrono (ASGI) se desarrolló en Django, para agregar soporte para WebSockets. + +Ese tipo de asincronía es lo que hizo popular a NodeJS (aunque NodeJS no es paralelo) y esa es la fortaleza de Go como lenguaje de programación. + +Y ese es el mismo nivel de rendimiento que obtienes con **FastAPI**. + +Y como puede tener paralelismo y asincronía al mismo tiempo, obtienes un mayor rendimiento que la mayoría de los frameworks de NodeJS probados y a la par con Go, que es un lenguaje compilado más cercano a C (todo gracias Starlette). + +### ¿Es la concurrencia mejor que el paralelismo? + +¡No! Esa no es la moraleja de la historia. + +La concurrencia es diferente al paralelismo. Y es mejor en escenarios **específicos** que implican mucha espera. Debido a eso, generalmente es mucho mejor que el paralelismo para el desarrollo de aplicaciones web. Pero no para todo. + +Entonces, para explicar eso, imagina la siguiente historia corta: + +> Tienes que limpiar una casa grande y sucia. + +*Sí, esa es toda la historia*. + +--- + +No hay esperas 🕙, solo hay mucho trabajo por hacer, en varios lugares de la casa. + +Podrías tener turnos como en el ejemplo de las hamburguesas, primero la sala de estar, luego la cocina, pero como no estás esperando nada, solo limpiando y limpiando, los turnos no afectarían nada. + +Tomaría la misma cantidad de tiempo terminar con o sin turnos (concurrencia) y habrías hecho la misma cantidad de trabajo. + +Pero en este caso, si pudieras traer a los 8 ex cajeros / cocineros / ahora limpiadores 👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳, y cada uno de ellos (y tú) podría tomar una zona de la casa para limpiarla, podría hacer todo el trabajo en **paralelo**, con la ayuda adicional y terminar mucho antes. + +En este escenario, cada uno de los limpiadores (incluido tú) sería un procesador, haciendo su parte del trabajo. + +Y como la mayor parte del tiempo de ejecución lo coge el trabajo real (en lugar de esperar), y el trabajo en un sistema lo realiza una CPU , a estos problemas se les llama "CPU bond". + +--- + +Ejemplos típicos de operaciones dependientes de CPU son cosas que requieren un procesamiento matemático complejo. + +Por ejemplo: + +* **Audio** o **procesamiento de imágenes**. +* **Visión por computadora**: una imagen está compuesta de millones de píxeles, cada píxel tiene 3 valores / colores, procesamiento que normalmente requiere calcular algo en esos píxeles, todo al mismo tiempo. +* **Machine Learning**: normalmente requiere muchas multiplicaciones de "matrices" y "vectores". Imagina en una enorme hoja de cálculo con números y tener que multiplicarlos todos al mismo tiempo. +* **Deep Learning**: este es un subcampo de Machine Learning, por lo tanto, aplica lo mismo. Es solo que no hay una sola hoja de cálculo de números para multiplicar, sino un gran conjunto de ellas, y en muchos casos, usa un procesador especial para construir y / o usar esos modelos. + +### Concurrencia + Paralelismo: Web + Machine Learning + +Con **FastAPI** puedes aprovechar la concurrencia que es muy común para el desarrollo web (atractivo principal de NodeJS). + +Pero también puedes aprovechar los beneficios del paralelismo y el multiprocesamiento (tener múltiples procesos ejecutándose en paralelo) para cargas de trabajo **CPU bond** como las de los sistemas de Machine Learning. + +Eso, más el simple hecho de que Python es el lenguaje principal para **Data Science**, Machine Learning y especialmente Deep Learning, hacen de FastAPI una muy buena combinación para las API y aplicaciones web de Data Science / Machine Learning (entre muchas otras). + +Para ver cómo lograr este paralelismo en producción, consulta la sección sobre [Despliegue](deployment.md){.internal-link target=_blank}. + +## `async` y `await` + +Las versiones modernas de python tienen una forma muy intuitiva de definir código asíncrono. Esto hace que se vea como un código "secuencial" normal y que haga la "espera" por ti en los momentos correctos. + +Cuando hay una operación que requerirá esperar antes de dar los resultados y tiene soporte para estas nuevas características de Python, puedes programarlo como: + +```Python +burgers = await get_burgers(2) +``` + +La clave aquí es `await`. Eso le dice a Python que tiene que esperar ⏸ a que `get_burgers (2)` termine de hacer lo suyo 🕙 antes de almacenar los resultados en `hamburguesas`. Con eso, Python sabrá que puede ir y hacer otra cosa 🔀 ⏯ mientras tanto (como recibir otra solicitud). + +Para que `await` funcione, tiene que estar dentro de una función que admita esta asincronía. Para hacer eso, simplemente lo declaras con `async def`: + +```Python hl_lines="1" +async def get_burgers(number: int): + # Do some asynchronous stuff to create the burgers + return burgers +``` + +...en vez de `def`: + +```Python hl_lines="2" +# This is not asynchronous +def get_sequential_burgers(number: int): + # Do some sequential stuff to create the burgers + return burgers +``` + +Con `async def`, Python sabe que, dentro de esa función, debe tener en cuenta las expresiones `wait` y que puede "pausar" ⏸ la ejecución de esa función e ir a hacer otra cosa 🔀 antes de regresar. + +Cuando desees llamar a una función `async def`, debes "esperarla". Entonces, esto no funcionará: + +```Python +# Esto no funcionará, porque get_burgers se definió con: async def +hamburguesas = get_burgers (2) +``` + +--- + +Por lo tanto, si estás utilizando una library que te dice que puedes llamarla con `await`, debes crear las *path operation functions* que la usan con `async def`, como en: + +```Python hl_lines="2 3" +@app.get('/burgers') +async def read_burgers(): + burgers = await get_burgers(2) + return burgers +``` + +### Más detalles técnicos + +Es posible que hayas notado que `await` solo se puede usar dentro de las funciones definidas con `async def`. + +Pero al mismo tiempo, las funciones definidas con `async def` deben ser "esperadas". Por lo tanto, las funciones con `async def` solo se pueden invocar dentro de las funciones definidas con `async def` también. + +Entonces, relacionado con la paradoja del huevo y la gallina, ¿cómo se llama a la primera función `async`? + +Si estás trabajando con **FastAPI** no tienes que preocuparte por eso, porque esa "primera" función será tu *path operation function*, y FastAPI sabrá cómo hacer lo pertinente. + +En el caso de que desees usar `async` / `await` sin FastAPI, revisa la documentación oficial de Python. + +### Otras formas de código asíncrono + +Este estilo de usar `async` y `await` es relativamente nuevo en el lenguaje. + +Pero hace que trabajar con código asíncrono sea mucho más fácil. + +Esta misma sintaxis (o casi idéntica) también se incluyó recientemente en las versiones modernas de JavaScript (en Browser y NodeJS). + +Pero antes de eso, manejar código asíncrono era bastante más complejo y difícil. + +En versiones anteriores de Python, podrías haber utilizado threads o Gevent. Pero el código es mucho más complejo de entender, depurar y desarrollar. + +En versiones anteriores de NodeJS / Browser JavaScript, habrías utilizado "callbacks". Lo que conduce a callback hell. + +## Coroutines + +**Coroutine** es un término sofisticado para referirse a la cosa devuelta por una función `async def`. Python sabe que es algo así como una función que puede iniciar y que terminará en algún momento, pero que también podría pausarse ⏸ internamente, siempre que haya un `await` dentro de ella. + +Pero toda esta funcionalidad de usar código asincrónico con `async` y `await` se resume muchas veces como usar "coroutines". Es comparable a la característica principal de Go, las "Goroutines". + +## Conclusión + +Veamos la misma frase de arriba: + +> Las versiones modernas de Python tienen soporte para **"código asíncrono"** usando algo llamado **"coroutines"**, con la sintaxis **`async` y `await`**. + +Eso ya debería tener más sentido ahora. ✨ + +Todo eso es lo que impulsa FastAPI (a través de Starlette) y lo que hace que tenga un rendimiento tan impresionante. + +## Detalles muy técnicos + +!!! warning "Advertencia" + Probablemente puedas saltarte esto. + + Estos son detalles muy técnicos de cómo **FastAPI** funciona a muy bajo nivel. + + Si tienes bastante conocimiento técnico (coroutines, threads, bloqueos, etc.) y tienes curiosidad acerca de cómo FastAPI gestiona `async def` vs `def` normal, continúa. + +### Path operation functions + +Cuando declaras una *path operation function* con `def` normal en lugar de `async def`, se ejecuta en un threadpool externo que luego es "awaited", en lugar de ser llamado directamente (ya que bloquearía el servidor). + +Si vienes de otro framework asíncrono que no funciona de la manera descrita anteriormente y estás acostumbrado a definir *path operation functions* del tipo sólo cálculo con `def` simple para una pequeña ganancia de rendimiento (aproximadamente 100 nanosegundos), ten en cuenta que en **FastAPI** el efecto sería bastante opuesto. En estos casos, es mejor usar `async def` a menos que tus *path operation functions* usen un código que realice el bloqueo I/O. + +Aún así, en ambas situaciones, es probable que **FastAPI** sea [aún más rápido](/#rendimiento){.Internal-link target=_blank} que (o al menos comparable) a tu framework anterior. + +### Dependencias + +Lo mismo se aplica para las dependencias. Si una dependencia es una función estándar `def` en lugar de `async def`, se ejecuta en el threadpool externo. + +### Subdependencias + +Puedes tener múltiples dependencias y subdependencias que se requieren unas a otras (como parámetros de las definiciones de cada función), algunas de ellas pueden crearse con `async def` y otras con `def` normal. Igual todo seguiría funcionando correctamente, y las creadas con `def` normal se llamarían en un thread externo (del threadpool) en lugar de ser "awaited". + +### Otras funciones de utilidades + +Cualquier otra función de utilidad que llames directamente se puede crear con `def` o `async def` normales y FastAPI no afectará la manera en que la llames. + +Esto contrasta con las funciones que FastAPI llama por ti: las *path operation functions* y dependencias. + +Si tu función de utilidad es creada con `def` normal, se llamará directamente (tal cual la escribes en tu código), no en un threadpool, si la función se crea con `async def`, entonces debes usar `await` con esa función cuando la llamas en tu código. + +--- + +Nuevamente, estos son detalles muy técnicos que probablemente sólo son útiles si los viniste a buscar expresamente. + +De lo contrario, la guía de la sección anterior debería ser suficiente: ¿Tienes prisa?. diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 8832f53f4bdb8..1d42105307fa1 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -30,6 +30,7 @@ nav: - tutorial/index.md - Guía de Usuario Avanzada: - advanced/index.md +- async.md markdown_extensions: - toc: permalink: true From 53d316f706224c38db60e30984ab9d3459abfa6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 20:56:58 +0200 Subject: [PATCH 111/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c4173e414b821..b9f0482aa7fb7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Spanish for [Concurrency and async / await - Concurrencia y async / await](https://fastapi.tiangolo.com/es/async/). PR [#1290](https://github.com/tiangolo/fastapi/pull/1290) by [@alvaropernas](https://github.com/alvaropernas). * Remove obsolete vote link. PR [#1289](https://github.com/tiangolo/fastapi/pull/1289) by [@donhui](https://github.com/donhui). * Allow disabling docs UIs by just disabling OpenAPI with `openapi_url=None`. New example in docs: [Advanced: Conditional OpenAPI](https://fastapi.tiangolo.com/advanced/conditional-openapi/). PR [#1421](https://github.com/tiangolo/fastapi/pull/1421). * Add translation to Portuguese for [Benchmarks - Comparações](https://fastapi.tiangolo.com/pt/benchmarks/). PR [#1274](https://github.com/tiangolo/fastapi/pull/1274) by [@Serrones](https://github.com/Serrones). From f7eea768f66780098a9be3e099201e464b84e9ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 May 2020 21:00:28 +0200 Subject: [PATCH 112/153] =?UTF-8?q?=F0=9F=94=96=20Release=200.54.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b9f0482aa7fb7..9d060f7ebc9aa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +## 0.54.2 + * Add translation to Spanish for [Concurrency and async / await - Concurrencia y async / await](https://fastapi.tiangolo.com/es/async/). PR [#1290](https://github.com/tiangolo/fastapi/pull/1290) by [@alvaropernas](https://github.com/alvaropernas). * Remove obsolete vote link. PR [#1289](https://github.com/tiangolo/fastapi/pull/1289) by [@donhui](https://github.com/donhui). * Allow disabling docs UIs by just disabling OpenAPI with `openapi_url=None`. New example in docs: [Advanced: Conditional OpenAPI](https://fastapi.tiangolo.com/advanced/conditional-openapi/). PR [#1421](https://github.com/tiangolo/fastapi/pull/1421). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 2c2612f2d1417..cda5248efef70 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.54.1" +__version__ = "0.54.2" from starlette import status From f8f0a6e46264c804996b0d9ba4c705eb4db281a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 12:11:17 +0200 Subject: [PATCH 113/153] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20in?= =?UTF-8?q?=20async=20docs=20(#1423)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/async.md | 4 ++-- docs/es/docs/async.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/async.md b/docs/en/docs/async.md index 44169c61f370e..756909e989c7f 100644 --- a/docs/en/docs/async.md +++ b/docs/en/docs/async.md @@ -265,7 +265,7 @@ To see how to achieve this parallelism in production see the section about [Depl ## `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: @@ -336,7 +336,7 @@ But before that, handling asynchronous code was quite more complex and difficult In previous versions of Python, you could have used threads or Gevent. 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 callback hell. +In previous versions of NodeJS / Browser JavaScript, you would have used "callbacks". Which leads to callback hell. ## Coroutines diff --git a/docs/es/docs/async.md b/docs/es/docs/async.md index 609fc4866f9b6..99e2f8b2645e7 100644 --- a/docs/es/docs/async.md +++ b/docs/es/docs/async.md @@ -265,7 +265,7 @@ Para ver cómo lograr este paralelismo en producción, consulta la sección sobr ## `async` y `await` -Las versiones modernas de python tienen una forma muy intuitiva de definir código asíncrono. Esto hace que se vea como un código "secuencial" normal y que haga la "espera" por ti en los momentos correctos. +Las versiones modernas de Python tienen una forma muy intuitiva de definir código asíncrono. Esto hace que se vea como un código "secuencial" normal y que haga la "espera" por ti en los momentos correctos. Cuando hay una operación que requerirá esperar antes de dar los resultados y tiene soporte para estas nuevas características de Python, puedes programarlo como: From d53a253c8d5f4be4854061baf8b106378313926d Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Sun, 17 May 2020 20:14:14 +1000 Subject: [PATCH 114/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20location=20of?= =?UTF-8?q?=20get=5Fdb=20in=20SQL=20docs=20(#1293)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/sql_databases/sql_app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_src/sql_databases/sql_app/main.py b/docs_src/sql_databases/sql_app/main.py index 33f63d332c4d8..e7508c59d45d0 100644 --- a/docs_src/sql_databases/sql_app/main.py +++ b/docs_src/sql_databases/sql_app/main.py @@ -13,8 +13,8 @@ # Dependency def get_db(): + db = SessionLocal() try: - db = SessionLocal() yield db finally: db.close() From 0f387553d144a40f5edab0f50a430fee6c161a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 12:11:32 +0200 Subject: [PATCH 115/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9d060f7ebc9aa..a9fb566ae7a7a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +* Fix typos in Async docs. PR [#1423](https://github.com/tiangolo/fastapi/pull/1423). + ## 0.54.2 * Add translation to Spanish for [Concurrency and async / await - Concurrencia y async / await](https://fastapi.tiangolo.com/es/async/). PR [#1290](https://github.com/tiangolo/fastapi/pull/1290) by [@alvaropernas](https://github.com/alvaropernas). From ae937734652fe86b46160b64b56ededf205717aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 12:15:58 +0200 Subject: [PATCH 116/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a9fb566ae7a7a..06789a1f301e8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update order of execution for `get_db` in SQLAlchemy tutorial. PR [#1293](https://github.com/tiangolo/fastapi/pull/1293) by [@bcb](https://github.com/bcb). * Fix typos in Async docs. PR [#1423](https://github.com/tiangolo/fastapi/pull/1423). ## 0.54.2 From 22f7eae3f2e8e65f88a9153f0a10116ea92c24f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 12:28:37 +0200 Subject: [PATCH 117/153] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Make=20sure=20the?= =?UTF-8?q?=20*=20in=20the=20README=20is=20consistent=20in=20the=20docs=20?= =?UTF-8?q?(#1424)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/en/docs/index.md | 2 +- docs/es/docs/index.md | 4 ++-- docs/pt/docs/index.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 263b9f3371202..90f24558cbfa5 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The key features are: * **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). -* **Fast to code**: Increase the speed to develop features by about 200% to 300% *. +* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * * **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * * **Intuitive**: Great editor support. Completion everywhere. Less time debugging. * **Easy**: Designed to be easy to use and learn. Less time reading docs. diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 263b9f3371202..90f24558cbfa5 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -33,7 +33,7 @@ The key features are: * **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). -* **Fast to code**: Increase the speed to develop features by about 200% to 300% *. +* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * * **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * * **Intuitive**: Great editor support. Completion everywhere. Less time debugging. * **Easy**: Designed to be easy to use and learn. Less time reading docs. diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 084494ae0815d..0a3fbe34bb478 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -32,8 +32,8 @@ Sus características principales son: * **Rapidez**: Alto rendimiento, a la par con **NodeJS** y **Go** (gracias a Starlette y Pydantic). [Uno de los frameworks de Python más rápidos](#rendimiento). -* **Rápido de programar**: Incrementa la velocidad de desarrollo entre 200% y 300% *. -* **Menos errores**: Reduce los errores humanos (de programador) aproximadamente un 40% *. +* **Rápido de programar**: Incrementa la velocidad de desarrollo entre 200% y 300%. * +* **Menos errores**: Reduce los errores humanos (de programador) aproximadamente un 40%. * * **Intuitivo**: Gran soporte en los editores con auto completado en todas partes. Gasta menos tiempo debugging. * **Fácil**: Está diseñado para ser fácil de usar y aprender. Gastando menos tiempo leyendo documentación. * **Corto**: Minimiza la duplicación de código. Múltiples funcionalidades con cada declaración de parámetros. Menos errores. diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index 138d8bd607ab7..3e6fd11f70718 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -37,7 +37,7 @@ The key features are: * **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). -* **Fast to code**: Increase the speed to develop features by about 200% to 300% *. +* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * * **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * * **Intuitive**: Great editor support. Completion everywhere. Less time debugging. * **Easy**: Designed to be easy to use and learn. Less time reading docs. From 1fa28b7cb6ab3f2dc0d51d189bd216c6ff228621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 12:30:00 +0200 Subject: [PATCH 118/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 06789a1f301e8..99233e62be96f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Make sure the `*` in short features in the docs is consistent (after `.`) in all languages. PR [#1424](https://github.com/tiangolo/fastapi/pull/1424). * Update order of execution for `get_db` in SQLAlchemy tutorial. PR [#1293](https://github.com/tiangolo/fastapi/pull/1293) by [@bcb](https://github.com/bcb). * Fix typos in Async docs. PR [#1423](https://github.com/tiangolo/fastapi/pull/1423). From b79e0026352a757b449446027bad16daba1bf77d Mon Sep 17 00:00:00 2001 From: Chris Allnutt Date: Sun, 17 May 2020 06:37:15 -0400 Subject: [PATCH 119/153] =?UTF-8?q?=E2=9C=8F=20Re-word=20and=20clarify=20d?= =?UTF-8?q?ocs=20for=20extra=20info=20in=20body-fields=20(#1299)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed Typo in [EN] tutorial: body-fields - remove duplicate of examples text * ✏️ Re-word and clarify extra info docs Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/body-fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index 28a81e41328bd..91e6b1a101a8d 100644 --- a/docs/en/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -39,7 +39,7 @@ You can then use `Field` with model attributes: You can declare extra information in `Field`, `Query`, `Body`, etc. And it will be included in the generated JSON Schema. -You will learn more about it later to declare examples examples. +You will learn more about adding extra information later in the docs, when learning to declare examples. ## Recap From 48ccef9ad2e1fe29609ab22d25ed946a2a1be41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 12:38:10 +0200 Subject: [PATCH 120/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 99233e62be96f..1dc3cf251bfbc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Re-word and clarify docs for extra info in fields. PR [#1299](https://github.com/tiangolo/fastapi/pull/1299) by [@chris-allnutt](https://github.com/chris-allnutt). * Make sure the `*` in short features in the docs is consistent (after `.`) in all languages. PR [#1424](https://github.com/tiangolo/fastapi/pull/1424). * Update order of execution for `get_db` in SQLAlchemy tutorial. PR [#1293](https://github.com/tiangolo/fastapi/pull/1293) by [@bcb](https://github.com/bcb). * Fix typos in Async docs. PR [#1423](https://github.com/tiangolo/fastapi/pull/1423). From 30e56ec835e9cac46dfd17a638516b8183d897ea Mon Sep 17 00:00:00 2001 From: Fabio Serrao Date: Sun, 17 May 2020 07:46:22 -0300 Subject: [PATCH 121/153] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20trans?= =?UTF-8?q?lation=20for=20index.md=20(#1300)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Portuguese Translation for index.md * ✏️ Update * for consistency with recent changes Co-authored-by: Sebastián Ramírez --- docs/pt/docs/index.md | 327 +++++++++++++++++++++--------------------- 1 file changed, 161 insertions(+), 166 deletions(-) diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index 3e6fd11f70718..10b2d6855f72f 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -1,12 +1,8 @@ - -{!../../../docs/missing-translation.md!} - -

    FastAPI

    - FastAPI framework, high performance, easy to learn, fast to code, ready for production + Framework FastAPI, alta performance, fácil de aprender, fácil de codar, pronto para produção

    @@ -25,80 +21,79 @@ --- -**Documentation**: https://fastapi.tiangolo.com +**Documentação**: https://fastapi.tiangolo.com -**Source Code**: https://github.com/tiangolo/fastapi +**Código fonte**: https://github.com/tiangolo/fastapi --- -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: +FastAPI é um moderno e rápido (alta performance) _framework web_ para construção de APIs com Python 3.6 ou superior, baseado nos _type hints_ padrões do Python. -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). +Os recursos chave são: -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. +* **Rápido**: alta performance, equivalente a **NodeJS** e **Go** (graças ao Starlette e Pydantic). [Um dos frameworks mais rápidos disponíveis](#performance). +* **Rápido para codar**: Aumenta a velocidade para desenvolver recursos entre 200% a 300%. * +* **Poucos bugs**: Reduz cerca de 40% de erros iduzidos por humanos (desenvolvedores). * +* **Intuitivo**: Grande suporte a _IDEs_. _Auto-Complete_ em todos os lugares. Menos tempo debugando. +* **Fácil**: Projetado para ser fácil de aprender e usar. Menos tempo lendo documentação. +* **Enxuto**: Minimize duplicação de código. Múltiplos recursos para cada declaração de parâmetro. Menos bugs. +* **Robusto**: Tenha código pronto para produção. E com documentação interativa automática. +* **Baseado em padrões**: Baseado em (e totalmente compatível com) os padrões abertos para APIs: OpenAPI (anteriormente conhecido como Swagger) e _JSON Schema_. -* estimation based on tests on an internal development team, building production applications. +* estimativas baseadas em testes realizados com equipe interna de desenvolvimento, construindo aplicações em produção. -## Opinions +## Opiniões -"*[...] 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.*" +"*[...] Estou usando **FastAPI** muito esses dias. [...] Estou na verdade planejando utilizar ele em todos os times de **serviços _Machine Learning_ na Microsoft**. Alguns deles estão sendo integrados no _core_ do produto **Windows** e alguns produtos **Office**.*"

    Kabir Khan - Microsoft (ref)
    --- -"*I’m over the moon excited about **FastAPI**. It’s so fun!*" +"*Estou extremamente entusiasmado com o **FastAPI**. É tão divertido!*" -
    Brian Okken - Python Bytes podcast host (ref)
    +
    Brian Okken - Python Bytes podcaster (ref)
    --- -"*Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that.*" +"*Honestamente, o que você construiu parece super sólido e rebuscado. De muitas formas, eu queria que o **Hug** fosse assim - é realmente inspirador ver alguém que construiu ele.*" -
    Timothy Crosley - Hug creator (ref)
    +
    Timothy Crosley - criador doHug (ref)
    --- -"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*" +"*Se você está procurando aprender um **_framework_ moderno** para construir aplicações _REST_, dê uma olhada no **FastAPI** [...] É rápido, fácil de usar e fácil de aprender [...]*" -"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*" +"*Nós trocamos nossas **APIs** por **FastAPI** [...] Acredito que vocês gostarão dele [...]*" -
    Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
    +
    Ines Montani - Matthew Honnibal - fundadores da Explosion AI - criadores da spaCy (ref) - (ref)
    --- -"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*" +"*Nós adotamos a biblioteca **FastAPI** para criar um servidor **REST** que possa ser chamado para obter **predições**. [para o Ludwig]*" -
    Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
    +
    Piero Molino, Yaroslav Dudin e Sai Sumanth Miryala - Uber (ref)
    --- -## **Typer**, the FastAPI of CLIs +## **Typer**, o FastAPI das interfaces de linhas de comando -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. +Se você estiver construindo uma aplicação _CLI_ para ser utilizada em um terminal ao invés de uma aplicação web, dê uma olhada no **Typer**. -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 +**Typer** é o irmão menor do FastAPI. E seu propósito é ser o **FastAPI das _CLIs_**. ⌨️ 🚀 -## Requirements +## Requisitos Python 3.6+ -FastAPI stands on the shoulders of giants: +FastAPI está nos ombros de gigantes: -* Starlette for the web parts. -* Pydantic for the data parts. +* Starlette para as partes web. +* Pydantic para a parte de dados. -## Installation +## Instalação
    @@ -110,7 +105,7 @@ $ pip install fastapi
    -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. +Você também precisará de um servidor ASGI para produção, tal como Uvicorn ou Hypercorn.
    @@ -122,11 +117,11 @@ $ pip install uvicorn
    -## Example +## Exemplo -### Create it +### Crie -* Create a file `main.py` with: +* Crie um arquivo `main.py` com: ```Python from fastapi import FastAPI @@ -145,9 +140,9 @@ def read_item(item_id: int, q: str = None): ```
    -Or use async def... +Ou use async def... -If your code uses `async` / `await`, use `async def`: +Se seu código utiliza `async` / `await`, use `async def`: ```Python hl_lines="7 12" from fastapi import FastAPI @@ -165,79 +160,79 @@ async def read_item(item_id: int, q: str = None): return {"item_id": item_id, "q": q} ``` -**Note**: +**Nota**: -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. +Se você não sabe, verifique a seção _"In a hurry?"_ sobre `async` e `await` nas docs.
    -### Run it +### Rode -Run the server with: +Rode o servidor com:
    ```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. +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. ```
    -About the command uvicorn main:app --reload... +Sobre o comando uvicorn main:app --reload... -The command `uvicorn main:app` refers to: +O comando `uvicorn main:app` se refere a: -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. +* `main`: o arquivo `main.py` (o "módulo" Python). +* `app`: o objeto criado dentro de `main.py` com a linha `app = FastAPI()`. +* `--reload`: faz o servidor recarregar após mudanças de código. Somente faça isso para desenvolvimento.
    -### Check it +### Verifique -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. +Abra seu navegador em http://127.0.0.1:8000/items/5?q=somequery. -You will see the JSON response as: +Você verá a resposta JSON como: ```JSON {"item_id": 5, "q": "somequery"} ``` -You already created an API that: +Você acabou de criar uma API que: -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. +* Recebe requisições HTTP nas _rotas_ `/` e `/items/{item_id}`. +* Ambas _rotas_ fazem operações `GET` (também conhecido como _métodos_ HTTP). +* A _rota_ `/items/{item_id}` tem um _parâmetro de rota_ `item_id` que deve ser um `int`. +* A _rota_ `/items/{item_id}` tem um _parâmetro query_ `q` `str` opcional. -### Interactive API docs +### Documentação Interativa da API -Now go to http://127.0.0.1:8000/docs. +Agora vá para http://127.0.0.1:8000/docs. -You will see the automatic interactive API documentation (provided by Swagger UI): +Você verá a documentação automática interativa da API (fornecida por Swagger UI): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternative API docs +### Documentação Alternativa da API -And now, go to http://127.0.0.1:8000/redoc. +E agora, vá para http://127.0.0.1:8000/redoc. -You will see the alternative automatic documentation (provided by ReDoc): +Você verá a documentação automática alternativa (fornecida por ReDoc): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Example upgrade +## Evoluindo o Exemplo -Now modify the file `main.py` to receive a body from a `PUT` request. +Agora modifique o arquivo `main.py` para receber um corpo para uma requisição `PUT`. -Declare the body using standard Python types, thanks to Pydantic. +Declare o corpo utilizando tipos padrão Python, graças ao Pydantic. ```Python hl_lines="2 7 8 9 10 23 24 25" from fastapi import FastAPI @@ -267,175 +262,175 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). +O servidor deverá recarregar automaticamente (porquê você adicionou `--reload` ao comando `uvicorn` acima). -### Interactive API docs upgrade +### Evoluindo a Documentação Interativa da API -Now go to http://127.0.0.1:8000/docs. +Agora vá para http://127.0.0.1:8000/docs. -* The interactive API documentation will be automatically updated, including the new body: +* A documentação interativa da API será automaticamente atualizada, incluindo o novo corpo: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: +* Clique no botão "Try it out", ele permiirá que você preencha os parâmetros e interaja diretamente com a API: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: +* Então clique no botão "Execute", a interface do usuário irá se comunicar com a API, enviar os parâmetros, pegar os resultados e mostrá-los na tela: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Alternative API docs upgrade +### Evoluindo a Documentação Alternativa da API -And now, go to http://127.0.0.1:8000/redoc. +E agora, vá para http://127.0.0.1:8000/redoc. -* The alternative documentation will also reflect the new query parameter and body: +* A documentação alternativa também irá refletir o novo parâmetro da _query_ e o corpo: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Recap +### Recapitulando -In summary, you declare **once** the types of parameters, body, etc. as function parameters. +Resumindo, você declara **uma vez** os tipos dos parâmetros, corpo etc. como parâmetros de função. -You do that with standard modern Python types. +Você faz com tipos padrão do Python moderno. -You don't have to learn a new syntax, the methods or classes of a specific library, etc. +Você não terá que aprender uma nova sintaxe, métodos ou classes de uma biblioteca específica etc. -Just standard **Python 3.6+**. +Apenas **Python 3.6+** padrão. -For example, for an `int`: +Por exemplo, para um `int`: ```Python item_id: int ``` -or for a more complex `Item` model: +ou para um modelo mais complexo, `Item`: ```Python item: Item ``` -...and with that single declaration you get: +...e com essa única declaração você tem: -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: +* Suporte ao Editor, incluindo: + * Completação. + * Verificação de tipos. +* Validação de dados: + * Erros automáticos e claros quando o dado é inválido. + * Validação até para objetos JSON profundamente aninhados. +* Conversão de dados de entrada: vindo da rede para dados e tipos Python. Consegue ler: * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: + * Parâmetros de rota. + * Parâmetros de _query_ . + * _Cookies_. + * Cabeçalhos. + * Formulários. + * Arquivos. +* Conversão de dados de saída de tipos e dados Python para dados de rede (como JSON): + * Converte tipos Python (`str`, `int`, `float`, `bool`, `list` etc). + * Objetos `datetime`. + * Objetos `UUID`. + * Modelos de Banco de Dados. + * ...e muito mais. +* Documentação interativa automática da API, incluindo 2 alternativas de interface de usuário: * Swagger UI. * ReDoc. --- -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. +Voltando ao código do exemplo anterior, **FastAPI** irá: + +* Validar que existe um `item_id` na rota para requisições `GET` e `PUT`. +* Validar que `item_id` é do tipo `int` para requisições `GET` e `PUT`. + * Se não é validado, o cliente verá um útil, claro erro. +* Verificar se existe um parâmetro de _query_ opcional nomeado como `q` (como em `http://127.0.0.1:8000/items/foo?q=somequery`) para requisições `GET`. + * Como o parâmetro `q` é declarado com `= None`, ele é opcional. + * Sem o `None` ele poderia ser obrigatório (como o corpo no caso de `PUT`). +* Para requisições `PUT` para `/items/{item_id}`, lerá o corpo como JSON e: + * Verifica que tem um atributo obrigatório `name` que deve ser `str`. + * Verifica que tem um atributo obrigatório `price` que deve ser `float`. + * Verifica que tem an atributo opcional `is_offer`, que deve ser `bool`, se presente. + * Tudo isso também funciona para objetos JSON profundamente aninhados. +* Converter de e para JSON automaticamente. +* Documentar tudo com OpenAPI, que poderá ser usado por: + * Sistemas de documentação interativos. + * Sistemas de clientes de geração de código automáticos, para muitas linguagens. +* Fornecer diretamente 2 interfaces _web_ de documentação interativa. --- -We just scratched the surface, but you already get the idea of how it all works. +Nós arranhamos apenas a superfície, mas você já tem idéia de como tudo funciona. -Try changing the line with: +Experimente mudar a seguinte linha: ```Python return {"item_name": item.name, "item_id": item_id} ``` -...from: +...de: ```Python ... "item_name": item.name ... ``` -...to: +...para: ```Python ... "item_price": item.price ... ``` -...and see how your editor will auto-complete the attributes and know their types: +...e veja como seu editor irá auto-completar os atributos e saberá os tipos: ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -For a more complete example including more features, see the Tutorial - User Guide. +Para um exemplo mais completo incluindo mais recursos, veja Tutorial - Guia do Usuário. -**Spoiler alert**: the tutorial - user guide includes: +**Alerta de Spoiler**: o tutorial - guia do usuário inclui: -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: +* Declaração de **parâmetetros** de diferentes lugares como: **cabeçalhos**, **cookies**, **campos de formulários** e **arquivos**. +* Como configurar **Limitações de Validação** como `maximum_length` ou `regex`. +* Um poderoso e fácil de usar sistema de **Injeção de Dependência**. +* Segurança e autenticação, incluindo suporte para **OAuth2** com autenticação **JWT tokens** e **HTTP Basic**. +* Técnicas mais avançadas (mas igualmente fáceis) para declaração de **modelos JSON profundamente aninhados** (graças ao Pydantic). +* Muitos recursos extras (graças ao Starlette) como: * **WebSockets** * **GraphQL** - * extremely easy tests based on `requests` and `pytest` + * testes extrememamente fáceis baseados em `requests` e `pytest` * **CORS** * **Cookie Sessions** - * ...and more. + * ...e mais. ## Performance -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) +Testes de performance da _Independent TechEmpower_ mostram aplicações **FastAPI** rodando sob Uvicorn como um dos _frameworks_ Python mais rápidos disponíveis, somente atrás de Starlette e Uvicorn (utilizados internamente pelo FastAPI). (*) -To understand more about it, see the section Benchmarks. +Para entender mais sobre performance, veja a seção Benchmarks. -## Optional Dependencies +## Dependências opcionais -Used by Pydantic: +Usados por Pydantic: -* ujson - for faster JSON "parsing". -* email_validator - for email validation. +* ujson - para JSON mais rápido "parsing". +* email_validator - para validação de email. -Used by Starlette: +Usados por Starlette: -* requests - Required if you want to use the `TestClient`. -* aiofiles - Required if you want to use `FileResponse` or `StaticFiles`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. +* requests - Necessário se você quiser utilizar o `TestClient`. +* aiofiles - Necessário se você quiser utilizar o `FileResponse` ou `StaticFiles`. +* jinja2 - Necessário se você quiser utilizar a configuração padrão de templates. +* python-multipart - Necessário se você quiser suporte com "parsing" de formulário, com `request.form()`. +* itsdangerous - Necessário para suporte a `SessionMiddleware`. +* pyyaml - Necessário para suporte a `SchemaGenerator` da Starlette (você provavelmente não precisará disso com o FastAPI). +* graphene - Necessário para suporte a `GraphQLApp`. +* ujson - Necessário se você quer utilizar `UJSONResponse`. -Used by FastAPI / Starlette: +Usados por FastAPI / Starlette: -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. +* uvicorn - para o servidor que carrega e serve sua aplicação. +* orjson - Necessário se você quer utilizar `ORJSONResponse`. -You can install all of these with `pip install fastapi[all]`. +Você pode instalar todas essas dependências com `pip install fastapi[all]`. -## License +## Licença -This project is licensed under the terms of the MIT license. +Esse projeto é licenciado sob os termos da licença MIT. From f93861e321da4d3e91605f46f78e99efd5405b32 Mon Sep 17 00:00:00 2001 From: Derek Bekoe Date: Sun, 17 May 2020 03:48:02 -0700 Subject: [PATCH 122/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20cors.md=20-=20C?= =?UTF-8?q?ORS=20max=5Fage=20600=20(#1301)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update max_age documentation from 60 to the actual default value of 600. https://github.com/encode/starlette/blob/master/starlette/middleware/cors.py#L23 Related PR https://github.com/encode/starlette/pull/909 --- docs/en/docs/tutorial/cors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/cors.md b/docs/en/docs/tutorial/cors.md index 1ebfec3619446..31da532e466a1 100644 --- a/docs/en/docs/tutorial/cors.md +++ b/docs/en/docs/tutorial/cors.md @@ -60,7 +60,7 @@ The following arguments are supported: * `allow_headers` - A list of HTTP request headers that should be supported for cross-origin requests. Defaults to `[]`. You can use `['*']` to allow all headers. The `Accept`, `Accept-Language`, `Content-Language` and `Content-Type` headers are always allowed for CORS requests. * `allow_credentials` - Indicate that cookies should be supported for cross-origin requests. Defaults to `False`. * `expose_headers` - Indicate any response headers that should be made accessible to the browser. Defaults to `[]`. -* `max_age` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to `60`. +* `max_age` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to `600`. The middleware responds to two particular types of HTTP request... From 27367df90cd9844a2988b3c5a53ef6f0afe2d6ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 12:46:57 +0200 Subject: [PATCH 123/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1dc3cf251bfbc..a7dc221e7bcc8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation of [main page to Portuguese](https://fastapi.tiangolo.com/pt/). PR [#1300](https://github.com/tiangolo/fastapi/pull/1300) by [@Serrones](https://github.com/Serrones). * Re-word and clarify docs for extra info in fields. PR [#1299](https://github.com/tiangolo/fastapi/pull/1299) by [@chris-allnutt](https://github.com/chris-allnutt). * Make sure the `*` in short features in the docs is consistent (after `.`) in all languages. PR [#1424](https://github.com/tiangolo/fastapi/pull/1424). * Update order of execution for `get_db` in SQLAlchemy tutorial. PR [#1293](https://github.com/tiangolo/fastapi/pull/1293) by [@bcb](https://github.com/bcb). From 7fc1bac54bf36b10f20cc3d4b591819c9d83a284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 12:50:00 +0200 Subject: [PATCH 124/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a7dc221e7bcc8..d91b4e9ef0503 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update CORS docs, fix correct default of `max_age=600`. PR [#1301](https://github.com/tiangolo/fastapi/pull/1301) by [@derekbekoe](https://github.com/derekbekoe). * Add translation of [main page to Portuguese](https://fastapi.tiangolo.com/pt/). PR [#1300](https://github.com/tiangolo/fastapi/pull/1300) by [@Serrones](https://github.com/Serrones). * Re-word and clarify docs for extra info in fields. PR [#1299](https://github.com/tiangolo/fastapi/pull/1299) by [@chris-allnutt](https://github.com/chris-allnutt). * Make sure the `*` in short features in the docs is consistent (after `.`) in all languages. PR [#1424](https://github.com/tiangolo/fastapi/pull/1424). From cc3d795bea415d89601a01b2032d987de7e9b97f Mon Sep 17 00:00:00 2001 From: Xie Wei <39515546+waynerv@users.noreply.github.com> Date: Sun, 17 May 2020 18:56:57 +0800 Subject: [PATCH 125/153] =?UTF-8?q?=E2=9C=8F=20Fix=202=20typos=20in=20docs?= =?UTF-8?q?=20(#1324)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/features.md | 2 +- docs/en/docs/tutorial/first-steps.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/features.md b/docs/en/docs/features.md index 8b5bb51f171ea..0e6daad1c7a0f 100644 --- a/docs/en/docs/features.md +++ b/docs/en/docs/features.md @@ -71,7 +71,7 @@ my_second_user: User = User(**second_user_data) ### Editor support -All the framework was designed to be easy and intuitive to use, all the decisions where tested on multiple editors even before starting development, to ensure the best development experience. +All the framework was designed to be easy and intuitive to use, all the decisions were tested on multiple editors even before starting development, to ensure the best development experience. In the last Python developer survey it was clear that the most used feature is "autocompletion". diff --git a/docs/en/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md index 2d212762ed8db..11a9d16e09129 100644 --- a/docs/en/docs/tutorial/first-steps.md +++ b/docs/en/docs/tutorial/first-steps.md @@ -239,7 +239,7 @@ So, in OpenAPI, each of the HTTP methods is called an "operation". We are going to call them "**operations**" too. -#### Define a *path operation function* +#### Define a *path operation decorator* ```Python hl_lines="6" {!../../../docs_src/first_steps/tutorial001.py!} From ca939fabf7d6caf0cb238be94153fb0d3d0655f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 12:59:17 +0200 Subject: [PATCH 126/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d91b4e9ef0503..ff03a03797a60 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix 2 typos in docs. PR [#1324](https://github.com/tiangolo/fastapi/pull/1324) by [@waynerv](https://github.com/waynerv). * Update CORS docs, fix correct default of `max_age=600`. PR [#1301](https://github.com/tiangolo/fastapi/pull/1301) by [@derekbekoe](https://github.com/derekbekoe). * Add translation of [main page to Portuguese](https://fastapi.tiangolo.com/pt/). PR [#1300](https://github.com/tiangolo/fastapi/pull/1300) by [@Serrones](https://github.com/Serrones). * Re-word and clarify docs for extra info in fields. PR [#1299](https://github.com/tiangolo/fastapi/pull/1299) by [@chris-allnutt](https://github.com/chris-allnutt). From f5c5dbb73952aaa8c8cd9b1f92200af6c0cb413c Mon Sep 17 00:00:00 2001 From: Fabio Serrao Date: Sun, 17 May 2020 08:32:44 -0300 Subject: [PATCH 127/153] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20trans?= =?UTF-8?q?lation=20for=20alternatives.md=20(#1325)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Portuguese translation for alternatives.md * 🔥 Remove file not yet translated * ✏️ Add small format and wording changes * 🔧 Update Portuguese MkDocs Co-authored-by: Sebastián Ramírez --- docs/pt/docs/alternatives.md | 413 +++++++++++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 414 insertions(+) create mode 100644 docs/pt/docs/alternatives.md diff --git a/docs/pt/docs/alternatives.md b/docs/pt/docs/alternatives.md new file mode 100644 index 0000000000000..384eb08020d6c --- /dev/null +++ b/docs/pt/docs/alternatives.md @@ -0,0 +1,413 @@ +# Alternativas, Inspiração e Comparações + +O que inspirou **FastAPI**, como ele se compara a outras alternativas e o que FastAPI aprendeu delas. + +## Introdução + +**FastAPI** não poderia existir se não fosse pelos trabalhos anteriores de outras pessoas. + +Houveram tantas ferramentas criadas que ajudaram a inspirar sua criação. + +Tenho evitado criar um novo framework por anos. Primeiramente tentei resolver todos os recursos cobertos pelo **FastAPI** utilizando muitos frameworks diferentes, plug-ins e ferramentas. + +Mas em algum ponto, não houve outra opção senão criar algo que fornecesse todos esses recursos, pegando as melhores idéias de ferramentas anteriores, e combinando eles da melhor forma possível, utilizando recursos da linguagem que não estavam disponíveis antes (_Type Hints_ no Python 3.6+). + +## Ferramentas anteriores + +### Django + +É o framework mais popular e largamente confiável. É utilizado para construir sistemas como o _Instagram_. + +É bem acoplado com banco de dados relacional (como MySQL ou PostgreSQL), então, tendo um banco de dados NoSQL (como Couchbase, MongoDB, Cassandra etc) como a principal ferramenta de armazenamento não é muito fácil. + +Foi criado para gerar HTML no _backend_, não para criar APIs utilizando um _frontend_ moderno (como React, Vue.js e Angular) ou por outros sistemas (como dispositivos IoT) comunicando com ele. + +### Django REST Framework + +Django REST framework foi criado para ser uma caixa de ferramentas flexível para construção de APIs web utilizando Django por baixo, para melhorar suas capacidades de API. + +Ele é utilizado por muitas companhias incluindo Mozilla, Red Hat e Eventbrite. + +Ele foi um dos primeiros exemplos de **documentação automática de API**, e essa foi especificamente uma das primeiras idéias que inspirou "a busca por" **FastAPI**. + +!!! note "Nota" + Django REST Framework foi criado por Tom Christie. O mesmo criador de Starlette e Uvicorn, nos quais **FastAPI** é baseado. + +!!! check "**FastAPI** inspirado para" + Ter uma documentação automática da API em interface web. + +### Flask + +Flask é um "microframework", não inclui integração com banco de dados nem muitas das coisas que vêm por padrão no Django. + +Sua simplicidade e flexibilidade permitem fazer coisas como utilizar bancos de dados NoSQL como principal sistema de armazenamento de dados. + +Por ser tão simples, é relativamente intuitivo de aprender, embora a documentação esteja de forma mais técnica em alguns pontos. + +Ele é comumente utilizado por outras aplicações que não necessariamente precisam de banco de dados, gerenciamento de usuários, ou algum dos muitos recursos que já vem instalados no Django. Embora muitos desses recursos possam ser adicionados com plug-ins. + +Esse desacoplamento de partes, e sendo um "microframework" que pode ser extendido para cobrir exatamente o que é necessário era um recurso chave que eu queria manter. + +Dada a simplicidade do Flask, parecia uma ótima opção para construção de APIs. A próxima coisa a procurar era um "Django REST Framework" para Flask. + +!!! check "**FastAPI** inspirado para" + Ser um microframework. Fazer ele fácil para misturar e combinar com ferramentas e partes necessárias. + + Ser simples e com sistema de roteamento fácil de usar. + +### Requests + +**FastAPI** não é uma alternativa para **Requests**. O escopo deles é muito diferente. + +Na verdade é comum utilizar Requests *dentro* de uma aplicação FastAPI. + +Ainda assim, FastAPI pegou alguma inspiração do Requests. + +**Requests** é uma biblioteca para interagir com APIs (como um cliente), enquanto **FastAPI** é uma biblioteca para *construir* APIs (como um servidor). + +Eles estão, mais ou menos, em pontas opostas, um complementando o outro. + +Requests tem um projeto muito simples e intuitivo, fácil de usar, com padrões sensíveis. Mas ao mesmo tempo, é muito poderoso e customizável. + +É por isso que, como dito no site oficial: + +> Requests é um dos pacotes Python mais baixados de todos os tempos + +O jeito de usar é muito simples. Por exemplo, para fazer uma requisição `GET`, você deveria escrever: + +```Python +response = requests.get("http://example.com/some/url") +``` + +A contra-parte da aplicação FastAPI, *rota de operação*, poderia parecer como: + +```Python hl_lines="1" +@app.get("/some/url") +def read_url(): + return {"message": "Hello World"} +``` + +Veja as similaridades em `requests.get(...)` e `@app.get(...)`. + +!!! check "**FastAPI** inspirado para" + * Ter uma API simples e intuitiva. + * Utilizar nomes de métodos HTTP (operações) diretamente, de um jeito direto e intuitivo. + * Ter padrões sensíveis, mas customizações poderosas. + +### Swagger / OpenAPI + +O principal recurso que eu queria do Django REST Framework era a documentação automática da API. + +Então eu descobri que existia um padrão para documentar APIs, utilizando JSON (ou YAML, uma extensão do JSON) chamado Swagger. + +E tinha uma interface web para APIs Swagger já criada. Então, sendo capaz de gerar documentação Swagger para uma API poderia permitir utilizar essa interface web automaticamente. + +Em algum ponto, Swagger foi dado para a Fundação Linux, e foi renomeado OpenAPI. + +Isso acontece porquê quando alguém fala sobre a versão 2.0 é comum dizer "Swagger", e para a versão 3+, "OpenAPI". + +!!! check "**FastAPI** inspirado para" + Adotar e usar um padrão aberto para especificações API, ao invés de algum esquema customizado. + + E integrar ferramentas de interface para usuários baseado nos padrões: + + * Swagger UI + * ReDoc + + Esses dois foram escolhidos por serem bem populares e estáveis, mas fazendo uma pesquisa rápida, você pode encontrar dúzias de interfaces alternativas adicionais para OpenAPI (assim você poderá utilizar com **FastAPI**). + +### Flask REST frameworks + +Existem vários Flask REST frameworks, mas depois de investir tempo e trabalho investigando eles, eu descobri que muitos estão descontinuados ou abandonados, com alguns tendo questões que fizeram eles inadequados. + +### Marshmallow + +Um dos principais recursos necessários em sistemas API é "serialização" de dados, que é pegar dados do código (Python) e converter eles em alguma coisa que possa ser enviado através da rede. Por exemplo, converter um objeto contendo dados de um banco de dados em um objeto JSON. Converter objetos `datetime` em strings etc. + +Outro grande recurso necessário nas APIs é validação de dados, certificando que os dados são válidos, dados certos parâmetros. Por exemplo, algum campo é `int`, e não alguma string aleatória. Isso é especialmente útil para dados que estão chegando. + +Sem um sistema de validação de dados, você teria que realizar todas as verificações manualmente, no código. + +Esses recursos são o que Marshmallow foi construído para fornecer. Ele é uma ótima biblioteca, e eu já utilizei muito antes. + +Mas ele foi criado antes da existência do _type hints_ do Python. Então, para definir todo o _schema_ você precisa utilizar específicas ferramentas e classes fornecidas pelo Marshmallow. + +!!! check "**FastAPI** inspirado para" + Usar código para definir "schemas" que forneçam, automaticamente, tipos de dados e validação. + +### Webargs + +Outro grande recurso necessário pelas APIs é a análise de dados vindos de requisições. + +Webargs é uma ferramente feita para fornecer o que está no topo de vários frameworks, inclusive Flask. + +Ele utiliza Marshmallow por baixo para validação de dados. E ele foi criado pelos mesmos desenvolvedores. + +Ele é uma grande ferramenta e eu também a utilizei muito, antes de ter o **FastAPI**. + +!!! info + Webargs foi criado pelos mesmos desenvolvedores do Marshmallow. + +!!! check "**FastAPI** inspirado para" + Ter validação automática de dados vindos de requisições. + +### APISpec + +Marshmallow e Webargs fornecem validação, análise e serialização como plug-ins. + +Mas a documentação ainda está faltando. Então APISpec foi criado. + +APISpec tem plug-ins para muitos frameworks (e tem um plug-in para Starlette também). + +O jeito como ele funciona é que você escreve a definição do _schema_ usando formato YAML dentro da _docstring_ de cada função controlando uma rota. + +E ele gera _schemas_ OpenAPI. + +É assim como funciona no Flask, Starlette, Responder etc. + +Mas então, nós temos novamente o problema de ter uma micro-sintaxe, dentro de uma string Python (um grande YAML). + +O editor não poderá ajudar muito com isso. E se nós modificarmos os parâmetros dos _schemas_ do Marshmallow e esquecer de modificar também aquela _docstring_ YAML, o _schema_ gerado pode ficar obsoleto. + +!!! info + APISpec foi criado pelos mesmos desenvolvedores do Marshmallow. + +!!! check "**FastAPI** inspirado para" + Dar suporte a padrões abertos para APIs, OpenAPI. + +### Flask-apispec + +É um plug-in Flask, que amarra junto Webargs, Marshmallow e APISpec. + +Ele utiliza a informação do Webargs e Marshmallow para gerar automaticamente _schemas_ OpenAPI, usando APISpec. + +É uma grande ferramenta, mas muito subestimada. Ela deveria ser um pouco mais popular do que muitos outros plug-ins Flask. É de ser esperado que sua documentação seja bem concisa e abstrata. + +Isso resolveu o problema de ter que escrever YAML (outra sintaxe) dentro das _docstrings_ Python. + +Essa combinação de Flask, Flask-apispec com Marshmallow e Webargs foi meu _backend stack_ favorito até construir **FastAPI**. + +Usando essa combinação levou a criação de vários geradores Flask _full-stack_. Há muitas _stacks_ que eu (e vários times externos) estou utilizando até agora: + +* https://github.com/tiangolo/full-stack +* https://github.com/tiangolo/full-stack-flask-couchbase +* https://github.com/tiangolo/full-stack-flask-couchdb + +E esses mesmos geradores _full-stack_ foram a base dos [Geradores de Projetos **FastAPI**](project-generation.md){.internal-link target=_blank}. + +!!! info + Flask-apispec foi criado pelos mesmos desenvolvedores do Marshmallow. + +!!! check "**FastAPI** inspirado para" + Gerar _schema_ OpenAPI automaticamente, a partir do mesmo código que define serialização e validação. + +### NestJS (and Angular) + +NestJS, que não é nem Python, é um framework NodeJS JavaScript (TypeScript) inspirado pelo Angular. + +Ele alcança de uma forma similar ao que pode ser feito com o Flask-apispec. + +Ele tem um sistema de injeção de dependência integrado, inspirado pelo Angular dois. É necessário fazer o pré-registro dos "injetáveis" (como todos os sistemas de injeção de dependência que conheço), então, adicionando verbosidade e repetição de código. + +Como os parâmetros são descritos com tipos TypeScript (similar aos _type hints_ do Python), o suporte ao editor é muito bom. + +Mas como os dados TypeScript não são preservados após a compilação para o JavaScript, ele não pode depender dos tipos para definir a validação, serialização e documentação ao mesmo tempo. Devido a isso e a algumas decisões de projeto, para pegar a validação, serialização e geração automática do _schema_, é necessário adicionar decoradores em muitos lugares. Então, ele se torna muito verboso. + +Ele também não controla modelos aninhados muito bem. Então, se o corpo JSON na requisição for um objeto JSON que contém campos internos que contém objetos JSON aninhados, ele não consegue ser validado e documentado apropriadamente. + +!!! check "**FastAPI** inspirado para" + Usar tipos Python para ter um ótimo suporte do editor. + + Ter um sistema de injeção de dependência poderoso. Achar um jeito de minimizar repetição de código. + +### Sanic + +Ele foi um dos primeiros frameworks Python extremamente rápido baseado em `asyncio`. Ele foi feito para ser muito similar ao Flask. + +!!! note "Detalhes técnicos" + Ele utiliza `uvloop` ao invés do '_loop_' `asyncio` padrão do Python. É isso que deixa ele tão rápido. + + Ele claramente inspirou Uvicorn e Starlette, que são atualmente mais rápidos que o Sanic em testes de performance abertos. + +!!! check "**FastAPI** inspirado para" + Achar um jeito de ter uma performance insana. + + É por isso que o **FastAPI** é baseado em Starlette, para que ele seja o framework mais rápido disponível (performance testada por terceiros). + +### Falcon + +Falcon é outro framework Python de alta performance, e é projetado para ser minimalista, e funciona como fundação de outros frameworks como Hug. + +Ele usa o padrão anterior para frameworks web Python (WSGI) que é síncrono, então ele não pode controlar _WebSockets_ e outros casos de uso. No entanto, ele também tem uma boa performance. + +Ele é projetado para ter funções que recebem dois parâmetros, uma "requisição" e uma "resposta". Então você "lê" as partes da requisição, e "escreve" partes para a resposta. Devido ao seu design, não é possível declarar parâmetros de requisição e corpos com _type hints_ Python padrão como parâmetros de funções. + +Então, validação de dados, serialização e documentação tem que ser feitos no código, não automaticamente. Ou eles terão que ser implementados como um framework acima do Falcon, como o Hug. Essa mesma distinção acontece em outros frameworks que são inspirados pelo design do Falcon, tendo um objeto de requisição e um objeto de resposta como parâmetros. + +!!! check "**FastAPI** inspirado para" + Achar jeitos de conseguir melhor performance. + + Juntamente com Hug (como Hug é baseado no Falcon) inspirou **FastAPI** para declarar um parâmetro de `resposta`nas funções. + + Embora no FastAPI seja opcional, é utilizado principalmente para configurar cabeçalhos, cookies e códigos de status alternativos. + +### Molten + +Eu descobri Molten nos primeiros estágios da construção do **FastAPI**. E ele tem umas idéias bem similares: + +* Baseado em _type hints_ Python. +* Validação e documentação desses tipos. +* Sistema de injeção de dependência. + +Ele não utiliza validação de dados, seriallização e documentação de bibliotecas de terceiros como o Pydantic, ele tem seu prórpio. Então, essas definições de tipo de dados não podem ser reutilizados tão facilmente. + +Ele exige um pouco mais de verbosidade nas configurações. E como é baseado no WSGI (ao invés de ASGI), ele não é projetado para ter a vantagem da alta performance fornecida por ferramentas como Uvicorn, Starlette e Sanic. + +O sistema de injeção de dependência exige pré-registro das dependências e as dependências são resolvidas baseadas nos tipos declarados. Então, não é possível declarar mais do que um "componente" que fornece um certo tipo. + +Rotas são declaradas em um único lugar, usando funções declaradas em outros lugares (ao invés de usar decoradores que possam ser colocados diretamente acima da função que controla o _endpoint_). Isso é mais perto de como o Django faz isso do que como Flask (e Starlette) faz. Ele separa no código coisas que são relativamente amarradas. + +!!! check "**FastAPI** inspirado para" + Definir validações extras para tipos de dados usando valores "padrão" de atributos dos modelos. Isso melhora o suporte do editor, e não estava disponível no Pydantic antes. + + Isso na verdade inspirou a atualização de partes do Pydantic, para dar suporte ao mesmo estilo de declaração da validação (toda essa funcionalidade já está disponível no Pydantic). + +### Hug + +Hug foi um dos primeiros frameworks a implementar a declaração de tipos de parâmetros usando Python _type hints_. Isso foi uma ótima idéia que inspirou outras ferramentas a fazer o mesmo. + +Ele usou tipos customizados em suas declarações ao invés dos tipos padrão Python, mas mesmo assim foi um grande passo. + +Ele também foi um dos primeiros frameworks a gerar um _schema_ customizado declarando a API inteira em JSON. + +Ele não era baseado em um padrão como OpenAPI e JSON Schema. Então não poderia ter interação direta com outras ferramentas, como Swagger UI. Mas novamente, era uma idéia muito inovadora. + +Hug tinha um incomum, interessante recurso: usando o mesmo framework, é possível criar tanto APIs como CLIs. + +Como é baseado nos padrões anteriores de frameworks web síncronos (WSGI), ele não pode controlar _Websockets_ e outras coisas, embora ele ainda tenha uma alta performance também. + +!!! info + Hug foi criado por Timothy Crosley, o mesmo criador do `isort`, uma grande ferramenta para ordenação automática de _imports_ em arquivos Python. + +!!! check "Idéias inspiradas para o **FastAPI**" + Hug inspirou partes do APIStar, e foi uma das ferramentas que eu achei mais promissora, ao lado do APIStar. + + Hug ajudou a inspirar o **FastAPI** a usar _type hints_ do Python para declarar parâmetros, e para gerar um _schema_ definindo a API automaticamente. + + Hug inspirou **FastAPI** a declarar um parâmetro de `resposta` em funções para definir cabeçalhos e cookies. + +### APIStar (<= 0.5) + +Antes de decidir construir **FastAPI** eu encontrei o servidor **APIStar**. Tinha quase tudo que eu estava procurando e tinha um grande projeto. + +Ele foi uma das primeiras implementações de um framework usando Python _type hints_ para declarar parâmetros e requisições que eu nunca vi (antes no NestJS e Molten). Eu encontrei ele mais ou menos na mesma época que o Hug. Mas o APIStar utilizava o padrão OpenAPI. + +Ele tinha validação de dados automática, serialização de dados e geração de _schema_ OpenAPI baseado nos mesmos _type hints_ em vários locais. + +Definições de _schema_ de corpo não utilizavam os mesmos Python _type hints_ como Pydantic, ele era um pouco mais similar ao Marshmallow, então, o suporte ao editor não seria tão bom, ainda assim, APIStar era a melhor opção disponível. + +Ele obteve as melhores performances em testes na época (somente batido por Starlette). + +A princípio, ele não tinha uma interface web com documentação automática da API, mas eu sabia que poderia adicionar o Swagger UI a ele. + +Ele tinha um sistema de injeção de dependência. Ele exigia pré-registro dos componentes, como outras ferramentas já discutidas acima. Mas ainda era um grande recurso. + +Eu nunca fui capaz de usar ele num projeto inteiro, por não ter integração de segurança, então, eu não pude substituir todos os recursos que eu tinha com os geradores _full-stack_ baseados no Flask-apispec. Eu tive em minha gaveta de projetos a idéia de criar um _pull request_ adicionando essa funcionalidade. + +Mas então, o foco do projeto mudou. + +Ele não era mais um framework web API, como o criador precisava focar no Starlette. + +Agora APIStar é um conjunto de ferramentas para validar especificações OpenAPI, não um framework web. + +!!! info + APIStar foi criado por Tom Christie. O mesmo cara que criou: + + * Django REST Framework + * Starlette (no qual **FastAPI** é baseado) + * Uvicorn (usado por Starlette e **FastAPI**) + +!!! check "**FastAPI** inspirado para" + Existir. + + A idéia de declarar múltiplas coisas (validação de dados, serialização e documentação) com os mesmos tipos Python, que ao mesmo tempo fornecesse grande suporte ao editor, era algo que eu considerava uma brilhante idéia. + + E após procurar por um logo tempo por um framework similar e testar muitas alternativas diferentes, APIStar foi a melhor opção disponível. + + Então APIStar parou de existir como um servidor e Starlette foi criado, e foi uma nova melhor fundação para tal sistema. Essa foi a inspiração final para construir **FastAPI**. + + Eu considero **FastAPI** um "sucessor espiritual" para o APIStar, evoluindo e melhorando os recursos, sistema de tipagem e outras partes, baseado na aprendizagem de todas essas ferramentas acima. + +## Usados por **FastAPI** + +### Pydantic + +Pydantic é uma biblioteca para definir validação de dados, serialização e documentação (usando JSON Schema) baseado nos Python _type hints_. + +Isso faz dele extremamente intuitivo. + +Ele é comparável ao Marshmallow. Embora ele seja mais rápido que Marshmallow em testes de performance. E ele é baseado nos mesmos Python _type hints_, o suporte ao editor é ótimo. + +!!! check "**FastAPI** usa isso para" + Controlar toda a validação de dados, serialização de dados e modelo de documentação automática (baseado no JSON Schema). + + **FastAPI** então pega dados do JSON Schema e coloca eles no OpenAPI, à parte de todas as outras coisas que ele faz. + +### Starlette + +Starlette é um framework/caixa de ferramentas ASGI peso leve, o que é ideal para construir serviços assíncronos de alta performance. + +Ele é muito simples e intuitivo. É projetado para ser extensível facilmente, e ter componentes modulares. + +Ele tem: + +* Performance seriamente impressionante. +* Suporte a WebSocket. +* Suporte a GraphQL. +* Tarefas de processamento interno por trás dos panos. +* Eventos de inicialização e encerramento. +* Cliente de testes construído com requests. +* Respostas CORS, GZip, Arquivos Estáticos, Streaming. +* Suporte para Sessão e Cookie. +* 100% coberto por testes. +* Código base 100% anotado com tipagem. +* Dependências complexas Zero. + +Starlette é atualmente o mais rápido framework Python testado. Somente ultrapassado pelo Uvicorn, que não é um framework, mas um servidor. + +Starlette fornece toda a funcionalidade básica de um microframework web. + +Mas ele não fornece validação de dados automática, serialização e documentação. + +Essa é uma das principais coisas que **FastAPI** adiciona no topo, tudo baseado em Python _type hints_ (usando Pydantic). Isso, mais o sistema de injeção de dependência, utilidades de segurança, geração de _schema_ OpenAPI, etc. + +!!! note "Detalhes Técnicos" + ASGI é um novo "padrão" sendo desenvolvido pelos membros do time central do Django. Ele ainda não está como "Padrão Python" (PEP), embora eles estejam em processo de fazer isso. + + No entanto, ele já está sendo utilizado como "padrão" por diversas ferramentas. Isso melhora enormemente a interoperabilidade, como você poderia trocar Uvicorn por qualquer outro servidor ASGI (como Daphne ou Hypercorn), ou você poderia adicionar ferramentas compatíveis com ASGI, como `python-socketio`. + +!!! check "**FastAPI** usa isso para" + Controlar todas as partes web centrais. Adiciona recursos no topo. + + A classe `FastAPI` em si herda `Starlette`. + + Então, qualquer coisa que você faz com Starlette, você pode fazer diretamente com **FastAPI**, pois ele é basicamente um Starlette com esteróides. + +### Uvicorn + +Uvicorn é um servidor ASGI peso leve, construído com uvloop e httptools. + +Ele não é um framework web, mas sim um servidor. Por exemplo, ele não fornece ferramentas para roteamento por rotas. Isso é algo que um framework como Starlette (ou **FastAPI**) poderia fornecer por cima. + +Ele é o servidor recomendado para Starlette e **FastAPI**. + +!!! check "**FastAPI** recomenda isso para" + O principal servidor web para rodar aplicações **FastAPI**. + + Você pode combinar ele com o Gunicorn, para ter um servidor multi-processos assíncrono. + + Verifique mais detalhes na seção [Deployment](deployment.md){.internal-link target=_blank}. + +## Performance e velocidade + +Para entender, comparar e ver a diferença entre Uvicorn, Starlette e FastAPI, verifique a seção sobre [Benchmarks](benchmarks.md){.internal-link target=_blank}. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 64bdeb9e085ab..105f7e6f32fbb 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -27,6 +27,7 @@ nav: - features.md - Tutorial - Guia de Usuário: - tutorial/index.md +- alternatives.md - history-design-future.md - benchmarks.md markdown_extensions: From fc70a2f36f4cbc6713d77b2c3050390575051283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 13:36:04 +0200 Subject: [PATCH 128/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ff03a03797a60..11d1864d5049c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Portuguese for [Alternatives, Inspiration and Comparisons - Alternativas, Inspiração e Comparações](https://fastapi.tiangolo.com/pt/alternatives/). PR [#1325](https://github.com/tiangolo/fastapi/pull/1325) by [@Serrones](https://github.com/Serrones). * Fix 2 typos in docs. PR [#1324](https://github.com/tiangolo/fastapi/pull/1324) by [@waynerv](https://github.com/waynerv). * Update CORS docs, fix correct default of `max_age=600`. PR [#1301](https://github.com/tiangolo/fastapi/pull/1301) by [@derekbekoe](https://github.com/derekbekoe). * Add translation of [main page to Portuguese](https://fastapi.tiangolo.com/pt/). PR [#1300](https://github.com/tiangolo/fastapi/pull/1300) by [@Serrones](https://github.com/Serrones). From 08e8dfccbe4cf7b12b98a67551164a7c886e647e Mon Sep 17 00:00:00 2001 From: Chen Rotem Levy Date: Sun, 17 May 2020 14:40:55 +0300 Subject: [PATCH 129/153] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?[en]=20tutorial:=20exception=20handeling=20(#1326)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix typo * ✏️ Fix typo Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/handling-errors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/handling-errors.md b/docs/en/docs/tutorial/handling-errors.md index 99ecc2dc82990..19a6c568491fb 100644 --- a/docs/en/docs/tutorial/handling-errors.md +++ b/docs/en/docs/tutorial/handling-errors.md @@ -115,7 +115,7 @@ So, you will receive a clean error, with an HTTP status code of `418` and a JSON **FastAPI** has some default exception handlers. -These handlers are in charge or returning the default JSON responses when you `raise` an `HTTPException` and when the request has invalid data. +These handlers are in charge of returning the default JSON responses when you `raise` an `HTTPException` and when the request has invalid data. You can override these exception handlers with your own. From 59f7e66ac30207d5a359c077c3cef644615fe5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 13:41:12 +0200 Subject: [PATCH 130/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 11d1864d5049c..5a76210c6679e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix typo. PR [#1326](https://github.com/tiangolo/fastapi/pull/1326) by [@chenl](https://github.com/chenl). * Add translation to Portuguese for [Alternatives, Inspiration and Comparisons - Alternativas, Inspiração e Comparações](https://fastapi.tiangolo.com/pt/alternatives/). PR [#1325](https://github.com/tiangolo/fastapi/pull/1325) by [@Serrones](https://github.com/Serrones). * Fix 2 typos in docs. PR [#1324](https://github.com/tiangolo/fastapi/pull/1324) by [@waynerv](https://github.com/waynerv). * Update CORS docs, fix correct default of `max_age=600`. PR [#1301](https://github.com/tiangolo/fastapi/pull/1301) by [@derekbekoe](https://github.com/derekbekoe). From 3fa033d8d5a4912fad9d4e2fd9d37c0962ef6cc0 Mon Sep 17 00:00:00 2001 From: Stavros Korokithakis Date: Sun, 17 May 2020 14:48:05 +0300 Subject: [PATCH 131/153] =?UTF-8?q?=F0=9F=93=9D=20Add=20warning=20about=20?= =?UTF-8?q?storing=20user=20passwords=20(#1336)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/response-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index 3f06cd4d573b5..9dd0b6dee6624 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -52,7 +52,7 @@ In this case, it might not be a problem, because the user himself is sending the But if we use the same model for another *path operation*, we could be sending our user's passwords to every client. !!! danger - Never send the plain password of a user in a response. + Never store the plain password of a user or send it in a response. ## Add an output model From 6001513c4ff44c74101a0932da61f9b44bb22c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 13:50:05 +0200 Subject: [PATCH 132/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5a76210c6679e..6fa4cb65b1edc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update warning about storing passwords in docs. PR [#1336](https://github.com/tiangolo/fastapi/pull/1336) by [@skorokithakis](https://github.com/skorokithakis). * Fix typo. PR [#1326](https://github.com/tiangolo/fastapi/pull/1326) by [@chenl](https://github.com/chenl). * Add translation to Portuguese for [Alternatives, Inspiration and Comparisons - Alternativas, Inspiração e Comparações](https://fastapi.tiangolo.com/pt/alternatives/). PR [#1325](https://github.com/tiangolo/fastapi/pull/1325) by [@Serrones](https://github.com/Serrones). * Fix 2 typos in docs. PR [#1324](https://github.com/tiangolo/fastapi/pull/1324) by [@waynerv](https://github.com/waynerv). From d9e65147c7226437b06178f29f8cbb8ecbaff9df Mon Sep 17 00:00:00 2001 From: retnikt Date: Sun, 17 May 2020 12:50:42 +0100 Subject: [PATCH 133/153] =?UTF-8?q?=E2=9C=8F=20Fix=20minor=20erratum=20in?= =?UTF-8?q?=20Question=20issue=20template=20(#1344)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/question.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index cc91b23395460..e7466133bef42 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -21,4 +21,4 @@ Is it possible to [...]? ### Additional context -Add any other context or screenshots about the feature request here. +Add any other context or screenshots about the question here. From c1b0e796c6ce27412f988a6b13691aa89faf85e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 13:51:46 +0200 Subject: [PATCH 134/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6fa4cb65b1edc..f25e5a999a5fb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update issue template for for questions. PR [#1344](https://github.com/tiangolo/fastapi/pull/1344) by [@retnikt](https://github.com/retnikt). * Update warning about storing passwords in docs. PR [#1336](https://github.com/tiangolo/fastapi/pull/1336) by [@skorokithakis](https://github.com/skorokithakis). * Fix typo. PR [#1326](https://github.com/tiangolo/fastapi/pull/1326) by [@chenl](https://github.com/chenl). * Add translation to Portuguese for [Alternatives, Inspiration and Comparisons - Alternativas, Inspiração e Comparações](https://fastapi.tiangolo.com/pt/alternatives/). PR [#1325](https://github.com/tiangolo/fastapi/pull/1325) by [@Serrones](https://github.com/Serrones). From b99f350a185d3f1965aecb0e091677e85f6ca459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 16:03:53 +0200 Subject: [PATCH 135/153] =?UTF-8?q?=F0=9F=93=9D=20Add=20links=20to=20GitHu?= =?UTF-8?q?b=20sponsors=20=F0=9F=92=B8=20(#1425)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Add links to GitHub sponsors * ✏ Update link to sponsors --- .github/FUNDING.yml | 1 + docs/en/docs/help-fastapi.md | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000..0ffc101a3f235 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [tiangolo] diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index 9c55e5c305d30..c1a7d918adaf8 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -100,6 +100,12 @@ You can GitHub sponsors. + +There you could buy me a coffee ☕️ to say thanks 😄. + --- Thanks! From 601d8eb8098591df7b2b95d83d319a6f8931402d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 17 May 2020 16:04:35 +0200 Subject: [PATCH 136/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f25e5a999a5fb..815070311500e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add links for funding through [GitHub sponsors](https://github.com/sponsors/tiangolo). PR [#1425](https://github.com/tiangolo/fastapi/pull/1425). * Update issue template for for questions. PR [#1344](https://github.com/tiangolo/fastapi/pull/1344) by [@retnikt](https://github.com/retnikt). * Update warning about storing passwords in docs. PR [#1336](https://github.com/tiangolo/fastapi/pull/1336) by [@skorokithakis](https://github.com/skorokithakis). * Fix typo. PR [#1326](https://github.com/tiangolo/fastapi/pull/1326) by [@chenl](https://github.com/chenl). From ee335bca82bd2a336f54a8a0d0d6a1d457f92389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 23 May 2020 16:04:25 +0200 Subject: [PATCH 137/153] =?UTF-8?q?=E2=9C=85=20Add=20test=20to=20support?= =?UTF-8?q?=20Enums=20with=20their=20own=20re-usable=20schema=20(#1461)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_path_params/test_tutorial005.py | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index bdf0a9e5977ee..b0e0535e8a627 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -76,10 +76,78 @@ } +openapi_schema2 = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model/{model_name}": { + "get": { + "summary": "Get Model", + "operationId": "get_model_model__model_name__get", + "parameters": [ + { + "required": True, + "schema": {"$ref": "#/definitions/ModelName"}, + "name": "model_name", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema + data = response.json() + assert data == openapi_schema or data == openapi_schema2 @pytest.mark.parametrize( From d375dc6ebe158c3ae8646d523d99f984c9e9259e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 23 May 2020 16:05:41 +0200 Subject: [PATCH 138/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 815070311500e..1d06895f2f62f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Allow enums to allow them to have their own schemas in OpenAPI. To support [samuelcolvin/pydantic#1432](https://github.com/samuelcolvin/pydantic/pull/1432) in FastAPI. PR [#1461](https://github.com/tiangolo/fastapi/pull/1461). * Add links for funding through [GitHub sponsors](https://github.com/sponsors/tiangolo). PR [#1425](https://github.com/tiangolo/fastapi/pull/1425). * Update issue template for for questions. PR [#1344](https://github.com/tiangolo/fastapi/pull/1344) by [@retnikt](https://github.com/retnikt). * Update warning about storing passwords in docs. PR [#1336](https://github.com/tiangolo/fastapi/pull/1336) by [@skorokithakis](https://github.com/skorokithakis). From 98bb9f13da4932fac8bc138f77fba38c34e553ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 23 May 2020 16:06:17 +0200 Subject: [PATCH 139/153] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.55?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1d06895f2f62f..4c213963134c0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +## 0.55.0 + * Allow enums to allow them to have their own schemas in OpenAPI. To support [samuelcolvin/pydantic#1432](https://github.com/samuelcolvin/pydantic/pull/1432) in FastAPI. PR [#1461](https://github.com/tiangolo/fastapi/pull/1461). * Add links for funding through [GitHub sponsors](https://github.com/sponsors/tiangolo). PR [#1425](https://github.com/tiangolo/fastapi/pull/1425). * Update issue template for for questions. PR [#1344](https://github.com/tiangolo/fastapi/pull/1344) by [@retnikt](https://github.com/retnikt). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index cda5248efef70..3566b321d1a7b 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.54.2" +__version__ = "0.55.0" from starlette import status From 5984233223777313e95a92d534ab1db711bd606a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 23 May 2020 18:56:18 +0200 Subject: [PATCH 140/153] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Enum=20handling=20?= =?UTF-8?q?with=20their=20own=20schema=20definitions=20(#1463)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 Fix extra support for enum with its own schema * ✅ Fix/update test for enum with its own schema * 🐛 Fix type declarations * 🔧 Update format and lint scripts to support locally installed Pydantic and Starlette * 🐛 Add temporary type ignores while enum schemas are merged --- fastapi/dependencies/utils.py | 10 +++ fastapi/openapi/utils.py | 80 ++++++++++++++----- fastapi/utils.py | 39 ++------- scripts/format.sh | 2 +- scripts/lint.sh | 2 +- .../test_path_params/test_tutorial005.py | 8 +- 6 files changed, 86 insertions(+), 55 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 43ab4a0985bc3..1a660f5d355fa 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -188,6 +188,16 @@ def get_flat_dependant( return flat_dependant +def get_flat_params(dependant: Dependant) -> List[ModelField]: + flat_dependant = get_flat_dependant(dependant, skip_repeats=True) + return ( + flat_dependant.path_params + + flat_dependant.query_params + + flat_dependant.header_params + + flat_dependant.cookie_params + ) + + def is_scalar_field(field: ModelField) -> bool: field_info = get_field_info(field) if not ( diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index c1e66fc8d29b1..b5778327bbad1 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -1,9 +1,10 @@ import http.client -from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, cast +from enum import Enum +from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast from fastapi import routing from fastapi.dependencies.models import Dependant -from fastapi.dependencies.utils import get_flat_dependant +from fastapi.dependencies.utils import get_flat_dependant, get_flat_params from fastapi.encoders import jsonable_encoder from fastapi.openapi.constants import ( METHODS_WITH_BODY, @@ -15,11 +16,14 @@ from fastapi.utils import ( generate_operation_id_for_path, get_field_info, - get_flat_models_from_routes, get_model_definitions, ) from pydantic import BaseModel -from pydantic.schema import field_schema, get_model_name_map +from pydantic.schema import ( + field_schema, + get_flat_models_from_fields, + get_model_name_map, +) from pydantic.utils import lenient_issubclass from starlette.responses import JSONResponse from starlette.routing import BaseRoute @@ -64,16 +68,6 @@ } -def get_openapi_params(dependant: Dependant) -> List[ModelField]: - flat_dependant = get_flat_dependant(dependant, skip_repeats=True) - return ( - flat_dependant.path_params - + flat_dependant.query_params - + flat_dependant.header_params - + flat_dependant.cookie_params - ) - - def get_openapi_security_definitions(flat_dependant: Dependant) -> Tuple[Dict, List]: security_definitions = {} operation_security = [] @@ -90,17 +84,22 @@ def get_openapi_security_definitions(flat_dependant: Dependant) -> Tuple[Dict, L def get_openapi_operation_parameters( + *, all_route_params: Sequence[ModelField], + model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str] ) -> List[Dict[str, Any]]: parameters = [] for param in all_route_params: field_info = get_field_info(param) field_info = cast(Param, field_info) + # ignore mypy error until enum schemas are released parameter = { "name": param.alias, "in": field_info.in_.value, "required": param.required, - "schema": field_schema(param, model_name_map={})[0], + "schema": field_schema( + param, model_name_map=model_name_map, ref_prefix=REF_PREFIX # type: ignore + )[0], } if field_info.description: parameter["description"] = field_info.description @@ -111,13 +110,16 @@ def get_openapi_operation_parameters( def get_openapi_operation_request_body( - *, body_field: Optional[ModelField], model_name_map: Dict[Type[BaseModel], str] + *, + body_field: Optional[ModelField], + model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str] ) -> Optional[Dict]: if not body_field: return None assert isinstance(body_field, ModelField) + # ignore mypy error until enum schemas are released body_schema, _, _ = field_schema( - body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX # type: ignore ) field_info = cast(Body, get_field_info(body_field)) request_media_type = field_info.media_type @@ -176,8 +178,10 @@ def get_openapi_path( operation.setdefault("security", []).extend(operation_security) if security_definitions: security_schemes.update(security_definitions) - all_route_params = get_openapi_params(route.dependant) - operation_parameters = get_openapi_operation_parameters(all_route_params) + all_route_params = get_flat_params(route.dependant) + operation_parameters = get_openapi_operation_parameters( + all_route_params=all_route_params, model_name_map=model_name_map + ) parameters.extend(operation_parameters) if parameters: operation["parameters"] = list( @@ -270,6 +274,38 @@ def get_openapi_path( return path, security_schemes, definitions +def get_flat_models_from_routes( + routes: Sequence[BaseRoute], +) -> Set[Union[Type[BaseModel], Type[Enum]]]: + body_fields_from_routes: List[ModelField] = [] + responses_from_routes: List[ModelField] = [] + request_fields_from_routes: List[ModelField] = [] + callback_flat_models: Set[Union[Type[BaseModel], Type[Enum]]] = set() + for route in routes: + if getattr(route, "include_in_schema", None) and isinstance( + route, routing.APIRoute + ): + if route.body_field: + assert isinstance( + route.body_field, ModelField + ), "A request body must be a Pydantic Field" + body_fields_from_routes.append(route.body_field) + if route.response_field: + responses_from_routes.append(route.response_field) + if route.response_fields: + responses_from_routes.extend(route.response_fields.values()) + if route.callbacks: + callback_flat_models |= get_flat_models_from_routes(route.callbacks) + params = get_flat_params(route.dependant) + request_fields_from_routes.extend(params) + + flat_models = callback_flat_models | get_flat_models_from_fields( + body_fields_from_routes + responses_from_routes + request_fields_from_routes, + known_models=set(), + ) + return flat_models + + def get_openapi( *, title: str, @@ -286,9 +322,11 @@ def get_openapi( components: Dict[str, Dict] = {} paths: Dict[str, Dict] = {} flat_models = get_flat_models_from_routes(routes) - model_name_map = get_model_name_map(flat_models) + # ignore mypy error until enum schemas are released + model_name_map = get_model_name_map(flat_models) # type: ignore + # ignore mypy error until enum schemas are released definitions = get_model_definitions( - flat_models=flat_models, model_name_map=model_name_map + flat_models=flat_models, model_name_map=model_name_map # type: ignore ) for route in routes: if isinstance(route, routing.APIRoute): diff --git a/fastapi/utils.py b/fastapi/utils.py index 154dd9aa180e3..c9022fbc3b43b 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -1,17 +1,16 @@ import functools import re from dataclasses import is_dataclass -from typing import Any, Dict, List, Optional, Sequence, Set, Type, Union, cast +from enum import Enum +from typing import Any, Dict, Optional, Set, Type, Union, cast import fastapi -from fastapi import routing from fastapi.logger import logger from fastapi.openapi.constants import REF_PREFIX from pydantic import BaseConfig, BaseModel, create_model from pydantic.class_validators import Validator -from pydantic.schema import get_flat_models_from_fields, model_process_schema +from pydantic.schema import model_process_schema from pydantic.utils import lenient_issubclass -from starlette.routing import BaseRoute try: from pydantic.fields import FieldInfo, ModelField, UndefinedType @@ -50,38 +49,16 @@ def warning_response_model_skip_defaults_deprecated() -> None: ) -def get_flat_models_from_routes(routes: Sequence[BaseRoute]) -> Set[Type[BaseModel]]: - body_fields_from_routes: List[ModelField] = [] - responses_from_routes: List[ModelField] = [] - callback_flat_models: Set[Type[BaseModel]] = set() - for route in routes: - if getattr(route, "include_in_schema", None) and isinstance( - route, routing.APIRoute - ): - if route.body_field: - assert isinstance( - route.body_field, ModelField - ), "A request body must be a Pydantic Field" - body_fields_from_routes.append(route.body_field) - if route.response_field: - responses_from_routes.append(route.response_field) - if route.response_fields: - responses_from_routes.extend(route.response_fields.values()) - if route.callbacks: - callback_flat_models |= get_flat_models_from_routes(route.callbacks) - flat_models = callback_flat_models | get_flat_models_from_fields( - body_fields_from_routes + responses_from_routes, known_models=set() - ) - return flat_models - - def get_model_definitions( - *, flat_models: Set[Type[BaseModel]], model_name_map: Dict[Type[BaseModel], str] + *, + flat_models: Set[Union[Type[BaseModel], Type[Enum]]], + model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], ) -> Dict[str, Any]: definitions: Dict[str, Dict] = {} for model in flat_models: + # ignore mypy error until enum schemas are released m_schema, m_definitions, m_nested_models = model_process_schema( - model, model_name_map=model_name_map, ref_prefix=REF_PREFIX + model, model_name_map=model_name_map, ref_prefix=REF_PREFIX # type: ignore ) definitions.update(m_definitions) model_name = model_name_map[model] diff --git a/scripts/format.sh b/scripts/format.sh index bbcb04354b01a..07ce78f699c0a 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -3,4 +3,4 @@ set -x autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place docs_src fastapi tests scripts --exclude=__init__.py black fastapi tests docs_src scripts -isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --thirdparty fastapi --apply fastapi tests docs_src scripts +isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --thirdparty fastapi --thirdparty pydantic --thirdparty starlette --apply fastapi tests docs_src scripts diff --git a/scripts/lint.sh b/scripts/lint.sh index 6472f18454adc..ec0f7f41bfcf5 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,4 +5,4 @@ set -x mypy fastapi black fastapi tests --check -isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --check-only --thirdparty fastapi fastapi tests +isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --check-only --thirdparty fastapi --thirdparty fastapi --thirdparty pydantic --thirdparty starlette fastapi tests diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index b0e0535e8a627..836a6264b22bc 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -87,7 +87,7 @@ "parameters": [ { "required": True, - "schema": {"$ref": "#/definitions/ModelName"}, + "schema": {"$ref": "#/components/schemas/ModelName"}, "name": "model_name", "in": "path", } @@ -124,6 +124,12 @@ } }, }, + "ModelName": { + "title": "ModelName", + "enum": ["alexnet", "resnet", "lenet"], + "type": "string", + "description": "An enumeration.", + }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], From 1092261ae18099482de89be92f279ae05572cd59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 23 May 2020 18:58:17 +0200 Subject: [PATCH 141/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4c213963134c0..40ddf949080ac 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +* Fix handling of enums with their own schema in path parameters. To support [samuelcolvin/pydantic#1432](https://github.com/samuelcolvin/pydantic/pull/1432) in FastAPI. PR [#1463](https://github.com/tiangolo/fastapi/pull/1463). + ## 0.55.0 * Allow enums to allow them to have their own schemas in OpenAPI. To support [samuelcolvin/pydantic#1432](https://github.com/samuelcolvin/pydantic/pull/1432) in FastAPI. PR [#1461](https://github.com/tiangolo/fastapi/pull/1461). From 8d844bc5cff051cdcef533d811204beb5aedf2f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 23 May 2020 18:59:02 +0200 Subject: [PATCH 142/153] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.55?= =?UTF-8?q?.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 40ddf949080ac..618eaafd1d3f4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +## 0.55.1 + * Fix handling of enums with their own schema in path parameters. To support [samuelcolvin/pydantic#1432](https://github.com/samuelcolvin/pydantic/pull/1432) in FastAPI. PR [#1463](https://github.com/tiangolo/fastapi/pull/1463). ## 0.55.0 diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 3566b321d1a7b..eb083ec0e486e 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.55.0" +__version__ = "0.55.1" from starlette import status From 374cdf29a93b0aeffe085b18c7c0ae309e9d80c8 Mon Sep 17 00:00:00 2001 From: Xie Wei <39515546+waynerv@users.noreply.github.com> Date: Sun, 24 May 2020 13:43:26 +0800 Subject: [PATCH 143/153] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20translat?= =?UTF-8?q?ion=20for=20docs/python-types.md=20(#1197)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Chinese tranlation for docs/python-types.md * improve translation --- docs/zh/docs/python-types.md | 286 +++++++++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 287 insertions(+) create mode 100644 docs/zh/docs/python-types.md diff --git a/docs/zh/docs/python-types.md b/docs/zh/docs/python-types.md new file mode 100644 index 0000000000000..0c9e9db470ed1 --- /dev/null +++ b/docs/zh/docs/python-types.md @@ -0,0 +1,286 @@ +# Python 类型提示简介 + +**Python 3.6+ 版本**加入了对"类型提示"的支持。 + +这些**"类型提示"**是一种新的语法(在 Python 3.6 版本加入)用来声明一个变量的类型。 + +通过声明变量的类型,编辑器和一些工具能给你提供更好的支持。 + +这只是一个关于 Python 类型提示的**快速入门 / 复习**。它仅涵盖与 **FastAPI** 一起使用所需的最少部分...实际上只有很少一点。 + +整个 **FastAPI** 都基于这些类型提示构建,它们带来了许多优点和好处。 + +但即使你不会用到 **FastAPI**,了解一下类型提示也会让你从中受益。 + +!!! note + 如果你已经精通 Python,并且了解关于类型提示的一切知识,直接跳到下一章节吧。 + +## 动机 + +让我们从一个简单的例子开始: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +运行这段程序将输出: + +``` +John Doe +``` + +这个函数做了下面这些事情: + +* 接收 `first_name` 和 `last_name` 参数。 +* 通过 `title()` 将每个参数的第一个字母转换为大写形式。 +* 中间用一个空格来拼接它们。 + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### 修改示例 + +这是一个非常简单的程序。 + +现在假设你将从头开始编写这段程序。 + +在某一时刻,你开始定义函数,并且准备好了参数...。 + +现在你需要调用一个"将第一个字母转换为大写形式的方法"。 + +等等,那个方法是什么来着?`upper`?还是 `uppercase`?`first_uppercase`?`capitalize`? + +然后你尝试向程序员老手的朋友——编辑器自动补全寻求帮助。 + +输入函数的第一个参数 `first_name`,输入点号(`.`)然后敲下 `Ctrl+Space` 来触发代码补全。 + +但遗憾的是并没有起什么作用: + + + +### 添加类型 + +让我们来修改上面例子的一行代码。 + +我们将把下面这段代码中的函数参数从: + +```Python + first_name, last_name +``` + +改成: + +```Python + first_name: str, last_name: str +``` + +就是这样。 + +这些就是"类型提示": + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +这和声明默认值是不同的,例如: + +```Python + first_name="john", last_name="doe" +``` + +这两者不一样。 + +我们用的是冒号(`:`),不是等号(`=`)。 + +而且添加类型提示一般不会改变原来的运行结果。 + +现在假设我们又一次正在创建这个函数,这次添加了类型提示。 + +在同样的地方,通过 `Ctrl+Space` 触发自动补全,你会发现: + + + +这样,你可以滚动查看选项,直到你找到看起来眼熟的那个: + + + +## 更多动机 + +下面是一个已经有类型提示的函数: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +因为编辑器已经知道了这些变量的类型,所以不仅能对代码进行补全,还能检查其中的错误: + + + +现在你知道了必须先修复这个问题,通过 `str(age)` 把 `age` 转换成字符串: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## 声明类型 + +你刚刚看到的就是声明类型提示的主要场景。用于函数的参数。 + +这也是你将在 **FastAPI** 中使用它们的主要场景。 + +### 简单类型 + +不只是 `str`,你能够声明所有的标准 Python 类型。 + +比如以下类型: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### 嵌套类型 + +有些容器数据结构可以包含其他的值,比如 `dict`、`list`、`set` 和 `tuple`。它们内部的值也会拥有自己的类型。 + +你可以使用 Python 的 `typing` 标准库来声明这些类型以及子类型。 + +它专门用来支持这些类型提示。 + +#### 列表 + +例如,让我们来定义一个由 `str` 组成的 `list` 变量。 + +从 `typing` 模块导入 `List`(注意是大写的 `L`): + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial006.py!} +``` + +同样以冒号(`:`)来声明这个变量。 + +输入 `List` 作为类型。 + +由于列表是带有"子类型"的类型,所以我们把子类型放在方括号中: + +```Python hl_lines="4" +{!../../../docs_src/python_types/tutorial006.py!} +``` + +这表示:"变量 `items` 是一个 `list`,并且这个列表里的每一个元素都是 `str`"。 + +这样,即使在处理列表中的元素时,你的编辑器也可以提供支持。 + +没有类型,几乎是不可能实现下面这样: + + + +注意,变量 `item` 是列表 `items` 中的元素之一。 + +而且,编辑器仍然知道它是一个 `str`,并为此提供了支持。 + +#### 元组和集合 + +声明 `tuple` 和 `set` 的方法也是一样的: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial007.py!} +``` + +这表示: + +* 变量 `items_t` 是一个 `tuple`,其中的每个元素都是 `int` 类型。 +* 变量 `items_s` 是一个 `set`,其中的每个元素都是 `bytes` 类型。 + +#### 字典 + +定义 `dict` 时,需要传入两个子类型,用逗号进行分隔。 + +第一个子类型声明 `dict` 的所有键。 + +第二个子类型声明 `dict` 的所有值: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial008.py!} +``` + +这表示: + +* 变量 `prices` 是一个 `dict`: + * 这个 `dict` 的所有键为 `str` 类型(可以看作是字典内每个元素的名称)。 + * 这个 `dict` 的所有值为 `float` 类型(可以看作是字典内每个元素的价格)。 + +### 类作为类型 + +你也可以将类声明为变量的类型。 + +假设你有一个名为 `Person` 的类,拥有 name 属性: + +```Python hl_lines="1 2 3" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +接下来,你可以将一个变量声明为 `Person` 类型: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +然后,你将再次获得所有的编辑器支持: + + + +## Pydantic 模型 + +Pydantic 是一个用来用来执行数据校验的 Python 库。 + +你可以将数据的"结构"声明为具有属性的类。 + +每个属性都拥有类型。 + +接着你用一些值来创建这个类的实例,这些值会被校验,并被转换为适当的类型(在需要的情况下),返回一个包含所有数据的对象。 + +然后,你将获得这个对象的所有编辑器支持。 + +下面的例子来自 Pydantic 官方文档: + +```Python +{!../../../docs_src/python_types/tutorial010.py!} +``` + +!!! info + 想进一步了解 Pydantic,请阅读其文档. + +整个 **FastAPI** 建立在 Pydantic 的基础之上。 + +实际上你将在 [教程 - 用户指南](tutorial/index.md){.internal-link target=_blank} 看到很多这种情况。 + +## **FastAPI** 中的类型提示 + +**FastAPI** 利用这些类型提示来做下面几件事。 + +使用 **FastAPI** 时用类型提示声明参数可以获得: + +* **编辑器支持**。 +* **类型检查**。 + +...并且 **FastAPI** 还会用这些类型声明来: + +* **定义参数要求**:声明对请求路径参数、查询参数、请求头、请求体、依赖等的要求。 +* **转换数据**:将来自请求的数据转换为需要的类型。 +* **校验数据**: 对于每一个请求: + * 当数据校验失败时自动生成**错误信息**返回给客户端。 +* 使用 OpenAPI **记录** API: + * 然后用于自动生成交互式文档的用户界面。 + +听上去有点抽象。不过不用担心。你将在 [教程 - 用户指南](tutorial/index.md){.internal-link target=_blank} 中看到所有的实战。 + +最重要的是,通过使用标准的 Python 类型,只需要在一个地方声明(而不是添加更多的类、装饰器等),**FastAPI** 会为你完成很多的工作。 + +!!! info + 如果你已经阅读了所有教程,回过头来想了解有关类型的更多信息,来自 `mypy` 的"速查表"是不错的资源。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 4f541075090e1..75a7e9707670b 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -25,6 +25,7 @@ nav: - pt: /pt/ - zh: /zh/ - features.md +- python-types.md - 教程 - 用户指南: - tutorial/index.md - deployment.md From 3b4413f9f5a33f0acbcff64761c7404a7e833398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 24 May 2020 07:47:19 +0200 Subject: [PATCH 144/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 618eaafd1d3f4..c54cce84b0a6f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +* Add translation to Chinese for [Python Types Intro - Python 类型提示简介](https://fastapi.tiangolo.com/zh/python-types/). PR [#1197](https://github.com/tiangolo/fastapi/pull/1197) by [@waynerv](https://github.com/waynerv). + ## 0.55.1 * Fix handling of enums with their own schema in path parameters. To support [samuelcolvin/pydantic#1432](https://github.com/samuelcolvin/pydantic/pull/1432) in FastAPI. PR [#1463](https://github.com/tiangolo/fastapi/pull/1463). From b0414b9929852e4dcbde6b11184a84b29d62358b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 24 May 2020 08:48:09 +0200 Subject: [PATCH 145/153] =?UTF-8?q?=F0=9F=93=9D=20Add=20new=20links=20(#14?= =?UTF-8?q?67)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Update opinions including Netflix and add format * 📝 Add new external links * 📝 Update README --- README.md | 26 ++++++++++++++++---------- docs/en/docs/external-links.md | 22 ++++++++++++++++++++++ docs/en/docs/index.md | 26 ++++++++++++++++---------- docs/es/docs/index.md | 26 ++++++++++++++++---------- 4 files changed, 70 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 90f24558cbfa5..b74421fa9d5aa 100644 --- a/README.md +++ b/README.md @@ -45,35 +45,41 @@ The key features are: ## 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._"
    Kabir Khan - Microsoft (ref)
    --- -"*I’m over the moon excited about **FastAPI**. It’s so fun!*" +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" -
    Brian Okken - Python Bytes podcast host (ref)
    +
    Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
    --- -"*Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that.*" +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" -
    Timothy Crosley - Hug creator (ref)
    +
    Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
    --- -"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*" +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" -"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*" +
    Brian Okken - Python Bytes podcast host (ref)
    -
    Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
    +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +
    Timothy Crosley - Hug creator (ref)
    --- -"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*" +"_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 [...]_" -
    Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
    +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +
    Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
    --- diff --git a/docs/en/docs/external-links.md b/docs/en/docs/external-links.md index 6b63ee349640a..34ba30e0a82df 100644 --- a/docs/en/docs/external-links.md +++ b/docs/en/docs/external-links.md @@ -61,6 +61,23 @@ Here's an incomplete list of some of them. * Apache Kafka producer and consumer with FastAPI and aiokafka by Benjamin Ramser. +* Real-time Notifications with Python and Postgres by Guillermo Cruz. + +* Microservice in Python using FastAPI by Paurakh Sharma Humagain. + +* Build simple API service with Python FastAPI — Part 1 by cuongld2. + +* FastAPI + Zeit.co = 🚀 + by Paul Sec. + +* Build a web API from scratch with FastAPI - the workshop by Sebastián Ramírez (tiangolo). + +* Build a Secure Twilio Webhook with Python and FastAPI by Twilio. + +* Using FastAPI with Django by Stavros Korokithakis. + +* Introducing Dispatch by Netflix. + ### Japanese * FastAPI|DB接続してCRUDするPython製APIサーバーを構築 by @mtitg. @@ -110,11 +127,16 @@ Here's an incomplete list of some of them. ## Podcasts * FastAPI on PythonBytes by Python Bytes FM. +* Build The Next Generation Of Python Web Applications With FastAPI - Episode 259 - interview to Sebastían Ramírez (tiangolo) by Podcast.`__init__`. ## Talks * PyCon UK 2019: FastAPI from the ground up by Chris Withers. +* PyConBY 2020: Serve ML models easily with FastAPI by Sebastián Ramírez (tiangolo). + +* [VIRTUAL] Py.Amsterdam's flying Software Circus: Intro to FastAPI by Sebastián Ramírez (tiangolo). + ## Projects Latest GitHub projects with the topic `fastapi`: diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 90f24558cbfa5..b74421fa9d5aa 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -45,35 +45,41 @@ The key features are: ## 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._"
    Kabir Khan - Microsoft (ref)
    --- -"*I’m over the moon excited about **FastAPI**. It’s so fun!*" +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" -
    Brian Okken - Python Bytes podcast host (ref)
    +
    Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
    --- -"*Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that.*" +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" -
    Timothy Crosley - Hug creator (ref)
    +
    Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
    --- -"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*" +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" -"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*" +
    Brian Okken - Python Bytes podcast host (ref)
    -
    Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
    +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +
    Timothy Crosley - Hug creator (ref)
    --- -"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*" +"_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 [...]_" -
    Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
    +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +
    Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
    --- diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 0a3fbe34bb478..e795f9578a7ac 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -44,35 +44,41 @@ Sus características principales son: ## Opiniones -"*[...] 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._"
    Kabir Khan - Microsoft (ref)
    --- -"*I’m over the moon excited about **FastAPI**. It’s so fun!*" +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" -
    Brian Okken - Python Bytes podcast host (ref)
    +
    Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
    --- -"*Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that.*" +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" -
    Timothy Crosley - Hug creator (ref)
    +
    Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
    --- -"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*" +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" -"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*" +
    Brian Okken - Python Bytes podcast host (ref)
    -
    Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
    +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +
    Timothy Crosley - Hug creator (ref)
    --- -"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*" +"_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 [...]_" -
    Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
    +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +
    Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
    --- From a0e4d38bea74940de013e04a6d6f399d62f04280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 24 May 2020 08:48:52 +0200 Subject: [PATCH 146/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c54cce84b0a6f..09f6b2a83458e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,22 @@ ## Latest changes +* Add new links: + * **English articles**: + * [Real-time Notifications with Python and Postgres](https://wuilly.com/2019/10/real-time-notifications-with-python-and-postgres/) by [Guillermo Cruz](https://wuilly.com/). + * [Microservice in Python using FastAPI](https://dev.to/paurakhsharma/microservice-in-python-using-fastapi-24cc) by [Paurakh Sharma Humagain](https://twitter.com/PaurakhSharma). + * [Build simple API service with Python FastAPI — Part 1](https://dev.to/cuongld2/build-simple-api-service-with-python-fastapi-part-1-581o) by [cuongld2](https://dev.to/cuongld2). + * [FastAPI + Zeit.co = 🚀](https://paulsec.github.io/posts/fastapi_plus_zeit_serverless_fu/) by [Paul Sec](https://twitter.com/PaulWebSec). + * [Build a web API from scratch with FastAPI - the workshop](https://dev.to/tiangolo/build-a-web-api-from-scratch-with-fastapi-the-workshop-2ehe) by [Sebastián Ramírez (tiangolo)](https://twitter.com/tiangolo). + * [Build a Secure Twilio Webhook with Python and FastAPI](https://www.twilio.com/blog/build-secure-twilio-webhook-python-fastapi) by [Twilio](https://www.twilio.com). + * [Using FastAPI with Django](https://www.stavros.io/posts/fastapi-with-django/) by [Stavros Korokithakis](https://twitter.com/Stavros). + * [Introducing Dispatch](https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072) by [Netflix](https://netflixtechblog.com/). + * **Podcasts**: + * [Build The Next Generation Of Python Web Applications With FastAPI - Episode 259 - interview to Sebastían Ramírez (tiangolo)](https://www.pythonpodcast.com/fastapi-web-application-framework-episode-259/) by [Podcast.`__init__`](https://www.pythonpodcast.com/). + * **Talks**: + * [PyConBY 2020: Serve ML models easily with FastAPI](https://www.youtube.com/watch?v=z9K5pwb0rt8) by [Sebastián Ramírez (tiangolo)](https://twitter.com/tiangolo). + * [[VIRTUAL] Py.Amsterdam's flying Software Circus: Intro to FastAPI](https://www.youtube.com/watch?v=PnpTY1f4k2U) by [Sebastián Ramírez (tiangolo)](https://twitter.com/tiangolo). + * PR [#1467](https://github.com/tiangolo/fastapi/pull/1467). * Add translation to Chinese for [Python Types Intro - Python 类型提示简介](https://fastapi.tiangolo.com/zh/python-types/). PR [#1197](https://github.com/tiangolo/fastapi/pull/1197) by [@waynerv](https://github.com/waynerv). ## 0.55.1 From bfd46e562b91e560ff2446eac38320aa2bd6e82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 5 Jun 2020 17:34:43 +0200 Subject: [PATCH 147/153] =?UTF-8?q?=F0=9F=94=A7=20Update=20issue-manager?= =?UTF-8?q?=20GitHub=20action=20(#1520)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflows/{main.yml => issue-manager.yml} | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) rename .github/workflows/{main.yml => issue-manager.yml} (60%) diff --git a/.github/workflows/main.yml b/.github/workflows/issue-manager.yml similarity index 60% rename from .github/workflows/main.yml rename to .github/workflows/issue-manager.yml index 5e0a496842b47..712930e0ba320 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/issue-manager.yml @@ -1,15 +1,24 @@ +name: Issue Manager + on: schedule: - - cron: "0 0 * * *" + - cron: "0 0 * * *" + issue_comment: + types: + - created + - edited + issues: + types: + - labeled jobs: issue-manager: runs-on: ubuntu-latest steps: - - uses: tiangolo/issue-manager@master - with: - token: ${{ secrets.GITHUB_TOKEN }} - config: > + - uses: tiangolo/issue-manager@0.2.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + config: > { "answered": { "users": ["tiangolo", "dmontagu"], From 8cfe254400a92c1184c354a92541b401932d24a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 5 Jun 2020 17:35:39 +0200 Subject: [PATCH 148/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 09f6b2a83458e..d1ee4584c1060 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update GitHub action issue-manager. PR [#1520](https://github.com/tiangolo/fastapi/pull/1520). * Add new links: * **English articles**: * [Real-time Notifications with Python and Postgres](https://wuilly.com/2019/10/real-time-notifications-with-python-and-postgres/) by [Guillermo Cruz](https://wuilly.com/). From 88a887329eaca29e5a78071e182285b042e0e54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 7 Jun 2020 22:00:15 +0200 Subject: [PATCH 149/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20help=20and=20is?= =?UTF-8?q?sue=20templates=20(#1531)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Update help docs: Gitter, issues, links also fix Gitter tab padding * 📝 Update new GitHub issue templates * 📝 Add note about extra help required for new issues --- .github/ISSUE_TEMPLATE/bug_report.md | 62 --------------- .github/ISSUE_TEMPLATE/feature_request.md | 92 +++++++++++++++++++++-- .github/ISSUE_TEMPLATE/question.md | 71 +++++++++++++++-- docs/en/docs/css/custom.css | 5 ++ docs/en/docs/help-fastapi.md | 75 ++++++++++-------- 5 files changed, 197 insertions(+), 108 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index d64ca6b4027c7..0000000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "[BUG]" -labels: bug -assignees: '' - ---- - -### Describe the bug - -Write here a clear and concise description of what the bug is. - -### To Reproduce - -Steps to reproduce the behavior with a minimum self-contained file. - -Replace each part with your own scenario: - -1. Create a file with: - -```Python -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} -``` - -3. Open the browser and call the endpoint `/`. -4. It returns a JSON with `{"Hello": "World"}`. -5. But I expected it to return `{"Hello": "Sara"}`. - -### Expected behavior - -Add 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: - -```bash -python -c "import fastapi; print(fastapi.__version__)" -``` - -- Python version, get it with: - -```bash -python --version -``` - -### Additional context - -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 15b94028ec9ec..75c02cc1fd5f6 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,26 +1,104 @@ --- name: Feature request about: Suggest an idea for this project -title: "[FEATURE]" +title: "" labels: enhancement assignees: '' --- -### Is your feature request related to a problem +### First check -Is your feature request related to a problem? +* [ ] I added a very descriptive title to this issue. +* [ ] 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: + * 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. -Add a clear and concise description of what the problem is. Ex. I want to be able to [...] but I can't because [...] + + +### Example + +Here's a self-contained [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with my use case: + + + +```Python +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} +``` + +### Description + + + +* 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 -Add a clear and concise description of what you want to happen. + + +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 -Add a clear and concise description of any alternative solutions or features you've considered. + + +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 feature request here. + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index e7466133bef42..c4953891630ec 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,24 +1,81 @@ --- -name: Question -about: Ask a question -title: "[QUESTION]" +name: Question or Problem +about: Ask a question or ask about a problem +title: "" labels: question -assignees: '' +assignees: "" --- ### First check +* [ ] I added a very descriptive title to this issue. * [ ] 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. + + + +### Example + +Here's a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with my use case: + + + +```Python +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} +``` ### Description -How can I [...]? + + +* 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: -Is it possible to [...]? +```bash +python --version +``` ### Additional context -Add any other context or screenshots about the question here. + diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 94bdbed43e807..b7de5e34ec533 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -11,3 +11,8 @@ a.internal-link::after { */ content: "\00A0↪"; } + +/* Give space to lower icons so Gitter chat doesn't get on top of them */ +.md-footer-meta { + padding-bottom: 2em; +} diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index c1a7d918adaf8..aed3ed6f6ec18 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -12,28 +12,18 @@ And there are several ways to get help too. ## Star **FastAPI** in GitHub -You can "star" FastAPI in GitHub (clicking the star button at the top right): https://github.com/tiangolo/fastapi. +You can "star" FastAPI in GitHub (clicking the star button at the top right): https://github.com/tiangolo/fastapi. ⭐️ 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): https://github.com/tiangolo/fastapi. +You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/fastapi. 👀 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. -## Join the chat - - - Join the chat at https://gitter.im/tiangolo/fastapi - - -Join the chat on Gitter: https://gitter.im/tiangolo/fastapi. - -There you can ask quick questions, help others, share ideas, etc. - ## Connect with the author You can connect with me (Sebastián Ramírez / `tiangolo`), the author. @@ -45,39 +35,32 @@ You can: * Follow me to see when I create a new Open Source project. * Follow me on **Twitter**. * Tell me how you use FastAPI (I love to hear that). - * Ask questions. + * Hear when I make announcements or release new tools. * Connect with me on **Linkedin**. - * Talk to me. - * Endorse me or recommend me :) -* Read what I write (or follow me) on **Medium**. - * Read other ideas, articles and tools I have created. - * Follow me to see when I publish something new. + * Hear when I make announcements or release new tools (although I use Twitter more often 🤷‍♂). +* Read what I write (or follow me) on **Dev.to** or **Medium**. + * Read other ideas, articles, and about tools I have created. + * Follow me to read when I publish something new. ## Tweet about **FastAPI** -Tweet about **FastAPI** and let me and others know why you like it. - -## Let me know how are you using **FastAPI** +Tweet about **FastAPI** 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. -You can let me know: - -* On **Twitter**. -* On **Linkedin**. -* On **Medium**. - ## Vote for FastAPI * Vote for **FastAPI** in Slant. +* Vote for **FastAPI** in AlternativeTo. +* Vote for **FastAPI** on awesome-rest. ## Help others with issues in GitHub -You can see existing issues and try and help others. +You can see existing issues and try and help others, most of the times they are questions that you might already know the answer for. 🤓 ## Watch the GitHub repository -You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/fastapi. +You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/fastapi. 👀 If you select "Watching" instead of "Releases only", you will receive notifications when someone creates a new issue. @@ -87,9 +70,10 @@ Then you can try and help them solving those issues. You can create a new issue in the GitHub repository, for example to: -* Report a bug/issue. +* Ask a question or ask about a problem. * Suggest a new feature. -* Ask a question. + +**Note**: if you create an issue then I'm going to ask you to also help others. 😉 ## Create a Pull Request @@ -100,12 +84,39 @@ You can + Join the chat at https://gitter.im/tiangolo/fastapi + + +Join the chat on Gitter: https://gitter.im/tiangolo/fastapi. + +There you can have quick conversations with others, help others, share ideas, etc. + +But have in mind that as it allows 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 to 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 Gitter chat. 😅 + +Conversations in Gitter are also not as easily searchable as in GitHub, so questions and answers might get lost in the conversation. + +On the other side, there's more than 1000 people in the chat, 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 GitHub sponsors. There you could buy me a coffee ☕️ to say thanks 😄. +## 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: + +* Samuel Colvin (Pydantic) +* Encode (Starlette, Uvicorn) + --- -Thanks! +Thanks! 🚀 From 543ef7753aff639ad3aed7c153e42f719e361d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 7 Jun 2020 22:02:36 +0200 Subject: [PATCH 150/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d1ee4584c1060..5833040889a76 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update new issue templates and docs: [Help FastAPI - Get Help](https://fastapi.tiangolo.com/help-fastapi/). PR [#1531](https://github.com/tiangolo/fastapi/pull/1531). * Update GitHub action issue-manager. PR [#1520](https://github.com/tiangolo/fastapi/pull/1520). * Add new links: * **English articles**: From 2f478eeca643c5c66370676cb521397479508e69 Mon Sep 17 00:00:00 2001 From: Ingmar Steen Date: Thu, 11 Jun 2020 23:53:19 +0200 Subject: [PATCH 151/153] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20ASGI?= =?UTF-8?q?=20root=5Fpath=20for=20openapi=20docs=20(#1199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use ASGI root_path when it is provided and openapi_prefix is empty. * Strip trailing slashes from root_path. * Please mypy. * Fix extending openapi test. * 📝 Add docs and tutorial for using root_path behind a proxy * ♻️ Refactor application root_path logic, use root_path, deprecate openapi_prefix * ✅ Add tests for Behind a Proxy with root_path * ♻️ Refactor test * 📝 Update/add docs for Sub-applications and Behind a Proxy * 📝 Update Extending OpenAPI with openapi_prefix parameter * ✅ Add test for deprecated openapi_prefix Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/behind-a-proxy.md | 281 ++++++++++++++++++ docs/en/docs/advanced/extending-openapi.md | 15 +- .../docs/advanced/sub-applications-proxy.md | 100 ------- docs/en/docs/advanced/sub-applications.md | 73 +++++ .../img/tutorial/behind-a-proxy/image01.png | Bin 0 -> 60301 bytes .../img/tutorial/behind-a-proxy/image02.png | Bin 0 -> 29123 bytes docs/en/mkdocs.yml | 3 +- docs_src/behind_a_proxy/tutorial001.py | 8 + docs_src/behind_a_proxy/tutorial002.py | 8 + docs_src/extending_openapi/tutorial001.py | 3 +- docs_src/sub_applications/tutorial001.py | 2 +- fastapi/applications.py | 33 +- tests/test_deprecated_openapi_prefix.py | 43 +++ .../test_behind_a_proxy/__init__.py | 0 .../test_behind_a_proxy/test_tutorial001.py | 36 +++ .../test_behind_a_proxy/test_tutorial002.py | 36 +++ 16 files changed, 527 insertions(+), 114 deletions(-) create mode 100644 docs/en/docs/advanced/behind-a-proxy.md delete mode 100644 docs/en/docs/advanced/sub-applications-proxy.md create mode 100644 docs/en/docs/advanced/sub-applications.md create mode 100644 docs/en/docs/img/tutorial/behind-a-proxy/image01.png create mode 100644 docs/en/docs/img/tutorial/behind-a-proxy/image02.png create mode 100644 docs_src/behind_a_proxy/tutorial001.py create mode 100644 docs_src/behind_a_proxy/tutorial002.py create mode 100644 tests/test_deprecated_openapi_prefix.py create mode 100644 tests/test_tutorial/test_behind_a_proxy/__init__.py create mode 100644 tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py create mode 100644 tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md new file mode 100644 index 0000000000000..660e374a43019 --- /dev/null +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -0,0 +1,281 @@ +# 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 that the JSON payload with the OpenAPI schema has the path defined as `/api/v1/app` (behind the proxy) instead of `/app`. For example, something like: + +```JSON hl_lines="5" +{ + "openapi": "3.0.2", + // More stuff here + "paths": { + "/api/v1/app": { + // 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: + +
    + +```console +$ uvicorn main:app --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
    + +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: + +
    + +```console +$ uvicorn main:app --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
    + +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 http://127.0.0.1:8000/app 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 Traefik. + +Download Traefik, 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: + +
    + +```console +$ ./traefik --configFile=traefik.toml + +INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml +``` + +
    + +And now start your app with Uvicorn, using the `--root-path` option: + +
    + +```console +$ uvicorn main:app --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
    + +### Check the responses + +Now, if you go to the URL with the port for Uvicorn: http://127.0.0.1:8000/app, 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: http://127.0.0.1:9999/api/vi/app. + +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 http://127.0.0.1:8000/docs: + + + +But if we access the docs UI at the "official" URL using the proxy, at `/api/v1/docs`, it works correctly! 🎉 + +Right as we wanted it. ✔️ + +This is because FastAPI uses this `root_path` internally to tell the docs UI to use the URL for OpenAPI with the path prefix provided by `root_path`. + +You can check it at http://127.0.0.1:9999/api/v1/docs: + + + +## 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. ✨ diff --git a/docs/en/docs/advanced/extending-openapi.md b/docs/en/docs/advanced/extending-openapi.md index f98be49a6f3c2..30cd857d5f80d 100644 --- a/docs/en/docs/advanced/extending-openapi.md +++ b/docs/en/docs/advanced/extending-openapi.md @@ -52,15 +52,22 @@ First, write all your **FastAPI** application as normally: Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function: -```Python hl_lines="2 15 16 17 18 19 20" +```Python hl_lines="2 15 16 17 18 19 20 21" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` +!!! tip + The `openapi_prefix` will contain any prefix needed for the generated OpenAPI *path operations*. + + FastAPI will automatically use the `root_path` to pass it in the `openapi_prefix`. + + But the important thing is that your function should receive that parameter `openapi_prefix` and pass it along. + ### 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 22 23" +```Python hl_lines="22 23 24" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` @@ -72,7 +79,7 @@ That way, your application won't have to generate the schema every time a user o 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" +```Python hl_lines="13 14 25 26" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` @@ -80,7 +87,7 @@ It will be generated only once, and then the same cached schema will be used for Now you can replace the `.openapi()` method with your new function. -```Python hl_lines="28" +```Python hl_lines="29" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` diff --git a/docs/en/docs/advanced/sub-applications-proxy.md b/docs/en/docs/advanced/sub-applications-proxy.md deleted file mode 100644 index 03a7f94460088..0000000000000 --- a/docs/en/docs/advanced/sub-applications-proxy.md +++ /dev/null @@ -1,100 +0,0 @@ -# Sub Applications - Behind a Proxy, Mounts - -There are at least two situations where you could need to create your **FastAPI** application using some specific paths. - -But then you need to set them up to be served with a path prefix. - -It could happen if you have a: - -* **Proxy** server. -* You are "**mounting**" a FastAPI application inside another FastAPI application (or inside another ASGI application, like Starlette). - -## Proxy - -Having a proxy in this case means that you could declare a path at `/app`, but then, you could need to 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` will actually be served at `/api/v1/app`. - -Even though your application "thinks" it is serving at `/app`. - -And the Proxy could be re-writing the path "on the fly" to keep your application convinced that it is serving at `/app`. - -Up to here, everything would work as normally. - -But then, when you open the integrated docs, they 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. - -So, it's needed that the frontend looks for the OpenAPI schema at `/api/v1/openapi.json`. - -And it's also needed that the returned JSON OpenAPI schema has the defined path at `/api/v1/app` (behind the proxy) instead of `/app`. - ---- - -For these cases, you can declare an `openapi_prefix` parameter in your `FastAPI` application. - -See the section below, about "mounting", for an example. - -## Mounting a **FastAPI** application - -"Mounting" means adding a complete "independent" application in a specific path, that then takes care of handling all the sub-paths. - -You could want to do this if you have several "independent" applications that you want to separate, having their own independent OpenAPI schema and user interfaces. - -### Top-level application - -First, create the main, top-level, **FastAPI** application, and its *path operations*: - -```Python hl_lines="3 6 7 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". - -When creating the sub-application, use the parameter `openapi_prefix`. In this case, with a prefix of `/subapi`: - -```Python hl_lines="11 14 15 16" -{!../../../docs_src/sub_applications/tutorial001.py!} -``` - -### Mount the sub-application - -In your top-level application, `app`, mount the sub-application, `subapi`. - -Here you need to make sure you use the same path that you used for the `openapi_prefix`, in this case, `/subapi`: - -```Python hl_lines="11 19" -{!../../../docs_src/sub_applications/tutorial001.py!} -``` - -## Check the automatic API docs - -Now, run `uvicorn`, if your file is at `main.py`, it would be: - -
    - -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
    - -And open the docs at http://127.0.0.1:8000/docs. - -You will see the automatic API docs for the main app, including only its own paths: - - - -And then, open the docs for the sub-application, at http://127.0.0.1:8000/subapi/docs. - -You will see the automatic API docs for the sub-application, including only its own sub-paths, with their correct prefix: - - - -If you try interacting with any of the two user interfaces, they will work, because the browser will be able to talk to the correct path (or sub-path). diff --git a/docs/en/docs/advanced/sub-applications.md b/docs/en/docs/advanced/sub-applications.md new file mode 100644 index 0000000000000..68d5790db8a03 --- /dev/null +++ b/docs/en/docs/advanced/sub-applications.md @@ -0,0 +1,73 @@ +# 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 all 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 7 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 15 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: + +
    + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
    + +And open the docs at http://127.0.0.1:8000/docs. + +You will see the automatic API docs for the main app, including only its own _path operations_: + + + +And then, open the docs for the sub-application, at http://127.0.0.1:8000/subapi/docs. + +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`: + + + +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}. diff --git a/docs/en/docs/img/tutorial/behind-a-proxy/image01.png b/docs/en/docs/img/tutorial/behind-a-proxy/image01.png new file mode 100644 index 0000000000000000000000000000000000000000..4ceae4421988a968a2903e55aacd66fc82741742 GIT binary patch literal 60301 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 RN&w_Q8Y()<#WyU&{tLI?+YkT% literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/tutorial/behind-a-proxy/image02.png b/docs/en/docs/img/tutorial/behind-a-proxy/image02.png new file mode 100644 index 0000000000000000000000000000000000000000..8012031401c8365f9ff01c92479e05854bc8bf00 GIT binary patch literal 29123 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 None: self.default_response_class = default_response_class @@ -68,7 +70,15 @@ def __init__( self.description = description self.version = version self.openapi_url = openapi_url - self.openapi_prefix = openapi_prefix.rstrip("/") + # TODO: remove when discarding the openapi_prefix parameter + if openapi_prefix: + logger.warning( + '"openapi_prefix" has been deprecated in favor of "root_path", which ' + "follows more closely the ASGI standard, is simpler, and more " + "automatic. Check the docs at " + "https://fastapi.tiangolo.com/advanced/sub-applications-proxy/" + ) + self.root_path = root_path or openapi_prefix self.docs_url = docs_url self.redoc_url = redoc_url self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url @@ -84,7 +94,7 @@ def __init__( self.openapi_schema: Optional[Dict[str, Any]] = None self.setup() - def openapi(self) -> Dict: + def openapi(self, openapi_prefix: str = "") -> Dict: if not self.openapi_schema: self.openapi_schema = get_openapi( title=self.title, @@ -92,7 +102,7 @@ def openapi(self) -> Dict: openapi_version=self.openapi_version, description=self.description, routes=self.routes, - openapi_prefix=self.openapi_prefix, + openapi_prefix=openapi_prefix, ) return self.openapi_schema @@ -100,17 +110,22 @@ def setup(self) -> None: if self.openapi_url: async def openapi(req: Request) -> JSONResponse: - return JSONResponse(self.openapi()) + root_path = req.scope.get("root_path", "").rstrip("/") + return JSONResponse(self.openapi(root_path)) self.add_route(self.openapi_url, openapi, include_in_schema=False) - openapi_url = self.openapi_prefix + self.openapi_url if self.openapi_url and self.docs_url: async def swagger_ui_html(req: Request) -> HTMLResponse: + root_path = req.scope.get("root_path", "").rstrip("/") + openapi_url = root_path + self.openapi_url + oauth2_redirect_url = self.swagger_ui_oauth2_redirect_url + if oauth2_redirect_url: + oauth2_redirect_url = root_path + oauth2_redirect_url return get_swagger_ui_html( openapi_url=openapi_url, title=self.title + " - Swagger UI", - oauth2_redirect_url=self.swagger_ui_oauth2_redirect_url, + oauth2_redirect_url=oauth2_redirect_url, init_oauth=self.swagger_ui_init_oauth, ) @@ -129,6 +144,8 @@ async def swagger_ui_redirect(req: Request) -> HTMLResponse: if self.openapi_url and self.redoc_url: async def redoc_html(req: Request) -> HTMLResponse: + root_path = req.scope.get("root_path", "").rstrip("/") + openapi_url = root_path + self.openapi_url return get_redoc_html( openapi_url=openapi_url, title=self.title + " - ReDoc" ) @@ -140,6 +157,8 @@ async def redoc_html(req: Request) -> HTMLResponse: ) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if self.root_path: + scope["root_path"] = self.root_path if AsyncExitStack: async with AsyncExitStack() as stack: scope["fastapi_astack"] = stack diff --git a/tests/test_deprecated_openapi_prefix.py b/tests/test_deprecated_openapi_prefix.py new file mode 100644 index 0000000000000..df7e69bd56237 --- /dev/null +++ b/tests/test_deprecated_openapi_prefix.py @@ -0,0 +1,43 @@ +from fastapi import FastAPI, Request +from fastapi.testclient import TestClient + +app = FastAPI(openapi_prefix="/api/v1") + + +@app.get("/app") +def read_main(request: Request): + return {"message": "Hello World", "root_path": request.scope.get("root_path")} + + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/api/v1/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, +} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_main(): + response = client.get("/app") + assert response.status_code == 200 + assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} diff --git a/tests/test_tutorial/test_behind_a_proxy/__init__.py b/tests/test_tutorial/test_behind_a_proxy/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py new file mode 100644 index 0000000000000..8b3b526edc38a --- /dev/null +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py @@ -0,0 +1,36 @@ +from fastapi.testclient import TestClient + +from behind_a_proxy.tutorial001 import app + +client = TestClient(app, root_path="/api/v1") + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/api/v1/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, +} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_main(): + response = client.get("/app") + assert response.status_code == 200 + assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py new file mode 100644 index 0000000000000..0a889c4697f55 --- /dev/null +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py @@ -0,0 +1,36 @@ +from fastapi.testclient import TestClient + +from behind_a_proxy.tutorial002 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/api/v1/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, +} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_main(): + response = client.get("/app") + assert response.status_code == 200 + assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} From da7826b0ebdf1cf816ebab3bb0b5143dbce4fa39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Jun 2020 00:05:17 +0200 Subject: [PATCH 152/153] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5833040889a76..ba40725eb1b0f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,14 @@ ## Latest changes +* Add support for ASGI `root_path`: + * Use `root_path` internally for mounted applications, so that OpenAPI and the docs UI works automatically without extra configurations and parameters. + * Add new `root_path` parameter for `FastAPI` applications to provide it in cases where it can be set with the command line (e.g. for Uvicorn and Hypercorn, with the parameter `--root-path`). + * Deprecate `openapi_prefix` parameter in favor of the new `root_path` parameter. + * Add new/updated docs for [Sub Applications - Mounts](https://fastapi.tiangolo.com/advanced/sub-applications/), without `openapi_prefix` (as it is now handled automatically). + * Add new/updated docs for [Behind a Proxy](https://fastapi.tiangolo.com/advanced/behind-a-proxy/), including how to setup a local testing proxy with Traefik and using `root_path`. + * Update docs for [Extending OpenAPI](https://fastapi.tiangolo.com/advanced/extending-openapi/) with the new `openapi_prefix` parameter passed (internally generated from `root_path`). + * Original PR [#1199](https://github.com/tiangolo/fastapi/pull/1199) by [@iksteen](https://github.com/iksteen). * Update new issue templates and docs: [Help FastAPI - Get Help](https://fastapi.tiangolo.com/help-fastapi/). PR [#1531](https://github.com/tiangolo/fastapi/pull/1531). * Update GitHub action issue-manager. PR [#1520](https://github.com/tiangolo/fastapi/pull/1520). * Add new links: From 072c2bc7f9d4900c8afe75f2775942a63325c569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Jun 2020 00:22:17 +0200 Subject: [PATCH 153/153] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.56?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ba40725eb1b0f..9b26f2c534c07 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +## 0.56.0 + * Add support for ASGI `root_path`: * Use `root_path` internally for mounted applications, so that OpenAPI and the docs UI works automatically without extra configurations and parameters. * Add new `root_path` parameter for `FastAPI` applications to provide it in cases where it can be set with the command line (e.g. for Uvicorn and Hypercorn, with the parameter `--root-path`). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index eb083ec0e486e..a0244bfaf5291 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.55.1" +__version__ = "0.56.0" from starlette import status