From 20d48345465e5b09756cc9ef62fb940c63255a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 24 Aug 2021 12:21:05 +0200 Subject: [PATCH 01/38] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20`read?= =?UTF-8?q?=5Fwith=5Form=5Fmode`,=20to=20support=20SQLModel=20relationship?= =?UTF-8?q?=20attributes=20(#3757)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/routing.py | 7 +++++ tests/test_read_with_orm_mode.py | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/test_read_with_orm_mode.py diff --git a/fastapi/routing.py b/fastapi/routing.py index 0ad082341..63ad72964 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -65,6 +65,13 @@ def _prepare_response_content( exclude_none: bool = False, ) -> Any: if isinstance(res, BaseModel): + read_with_orm_mode = getattr(res.__config__, "read_with_orm_mode", None) + if read_with_orm_mode: + # Let from_orm extract the data from this model instead of converting + # it now to a dict. + # Otherwise there's no way to extract lazy data that requires attribute + # access instead of dict iteration, e.g. lazy relationships. + return res return res.dict( by_alias=True, exclude_unset=exclude_unset, diff --git a/tests/test_read_with_orm_mode.py b/tests/test_read_with_orm_mode.py new file mode 100644 index 000000000..360ad2503 --- /dev/null +++ b/tests/test_read_with_orm_mode.py @@ -0,0 +1,53 @@ +from typing import Any + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class PersonBase(BaseModel): + name: str + lastname: str + + +class Person(PersonBase): + @property + def full_name(self) -> str: + return f"{self.name} {self.lastname}" + + class Config: + orm_mode = True + read_with_orm_mode = True + + +class PersonCreate(PersonBase): + pass + + +class PersonRead(PersonBase): + full_name: str + + class Config: + orm_mode = True + + +app = FastAPI() + + +@app.post("/people/", response_model=PersonRead) +def create_person(person: PersonCreate) -> Any: + db_person = Person.from_orm(person) + return db_person + + +client = TestClient(app) + + +def test_read_with_orm_mode() -> None: + person_data = {"name": "Dive", "lastname": "Wilson"} + response = client.post("/people/", json=person_data) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == person_data["name"] + assert data["lastname"] == person_data["lastname"] + assert data["full_name"] == person_data["name"] + " " + person_data["lastname"] From dc9c570733441a83232d111ca0df3f93876a8097 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 24 Aug 2021 10:21:45 +0000 Subject: [PATCH 02/38] =?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 15a35f598..0421f754e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for `read_with_orm_mode`, to support SQLModel relationship attributes. PR [#3757](https://github.com/tiangolo/fastapi/pull/3757) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Portuguese translation of `docs/fastapi-people.md`. PR [#3461](https://github.com/tiangolo/fastapi/pull/3461) by [@ComicShrimp](https://github.com/ComicShrimp). * 🌐 Add Chinese translation for `docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#3492](https://github.com/tiangolo/fastapi/pull/3492) by [@jaystone776](https://github.com/jaystone776). * 🔧 Add new Translation tracking issues for German and Indonesian. PR [#3718](https://github.com/tiangolo/fastapi/pull/3718) by [@tiangolo](https://github.com/tiangolo). From 6ac35b1c2aed4392f5227ee5458e23eca2fc52c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 24 Aug 2021 14:13:47 +0200 Subject: [PATCH 03/38] =?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 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0421f754e..f2c89aaaa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,7 +2,10 @@ ## Latest Changes -* ✨ Add support for `read_with_orm_mode`, to support SQLModel relationship attributes. PR [#3757](https://github.com/tiangolo/fastapi/pull/3757) by [@tiangolo](https://github.com/tiangolo). +* ✨ Add support for `read_with_orm_mode`, to support [SQLModel](https://sqlmodel.tiangolo.com/) relationship attributes. PR [#3757](https://github.com/tiangolo/fastapi/pull/3757) by [@tiangolo](https://github.com/tiangolo). + +### Translations + * 🌐 Add Portuguese translation of `docs/fastapi-people.md`. PR [#3461](https://github.com/tiangolo/fastapi/pull/3461) by [@ComicShrimp](https://github.com/ComicShrimp). * 🌐 Add Chinese translation for `docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#3492](https://github.com/tiangolo/fastapi/pull/3492) by [@jaystone776](https://github.com/jaystone776). * 🔧 Add new Translation tracking issues for German and Indonesian. PR [#3718](https://github.com/tiangolo/fastapi/pull/3718) by [@tiangolo](https://github.com/tiangolo). @@ -10,12 +13,15 @@ * 🌐 Add Portuguese translation for `docs/advanced/index.md`. PR [#3460](https://github.com/tiangolo/fastapi/pull/3460) by [@ComicShrimp](https://github.com/ComicShrimp). * 🌐 Portuguese translation of `docs/async.md`. PR [#1330](https://github.com/tiangolo/fastapi/pull/1330) by [@Serrones](https://github.com/Serrones). * 🌐 Add French translation for `docs/async.md`. PR [#3416](https://github.com/tiangolo/fastapi/pull/3416) by [@Smlep](https://github.com/Smlep). + +### Internal + * ✨ Add GitHub Action: Notify Translations. PR [#3715](https://github.com/tiangolo/fastapi/pull/3715) by [@tiangolo](https://github.com/tiangolo). * ✨ Update computation of FastAPI People and sponsors. PR [#3714](https://github.com/tiangolo/fastapi/pull/3714) by [@tiangolo](https://github.com/tiangolo). * ✨ Enable recent Material for MkDocs Insiders features. PR [#3710](https://github.com/tiangolo/fastapi/pull/3710) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove/clean extra imports from examples in docs for features. PR [#3709](https://github.com/tiangolo/fastapi/pull/3709) by [@tiangolo](https://github.com/tiangolo). * ➕ Update docs library to include sources in Markdown. PR [#3648](https://github.com/tiangolo/fastapi/pull/3648) by [@tiangolo](https://github.com/tiangolo). -* ⬆ Add support for Python 3.9. PR [#2298](https://github.com/tiangolo/fastapi/pull/2298) by [@Kludex](https://github.com/Kludex). +* ⬆ Enable tests for Python 3.9. PR [#2298](https://github.com/tiangolo/fastapi/pull/2298) by [@Kludex](https://github.com/Kludex). * 👥 Update FastAPI People. PR [#3642](https://github.com/tiangolo/fastapi/pull/3642) by [@github-actions[bot]](https://github.com/apps/github-actions). ## 0.68.0 From 7b6e198d314e320256c2ed8b62430b2a42c31cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 24 Aug 2021 14:16:09 +0200 Subject: [PATCH 04/38] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.68.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 3 +++ fastapi/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f2c89aaaa..921c2b992 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.68.1 + * ✨ Add support for `read_with_orm_mode`, to support [SQLModel](https://sqlmodel.tiangolo.com/) relationship attributes. PR [#3757](https://github.com/tiangolo/fastapi/pull/3757) by [@tiangolo](https://github.com/tiangolo). ### Translations diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 60f90b80d..316362669 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.68.0" +__version__ = "0.68.1" from starlette import status as status From f49ba24b71c5c2539ba9cc8b8d1408072da5f631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Aug 2021 10:49:08 +0200 Subject: [PATCH 05/38] =?UTF-8?q?=F0=9F=94=A7=20Add=20new=20Sponsor=20Calm?= =?UTF-8?q?code.io=20(#3777)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors.yml | 4 ++++ docs/en/data/sponsors_badge.yml | 1 + docs/en/docs/img/sponsors/calmcode.jpg | Bin 0 -> 16949 bytes 3 files changed, 5 insertions(+) create mode 100644 docs/en/docs/img/sponsors/calmcode.jpg diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index b5bb9a773..e0a4ee783 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -18,3 +18,7 @@ silver: - url: https://testdriven.io/courses/tdd-fastapi/ title: Learn to build high-quality web apps with best practices img: https://fastapi.tiangolo.com/img/sponsors/testdriven.svg +bronze: + - url: https://calmcode.io + title: Code. Simply. Clearly. Calmly. + img: https://fastapi.tiangolo.com/img/sponsors/calmcode.jpg diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 066a82506..9e95a6255 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -4,3 +4,4 @@ logins: - investsuite - vimsoHQ - mikeckennedy + - koaning diff --git a/docs/en/docs/img/sponsors/calmcode.jpg b/docs/en/docs/img/sponsors/calmcode.jpg new file mode 100644 index 0000000000000000000000000000000000000000..59e68dd07357878a76bd228b61fb62f201bd0236 GIT binary patch literal 16949 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 Date: Fri, 27 Aug 2021 08:49:40 +0000 Subject: [PATCH 06/38] =?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 921c2b992..1502e4de6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add new Sponsor Calmcode.io. PR [#3777](https://github.com/tiangolo/fastapi/pull/3777) by [@tiangolo](https://github.com/tiangolo). ## 0.68.1 From c8b4e4d455cf0ca370d2814ac019ffe74e47eb12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 Sep 2021 17:28:22 +0300 Subject: [PATCH 07/38] =?UTF-8?q?=F0=9F=8E=A8=20Tweak=20CSS=20styles=20for?= =?UTF-8?q?=20shell=20animations=20(#3888)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/css/custom.css | 14 ++++++++++++++ docs/en/docs/css/termynal.css | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 997bcd3af..7d3503e49 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -1,3 +1,13 @@ +.termynal-comment { + color: #4a968f; + font-style: italic; + display: block; +} + +.termy [data-termynal] { + white-space: pre-wrap; +} + a.external-link::after { /* \00A0 is a non-breaking space to make the mark be on the same line as the link @@ -12,6 +22,10 @@ a.internal-link::after { content: "\00A0↪"; } +.shadow { + box-shadow: 5px 5px 10px #999; +} + /* 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/css/termynal.css b/docs/en/docs/css/termynal.css index 0484e65d4..406c00897 100644 --- a/docs/en/docs/css/termynal.css +++ b/docs/en/docs/css/termynal.css @@ -17,7 +17,8 @@ max-width: 100%; background: var(--color-bg); color: var(--color-text); - font-size: 18px; + /* font-size: 18px; */ + font-size: 15px; /* 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; From eaeafc32c49100d6b3f92acdf225761ea48bb9a7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Sep 2021 14:29:04 +0000 Subject: [PATCH 08/38] =?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 1502e4de6..ea50f4ab3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🎨 Tweak CSS styles for shell animations. PR [#3888](https://github.com/tiangolo/fastapi/pull/3888) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add new Sponsor Calmcode.io. PR [#3777](https://github.com/tiangolo/fastapi/pull/3777) by [@tiangolo](https://github.com/tiangolo). ## 0.68.1 From 3a786a7a3aef87fc1a313acf60a0782324ca3222 Mon Sep 17 00:00:00 2001 From: Maximilian Wassink Date: Mon, 13 Sep 2021 19:29:03 +0200 Subject: [PATCH 09/38] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20translation?= =?UTF-8?q?=20for=20`docs/features.md`=20(#3699)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/features.md | 204 +++++++++++++++++++++++++++++++++++++++ docs/de/mkdocs.yml | 1 + 2 files changed, 205 insertions(+) create mode 100644 docs/de/docs/features.md diff --git a/docs/de/docs/features.md b/docs/de/docs/features.md new file mode 100644 index 000000000..e5b38616f --- /dev/null +++ b/docs/de/docs/features.md @@ -0,0 +1,204 @@ +# Merkmale + +## FastAPI Merkmale + +**FastAPI** ermöglicht Ihnen folgendes: + +### Basiert auf offenen Standards + +* OpenAPI für API-Erstellung, zusammen mit Deklarationen von Pfad Operationen, Parameter, Nachrichtenrumpf-Anfragen (englisch: body request), Sicherheit, etc. +* Automatische Dokumentation der Datenentitäten mit dem JSON Schema (OpenAPI basiert selber auf dem JSON Schema). +* Entworfen auf Grundlage dieser Standards nach einer sorgfältigen Studie, statt einer nachträglichen Schicht über diesen Standards. +* Dies ermöglicht automatische **Quellcode-Generierung auf Benutzerebene** in vielen Sprachen. + +### Automatische Dokumentation + +Mit einer interaktiven API-Dokumentation und explorativen webbasierten Benutzerschnittstellen. Da FastAPI auf OpenAPI basiert, gibt es hierzu mehrere Optionen, wobei zwei standartmäßig vorhanden sind. + +* Swagger UI, bietet interaktive Exploration: testen und rufen Sie ihre API direkt vom Webbrowser auf. + +![Swagger UI Interaktion](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Alternative API-Dokumentation mit ReDoc. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Nur modernes Python + +Alles basiert auf **Python 3.6 Typ**-Deklarationen (dank Pydantic). Es muss keine neue Syntax gelernt werden, nur standardisiertes modernes Python. + + + +Wenn Sie eine kurze, zweiminütige, Auffrischung in der Benutzung von Python Typ-Deklarationen benötigen (auch wenn Sie FastAPI nicht nutzen), schauen Sie sich diese kurze Einführung an (Englisch): Python Types{.internal-link target=_blank}. + +Sie schreiben Standard-Python mit Typ-Deklarationen: + +```Python +from typing import List, Dict +from datetime import date + +from pydantic import BaseModel + +# Deklariere eine Variable als str +# und bekomme Editor-Unterstütung innerhalb der Funktion +def main(user_id: str): + return user_id + + +# Ein Pydantic model +class User(BaseModel): + id: int + name: str + joined: date +``` + +Dies kann nun wiefolgt benutzt werden: + +```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` bedeutet: + + Übergebe die Schlüssel und die zugehörigen Werte des `second_user_data` Datenwörterbuches direkt als Schlüssel-Wert Argumente, äquivalent zu: `User(id=4, name="Mary", joined="2018-11-30")` + +### Editor Unterstützung + +FastAPI wurde so entworfen, dass es einfach und intuitiv zu benutzen ist; alle Entscheidungen wurden auf mehreren Editoren getestet (sogar vor der eigentlichen Implementierung), um so eine best mögliche Entwicklererfahrung zu gewährleisten. + +In der letzen Python Entwickler Umfrage stellte sich heraus, dass die meist genutzte Funktion die "Autovervollständigung" ist. + +Die gesamte Struktur von **FastAPI** soll dem gerecht werden. Autovervollständigung funktioniert überall. + +Sie müssen selten in die Dokumentation schauen. + +So kann ihr Editor Sie unterstützen: + +* in Visual Studio Code: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* in PyCharm: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +Sie bekommen Autovervollständigung an Stellen, an denen Sie dies vorher nicht für möglich gehalten hätten. Zum Beispiel der `price` Schlüssel aus einem JSON Datensatz (dieser könnte auch verschachtelt sein) aus einer Anfrage. + +Hierdurch werden Sie nie wieder einen falschen Schlüsselnamen benutzen und sparen sich lästiges Suchen in der Dokumentation, um beispielsweise herauszufinden ob Sie `username` oder `user_name` als Schlüssel verwenden. + +### Kompakt + +FastAPI nutzt für alles sensible **Standard-Einstellungen**, welche optional überall konfiguriert werden können. Alle Parameter können ganz genau an Ihre Bedürfnisse angepasst werden, sodass sie genau die API definieren können, die sie brachen. + +Aber standartmäßig, **"funktioniert einfach"** alles. + +### Validierung + +* Validierung für die meisten (oder alle?) Python **Datentypen**, hierzu gehören: + * JSON Objekte (`dict`). + * JSON Listen (`list`), die den Typ ihrer Elemente definieren. + * Zeichenketten (`str`), mit definierter minimaler und maximaler Länge. + * Zahlen (`int`, `float`) mit minimaler und maximaler Größe, usw. + +* Validierung für ungewögnliche Typen, wie: + * URL. + * Email. + * UUID. + * ... und andere. + +Die gesamte Validierung übernimmt das etablierte und robuste **Pydantic**. + +### Sicherheit und Authentifizierung + +Sicherheit und Authentifizierung integriert. Ohne einen Kompromiss aufgrund einer Datenbank oder den Datenentitäten. + +Unterstützt alle von OpenAPI definierten Sicherheitsschemata, hierzu gehören: + +* HTTP Basis Authentifizierung. +* **OAuth2** (auch mit **JWT Zugriffstokens**). Schauen Sie sich hierzu dieses Tutorial an: [OAuth2 mit JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* API Schlüssel in: + * Kopfzeile (HTTP Header). + * Anfrageparametern. + * Cookies, etc. + +Zusätzlich gibt es alle Sicherheitsfunktionen von Starlette (auch **session cookies**). + +Alles wurde als wiederverwendbare Werkzeuge und Komponenten geschaffen, die einfach in ihre Systeme, Datenablagen, relationale und nicht-relationale Datenbanken, ..., integriert werden können. + +### Einbringen von Abhängigkeiten (meist: Dependency Injection) + +FastAPI enthält ein extrem einfaches, aber extrem mächtiges Dependency Injection System. + +* Selbst Abhängigkeiten können Abhängigkeiten haben, woraus eine Hierachie oder ein **"Graph" von Abhängigkeiten** entsteht. +* **Automatische Umsetzung** durch FastAPI. +* Alle abhängigen Komponenten könnten Daten von Anfragen, **Erweiterungen der Pfadoperations-**Einschränkungen und der automatisierten Dokumentation benötigen. +* **Automatische Validierung** selbst für *Pfadoperationen*-Parameter, die in den Abhängigkeiten definiert wurden. +* Unterstütz komplexe Benutzerauthentifizierungssysteme, mit **Datenbankverbindungen**, usw. +* **Keine Kompromisse** bei Datenbanken, Eingabemasken, usw. Sondern einfache Integration von allen. + +### Unbegrenzte Erweiterungen + +Oder mit anderen Worten, sie werden nicht benötigt. Importieren und nutzen Sie Quellcode nach Bedarf. + +Jede Integration wurde so entworfen, dass sie einfach zu nutzen ist (mit Abhängigkeiten), sodass Sie eine Erweiterung für Ihre Anwendung mit nur zwei Zeilen an Quellcode implementieren können. Hierbei nutzen Sie die selbe Struktur und Syntax, wie bei Pfadoperationen. + +### Getestet + +* 100% Testabdeckung. +* 100% Typen annotiert. +* Verwendet in Produktionsanwendungen. + +## Starlette's Merkmale + +**FastAPI** ist vollkommen kompatibel (und basiert auf) Starlette. Das bedeutet, auch ihr eigner Starlett Quellcode funktioniert. + +`FastAPI` ist eigentlich eine Unterklasse von `Starlette`. Wenn sie also bereits Starlette kennen oder benutzen, können Sie das meiste Ihres Wissen direkt anwenden. + +Mit **FastAPI** bekommen Sie viele von **Starlette**'s Funktionen (da FastAPI nur Starlette auf Steroiden ist): + +* Stark beeindruckende Performanz. Es ist eines der schnellsten Python frameworks, auf Augenhöhe mit **NodeJS** und **Go**. +* **WebSocket**-Unterstützung. +* **GraphQL**-Unterstützung. +* Hintergrundaufgaben im selben Prozess. +* Ereignisse für das Starten und Herunterfahren. +* Testclient basierend auf `requests`. +* **CORS**, GZip, statische Dateien, Antwortfluss. +* **Sitzungs und Cookie** Unterstützung. +* 100% Testabdeckung. +* 100% Typen annotiert. + +## Pydantic's Merkmale + +**FastAPI** ist vollkommen kompatibel (und basiert auf) Pydantic. Das bedeutet, auch jeder zusätzliche Pydantic Quellcode funktioniert. + +Verfügbar sind ebenso externe auf Pydantic basierende Bibliotheken, wie ORMs, ODMs für Datenbanken. + +Daher können Sie in vielen Fällen das Objekt einer Anfrage **direkt zur Datenbank** schicken, weil alles automatisch validiert wird. + +Das selbe gilt auch für die andere Richtung: Sie können jedes Objekt aus der Datenbank **direkt zum Klienten** schicken. + +Mit **FastAPI** bekommen Sie alle Funktionen von **Pydantic** (da FastAPI für die gesamte Datenverarbeitung Pydantic nutzt): + +* **Kein Kopfzerbrechen**: + * Sie müssen keine neue Schemadefinitionssprache lernen. + * Wenn Sie mit Python's Typisierung arbeiten können, können Sie auch mit Pydantic arbeiten. +* Gutes Zusammenspiel mit Ihrer/Ihrem **IDE/linter/Gehirn**: + * Weil Datenstrukturen von Pydantic einfach nur Instanzen ihrer definierten Klassen sind, sollten Autovervollständigung, Linting, mypy und ihre Intuition einwandfrei funktionieren. +* **Schnell**: + * In Vergleichen ist Pydantic schneller als jede andere getestete Bibliothek. +* Validierung von **komplexen Strukturen**: + * Benutzung von hierachischen Pydantic Schemata, Python `typing`’s `List` und `Dict`, etc. + * Validierungen erlauben klare und einfache Datenschemadefinition, überprüft und dokumentiert als JSON Schema. + * Sie können stark **verschachtelte JSON** Objekte haben und diese sind trotzdem validiert und annotiert. +* **Erweiterbar**: + * Pydantic erlaubt die Definition von eigenen Datentypen oder sie können die Validierung mit einer `validator` dekorierten Methode erweitern.. +* 100% Testabdeckung. diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 49fad8ec4..77882db03 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -54,6 +54,7 @@ nav: - tr: /tr/ - uk: /uk/ - zh: /zh/ +- features.md markdown_extensions: - toc: permalink: true From bee35f5ae1fc58e7ab125427ad4287210e99d8b3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 13 Sep 2021 17:29:47 +0000 Subject: [PATCH 10/38] =?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 ea50f4ab3..a581cf394 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add German translation for `docs/features.md`. PR [#3699](https://github.com/tiangolo/fastapi/pull/3699) by [@mawassk](https://github.com/mawassk). * 🎨 Tweak CSS styles for shell animations. PR [#3888](https://github.com/tiangolo/fastapi/pull/3888) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add new Sponsor Calmcode.io. PR [#3777](https://github.com/tiangolo/fastapi/pull/3777) by [@tiangolo](https://github.com/tiangolo). From 64e7deaebc1bebb327cc146f1d65e088b282e731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 27 Sep 2021 16:40:38 +0200 Subject: [PATCH 11/38] =?UTF-8?q?=F0=9F=93=9D=20Upgrade=20HTTPS=20guide=20?= =?UTF-8?q?with=20more=20explanations=20and=20diagrams=20(#3950)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/deployment/https.md | 192 ++++++++++-- .../en/docs/img/deployment/https/https.drawio | 277 ++++++++++++++++++ docs/en/docs/img/deployment/https/https.svg | 62 ++++ .../docs/img/deployment/https/https01.drawio | 78 +++++ docs/en/docs/img/deployment/https/https01.svg | 57 ++++ .../docs/img/deployment/https/https02.drawio | 110 +++++++ docs/en/docs/img/deployment/https/https02.svg | 57 ++++ .../docs/img/deployment/https/https03.drawio | 131 +++++++++ docs/en/docs/img/deployment/https/https03.svg | 62 ++++ .../docs/img/deployment/https/https04.drawio | 152 ++++++++++ docs/en/docs/img/deployment/https/https04.svg | 62 ++++ .../docs/img/deployment/https/https05.drawio | 166 +++++++++++ docs/en/docs/img/deployment/https/https05.svg | 62 ++++ .../docs/img/deployment/https/https06.drawio | 183 ++++++++++++ docs/en/docs/img/deployment/https/https06.svg | 62 ++++ .../docs/img/deployment/https/https07.drawio | 203 +++++++++++++ docs/en/docs/img/deployment/https/https07.svg | 62 ++++ .../docs/img/deployment/https/https08.drawio | 217 ++++++++++++++ docs/en/docs/img/deployment/https/https08.svg | 62 ++++ 19 files changed, 2232 insertions(+), 25 deletions(-) create mode 100644 docs/en/docs/img/deployment/https/https.drawio create mode 100644 docs/en/docs/img/deployment/https/https.svg create mode 100644 docs/en/docs/img/deployment/https/https01.drawio create mode 100644 docs/en/docs/img/deployment/https/https01.svg create mode 100644 docs/en/docs/img/deployment/https/https02.drawio create mode 100644 docs/en/docs/img/deployment/https/https02.svg create mode 100644 docs/en/docs/img/deployment/https/https03.drawio create mode 100644 docs/en/docs/img/deployment/https/https03.svg create mode 100644 docs/en/docs/img/deployment/https/https04.drawio create mode 100644 docs/en/docs/img/deployment/https/https04.svg create mode 100644 docs/en/docs/img/deployment/https/https05.drawio create mode 100644 docs/en/docs/img/deployment/https/https05.svg create mode 100644 docs/en/docs/img/deployment/https/https06.drawio create mode 100644 docs/en/docs/img/deployment/https/https06.svg create mode 100644 docs/en/docs/img/deployment/https/https07.drawio create mode 100644 docs/en/docs/img/deployment/https/https07.svg create mode 100644 docs/en/docs/img/deployment/https/https08.drawio create mode 100644 docs/en/docs/img/deployment/https/https08.svg diff --git a/docs/en/docs/deployment/https.md b/docs/en/docs/deployment/https.md index c735f1f4a..1a3b1a0aa 100644 --- a/docs/en/docs/deployment/https.md +++ b/docs/en/docs/deployment/https.md @@ -7,42 +7,184 @@ But it is way more complex than that. !!! tip If you are in a hurry or don't care, continue with the next sections for step by step instructions to set everything up with different techniques. -To learn the basics of HTTPS, from a consumer perspective, check https://howhttps.works/. +To **learn the basics of HTTPS**, from a consumer perspective, check https://howhttps.works/. -Now, from a developer's perspective, here are several things to have in mind while thinking about HTTPS: +Now, from a **developer's perspective**, here are several things to have in mind while thinking about HTTPS: -* For HTTPS, the server needs to have "certificates" generated by a third party. - * Those certificates are actually acquired from the third-party, not "generated". -* Certificates have a lifetime. - * They expire. - * And then they need to be renewed, acquired again from the third party. -* The encryption of the connection happens at the TCP level. - * That's one layer below HTTP. - * So, the certificate and encryption handling is done before HTTP. -* TCP doesn't know about "domains". Only about IP addresses. - * The information about the specific domain requested goes in the HTTP data. -* The HTTPS certificates "certify" a certain domain, but the protocol and encryption happen at the TCP level, before knowing which domain is being dealt with. -* By default, that would mean that you can only have one HTTPS certificate per IP address. +* For HTTPS, **the server** needs to **have "certificates"** generated by a **third party**. + * Those certificates are actually **acquired** from the third party, not "generated". +* Certificates have a **lifetime**. + * They **expire**. + * And then they need to be **renewed**, **acquired again** from the third party. +* The encryption of the connection happens at the **TCP level**. + * That's one layer **below HTTP**. + * So, the **certificate and encryption** handling is done **before HTTP**. +* **TCP doesn't know about "domains"**. Only about IP addresses. + * The information about the **specific domain** requested goes in the **HTTP data**. +* The **HTTPS certificates** "certify" a **certain domain**, but the protocol and encryption happen at the TCP level, **before knowing** which domain is being dealt with. +* **By default**, that would mean that you can only have **one HTTPS certificate per IP address**. * No matter how big your server is or how small each application you have on it might be. - * There is a solution to this, however. -* There's an extension to the TLS protocol (the one handling the encryption at the TCP level, before HTTP) called SNI. - * This SNI extension allows one single server (with a single IP address) to have several HTTPS certificates and serve multiple HTTPS domains/applications. - * For this to work, a single component (program) running on the server, listening on the public IP address, must have all the HTTPS certificates in the server. -* After obtaining a secure connection, the communication protocol is still HTTP. - * The contents are encrypted, even though they are being sent with the HTTP protocol. + * There is a **solution** to this, however. +* There's an **extension** to the **TLS** protocol (the one handling the encryption at the TCP level, before HTTP) called **SNI**. + * This SNI extension allows one single server (with a **single IP address**) to have **several HTTPS certificates** and serve **multiple HTTPS domains/applications**. + * For this to work, a **single** component (program) running on the server, listening on the **public IP address**, must have **all the HTTPS certificates** in the server. +* **After** obtaining a secure connection, the communication protocol is **still HTTP**. + * The contents are **encrypted**, even though they are being sent with the **HTTP protocol**. -It is a common practice to have one program/HTTP server running on the server (the machine, host, etc.) and managing all the HTTPS parts : sending the decrypted HTTP requests to the actual HTTP application running in the same server (the **FastAPI** application, in this case), take the HTTP response from the application, encrypt it using the appropriate certificate and sending it back to the client using HTTPS. This server is often called a TLS Termination Proxy. +It is a common practice to have **one program/HTTP server** running on the server (the machine, host, etc.) and **managing all the HTTPS parts**: receiving the **encrypted HTTPS requests**, sending the **decrypted HTTP requests** to the actual HTTP application running in the same server (the **FastAPI** application, in this case), take the **HTTP response** from the application, **encrypt it** using the appropriate **HTTPS certificate** and sending it back to the client using **HTTPS**. This server is often called a **TLS Termination Proxy**. + +Some of the options you could use as a TLS Termination Proxy are: + +* Traefik (that can also handle certificate renewals) +* Caddy (that can also handle certificate renewals) +* Nginx +* HAProxy ## Let's Encrypt -Before Let's Encrypt, these HTTPS certificates were sold by trusted third-parties. +Before Let's Encrypt, these **HTTPS certificates** were sold by trusted third parties. The process to acquire one of these certificates used to be cumbersome, require quite some paperwork and the certificates were quite expensive. -But then Let's Encrypt was created. +But then **Let's Encrypt** was created. -It is a project from the Linux Foundation. It provides HTTPS certificates for free. In an automated way. These certificates use all the standard cryptographic security, and are short lived (about 3 months), so the security is actually better because of their reduced lifespan. +It is a project from the Linux Foundation. It provides **HTTPS certificates for free**, in an automated way. These certificates use all the standard cryptographic security, and are short-lived (about 3 months), so the **security is actually better** because of their reduced lifespan. The domains are securely verified and the certificates are generated automatically. This also allows automating the renewal of these certificates. -The idea is to automate the acquisition and renewal of these certificates, so that you can have secure HTTPS, for free, forever. +The idea is to automate the acquisition and renewal of these certificates so that you can have **secure HTTPS, for free, forever**. + +## HTTPS for Developers + +Here's an example of how an HTTPS API could look like, step by step, paying attention mainly to the ideas important for developers. + +### Domain Name + +It would probably all start by you **acquiring** some **domain name**. Then, you would configure it in a DNS server (possibly your same cloud provider). + +You would probably get a cloud server (a virtual machine) or something similar, and it would have a fixed **public IP address**. + +In the DNS server(s) you would configure a record (an "`A record`") to point **your domain** to the public **IP address of your server**. + +You would probably do this just once, the first time, when setting everything up. + +!!! tip + This Domain Name part is way before HTTPS, but as everything depends on the domain and the IP address, it's worth mentioning it here. + +### DNS + +Now let's focus on all the actual HTTPS parts. + +First, the browser would check with the **DNS servers** what is the **IP for the domain**, in this case, `someapp.example.com`. + +The DNS servers would tell the browser to use some specific **IP address**. That would be the public IP address used by your server, that you configured in the DNS servers. + + + +### TLS Handshake Start + +The browser would then communicate with that IP address on **port 443** (the HTTPS port). + +The first part of the communication is just to establish the connection between the client and the server and to decide the cryptographic keys they will use, etc. + + + +This interaction between the client and the server to establish the TLS connection is called the **TLS handshake**. + +### TLS with SNI Extension + +**Only one process** in the server can be listening on a specific **port** in a specific **IP address**. There could be other processes listening on other ports in the same IP address, but only one for each combination of IP address and port. + +TLS (HTTPS) uses the specific port `443` by default. So that's the port we would need. + +As only one process can be listening on this port, the process that would do it would be the **TLS Termination Proxy**. + +The TLS Termination Proxy would have access to one or more **TLS certificates** (HTTPS certificates). + +Using the **SNI extension** discussed above, the TLS Termination Proxy would check which of the TLS (HTTPS) certificates available it should use for this connection, using the one that matches the domain expected by the client. + +In this case, it would use the certificate for `someapp.example.com`. + + + +The client already **trusts** the entity that generated that TLS certificate (in this case Let's Encrypt, but we'll see about that later), so it can **verify** that the certificate is valid. + +Then, using the certificate, the client and the TLS Termination Proxy **decide how to encrypt** the rest of the **TCP communication**. This completes the **TLS Handshake** part. + +After this, the client and the server have an **encrypted TCP connection**, this is what TLS provides. And then they can use that connection to start the actual **HTTP communication**. + +And that's what **HTTPS** is, it's just plain **HTTP** inside a **secure TLS connection** instead of a pure (unencrypted) TCP connection. + +!!! tip + Notice that the encryption of the communication happens at the **TCP level**, not at the HTTP level. + +### HTTPS Request + +Now that the client and server (specifically the browser and the TLS Termination Proxy) have an **encrypted TCP connection**, they can start the **HTTP communication**. + +So, the client sends an **HTTPS request**. This is just an HTTP request through an encrypted TLS connection. + + + +### Decrypt the Request + +The TLS Termination Proxy would use the encryption agreed to **decrypt the request**, and would transmit the **plain (decrypted) HTTP request** to the process running the application (for example a process with Uvicorn running the FastAPI application). + + + +### HTTP Response + +The application would process the request and send a **plain (unencrypted) HTTP response** to the TLS Termination Proxy. + + + +### HTTPS Response + +The TLS Termination Proxy would then **encrypt the response** using the cryptography agreed before (that started with the certificate for `someapp.example.com`), and send it back to the browser. + +Next, the browser would verify that the response is valid and encrypted with the right cryptographic key, etc. It would then **decrypt the response** and process it. + + + +The client (browser) will know that the response comes from the correct server because it is using the cryptography they agreed using the **HTTPS certificate** before. + +### Multiple Applications + +In the same server (or servers), there could be **multiple applications**, for example, other API programs or a database. + +Only one process can be handling the specific IP and port (the TLS Termination Proxy in our example) but the other applications/processes can be running on the server(s) too, as long as they don't try to use the same **combination of public IP and port**. + + + +That way, the TLS Termination Proxy could handle HTTPS and certificates for **multiple domains**, for multiple applications, and then transmit the requests to the right application in each case. + +### Certificate Renewal + +At some point in the future, each certificate would **expire** (about 3 months after acquiring it). + +And then, there would be another program (in some cases it's another program, in some cases it could be the same TLS Termination Proxy) that would talk to Let's Encrypt, and renew the certificate(s). + + + +The **TLS certificates** are **associated with a domain name**, not with an IP address. + +So, to renew the certificates, the renewal program needs to **prove** to the authority (Let's Encrypt) that it indeed **"owns" and controls that domain**. + +To do that, and to accommodate different application needs, there are several ways it can do it. Some popular ways are: + +* **Modify some DNS records**. + * For this, the renewal program needs to support the APIs of the DNS provider, so, depending on the DNS provider you are using, this might or might not be an option. +* **Run as a server** (at least during the certificate acquisition process) on the public IP address associated with the domain. + * As we said above, only one process can be listening on a specific IP and port. + * This is one of the reasons why it's very useful when the same TLS Termination Proxy also takes care of the certificate renewal process. + * Otherwise, you might have to stop the TLS Termination Proxy momentarily, start the renewal program to acquire the certificates, then configure them with the TLS Termination Proxy, and then restart the TLS Termination Proxy. This is not ideal, as your app(s) will not be available during the time that the TLS Termination Proxy is off. + +All this renewal process, while still serving the app, is one of the main reasons why you would want to have a **separate system to handle HTTPS** with a TLS Termination Proxy instead of just using the TLS certificates with the application server directly (e.g. Uvicorn). + +## Recap + +Having **HTTPS** is very important, and quite **critical** in most cases. Most of the effort you as a developer have to put around HTTPS is just about **understanding these concepts** and how they work. + +But once you know the basic information of **HTTPS for developers** you can easily combine and configure different tools to help you manage everything in a simple way. + +In some of the next chapters I'll show you several concrete examples of how to set up **HTTPS** for **FastAPI** applications. 🔒 diff --git a/docs/en/docs/img/deployment/https/https.drawio b/docs/en/docs/img/deployment/https/https.drawio new file mode 100644 index 000000000..31cfab96b --- /dev/null +++ b/docs/en/docs/img/deployment/https/https.drawio @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https.svg b/docs/en/docs/img/deployment/https/https.svg new file mode 100644 index 000000000..e63345eba --- /dev/null +++ b/docs/en/docs/img/deployment/https/https.svg @@ -0,0 +1,62 @@ +
Server(s)
Server(s)
https://someapp.example.com
https://someapp.example.com
DNS Servers
DNS Servers
TLS Termination Proxy
TLS Termination Proxy +
Cert Renovation Program
Cert Renovation Program
Let's Encrypt
Let's Encrypt
FastAPI app for: someapp.example.com
FastAPI app for: someapp.example.com
Another app: another.example.com
Another app: another.example.com
One more app: onemore.example.com
One more app: onemore.example.com
A Database
A Database
Plain response from: someapp.example.com
Plain response from: someapp.example.com
Port 443 (HTTPS)
Port 443 (HTTPS)
Encrypted request for: someapp.example.com
Encrypted request for: someapp.example.com
Who is: someapp.example.com
Who is: someapp.example.com
IP:
123.124.125.126
IP:...
Renew HTTPS cert for: someapp.example.com
Renew HTTPS cert for: someapp.example.com
New HTTPS cert for: someapp.example.com
New HTTPS cert for: someapp.example.com
TLS Handshake
TLS Handshake
Encrypted response from: someapp.example.com
Encrypted response from: someapp.example.com
HTTPS certificates
HTTPS certificates +
someapp.example.com
someapp.example.com +
another.example.net
another.example.net +
onemore.example.org
onemore.example.org +
IP:
123.124.125.126
IP:...
Decrypted request for: someapp.example.com
Decrypted request for: someapp.example.com
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https01.drawio b/docs/en/docs/img/deployment/https/https01.drawio new file mode 100644 index 000000000..9bc5340ce --- /dev/null +++ b/docs/en/docs/img/deployment/https/https01.drawio @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https01.svg b/docs/en/docs/img/deployment/https/https01.svg new file mode 100644 index 000000000..4fee0adfc --- /dev/null +++ b/docs/en/docs/img/deployment/https/https01.svg @@ -0,0 +1,57 @@ +
https://someapp.example.com
https://someapp.example.com
DNS Servers
DNS Servers
Who is: someapp.example.com
Who is: someapp.example.com
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https02.drawio b/docs/en/docs/img/deployment/https/https02.drawio new file mode 100644 index 000000000..0f7578d3e --- /dev/null +++ b/docs/en/docs/img/deployment/https/https02.drawio @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https02.svg b/docs/en/docs/img/deployment/https/https02.svg new file mode 100644 index 000000000..1f37a7098 --- /dev/null +++ b/docs/en/docs/img/deployment/https/https02.svg @@ -0,0 +1,57 @@ +
Server(s)
Server(s)
https://someapp.example.com
https://someapp.example.com
DNS Servers
DNS Servers
Port 443 (HTTPS)
Port 443 (HTTPS)
IP:
123.124.125.126
IP:...
Who is: someapp.example.com
Who is: someapp.example.com
IP:
123.124.125.126
IP:...
TLS Handshake
TLS Handshake
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https03.drawio b/docs/en/docs/img/deployment/https/https03.drawio new file mode 100644 index 000000000..c5766086c --- /dev/null +++ b/docs/en/docs/img/deployment/https/https03.drawio @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https03.svg b/docs/en/docs/img/deployment/https/https03.svg new file mode 100644 index 000000000..e68e1c459 --- /dev/null +++ b/docs/en/docs/img/deployment/https/https03.svg @@ -0,0 +1,62 @@ +
Server(s)
Server(s)
https://someapp.example.com
https://someapp.example.com
DNS Servers
DNS Servers
TLS Termination Proxy
TLS Termination Proxy +
Port 443 (HTTPS)
Port 443 (HTTPS)
Who is: someapp.example.com
Who is: someapp.example.com
IP:
123.124.125.126
IP:...
TLS Handshake
TLS Handshake
HTTPS certificates
HTTPS certificates +
someapp.example.com
someapp.example.com +
another.example.net
another.example.net +
onemore.example.org
onemore.example.org +
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https04.drawio b/docs/en/docs/img/deployment/https/https04.drawio new file mode 100644 index 000000000..ea357a6c1 --- /dev/null +++ b/docs/en/docs/img/deployment/https/https04.drawio @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https04.svg b/docs/en/docs/img/deployment/https/https04.svg new file mode 100644 index 000000000..4c9b7999b --- /dev/null +++ b/docs/en/docs/img/deployment/https/https04.svg @@ -0,0 +1,62 @@ +
Server(s)
Server(s)
https://someapp.example.com
https://someapp.example.com
DNS Servers
DNS Servers
TLS Termination Proxy
TLS Termination Proxy +
Port 443 (HTTPS)
Port 443 (HTTPS)
Encrypted request for: someapp.example.com
Encrypted request for: someapp.example.com
Who is: someapp.example.com
Who is: someapp.example.com
IP:
123.124.125.126
IP:...
TLS Handshake
TLS Handshake
HTTPS certificates
HTTPS certificates +
someapp.example.com
someapp.example.com +
another.example.net
another.example.net +
onemore.example.org
onemore.example.org +
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https05.drawio b/docs/en/docs/img/deployment/https/https05.drawio new file mode 100644 index 000000000..9b8b7c6f7 --- /dev/null +++ b/docs/en/docs/img/deployment/https/https05.drawio @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https05.svg b/docs/en/docs/img/deployment/https/https05.svg new file mode 100644 index 000000000..d11647b9b --- /dev/null +++ b/docs/en/docs/img/deployment/https/https05.svg @@ -0,0 +1,62 @@ +
Server(s)
Server(s)
https://someapp.example.com
https://someapp.example.com
DNS Servers
DNS Servers
TLS Termination Proxy
TLS Termination Proxy +
FastAPI app for: someapp.example.com
FastAPI app for: someapp.example.com
Decrypted request for: someapp.example.com
Decrypted request for: someapp.example.com
Port 443 (HTTPS)
Port 443 (HTTPS)
Encrypted request for: someapp.example.com
Encrypted request for: someapp.example.com
Who is: someapp.example.com
Who is: someapp.example.com
IP:
123.124.125.126
IP:...
TLS Handshake
TLS Handshake
HTTPS certificates
HTTPS certificates +
someapp.example.com
someapp.example.com +
another.example.net
another.example.net +
onemore.example.org
onemore.example.org +
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https06.drawio b/docs/en/docs/img/deployment/https/https06.drawio new file mode 100644 index 000000000..5bb85813f --- /dev/null +++ b/docs/en/docs/img/deployment/https/https06.drawio @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https06.svg b/docs/en/docs/img/deployment/https/https06.svg new file mode 100644 index 000000000..10e03b7c5 --- /dev/null +++ b/docs/en/docs/img/deployment/https/https06.svg @@ -0,0 +1,62 @@ +
Server(s)
Server(s)
https://someapp.example.com
https://someapp.example.com
DNS Servers
DNS Servers
TLS Termination Proxy
TLS Termination Proxy +
FastAPI app for: someapp.example.com
FastAPI app for: someapp.example.com
Plain response from: someapp.example.com
Plain response from: someapp.example.com
Decrypted request for: someapp.example.com
Decrypted request for: someapp.example.com
Port 443 (HTTPS)
Port 443 (HTTPS)
Encrypted request for: someapp.example.com
Encrypted request for: someapp.example.com
Who is: someapp.example.com
Who is: someapp.example.com
IP:
123.124.125.126
IP:...
TLS Handshake
TLS Handshake
HTTPS certificates
HTTPS certificates +
someapp.example.com
someapp.example.com +
another.example.net
another.example.net +
onemore.example.org
onemore.example.org +
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https07.drawio b/docs/en/docs/img/deployment/https/https07.drawio new file mode 100644 index 000000000..1ca994b22 --- /dev/null +++ b/docs/en/docs/img/deployment/https/https07.drawio @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https07.svg b/docs/en/docs/img/deployment/https/https07.svg new file mode 100644 index 000000000..e409d8871 --- /dev/null +++ b/docs/en/docs/img/deployment/https/https07.svg @@ -0,0 +1,62 @@ +
Server(s)
Server(s)
https://someapp.example.com
https://someapp.example.com
DNS Servers
DNS Servers
TLS Termination Proxy
TLS Termination Proxy +
FastAPI app for: someapp.example.com
FastAPI app for: someapp.example.com
Plain response from: someapp.example.com
Plain response from: someapp.example.com
Decrypted request for: someapp.example.com
Decrypted request for: someapp.example.com
Port 443 (HTTPS)
Port 443 (HTTPS)
Encrypted request for: someapp.example.com
Encrypted request for: someapp.example.com
Who is: someapp.example.com
Who is: someapp.example.com
IP:
123.124.125.126
IP:...
TLS Handshake
TLS Handshake
Encrypted response from: someapp.example.com
Encrypted response from: someapp.example.com
HTTPS certificates
HTTPS certificates +
someapp.example.com
someapp.example.com +
another.example.net
another.example.net +
onemore.example.org
onemore.example.org +
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https08.drawio b/docs/en/docs/img/deployment/https/https08.drawio new file mode 100644 index 000000000..8a4f41056 --- /dev/null +++ b/docs/en/docs/img/deployment/https/https08.drawio @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/en/docs/img/deployment/https/https08.svg b/docs/en/docs/img/deployment/https/https08.svg new file mode 100644 index 000000000..3047dd821 --- /dev/null +++ b/docs/en/docs/img/deployment/https/https08.svg @@ -0,0 +1,62 @@ +
Server(s)
Server(s)
https://someapp.example.com
https://someapp.example.com
DNS Servers
DNS Servers
TLS Termination Proxy
TLS Termination Proxy +
FastAPI app for: someapp.example.com
FastAPI app for: someapp.example.com
Another app: another.example.com
Another app: another.example.com
One more app: onemore.example.com
One more app: onemore.example.com
A Database
A Database
Plain response from: someapp.example.com
Plain response from: someapp.example.com
Decrypted request for: someapp.example.com
Decrypted request for: someapp.example.com
Port 443 (HTTPS)
Port 443 (HTTPS)
Encrypted request for: someapp.example.com
Encrypted request for: someapp.example.com
Who is: someapp.example.com
Who is: someapp.example.com
IP:
123.124.125.126
IP:...
TLS Handshake
TLS Handshake
Encrypted response from: someapp.example.com
Encrypted response from: someapp.example.com
HTTPS certificates
HTTPS certificates +
someapp.example.com
someapp.example.com +
another.example.net
another.example.net +
onemore.example.org
onemore.example.org +
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1
\ No newline at end of file From fe086a490396eff622a067468531c8776343217f Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 27 Sep 2021 14:41:20 +0000 Subject: [PATCH 12/38] =?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 a581cf394..f67b11f0f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Upgrade HTTPS guide with more explanations and diagrams. PR [#3950](https://github.com/tiangolo/fastapi/pull/3950) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add German translation for `docs/features.md`. PR [#3699](https://github.com/tiangolo/fastapi/pull/3699) by [@mawassk](https://github.com/mawassk). * 🎨 Tweak CSS styles for shell animations. PR [#3888](https://github.com/tiangolo/fastapi/pull/3888) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add new Sponsor Calmcode.io. PR [#3777](https://github.com/tiangolo/fastapi/pull/3777) by [@tiangolo](https://github.com/tiangolo). From cb7e79ab8a27b8f453ded2674adb92f8738d7cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 1 Oct 2021 22:44:19 +0200 Subject: [PATCH 13/38] =?UTF-8?q?=F0=9F=93=9D=20Re-write=20and=20extend=20?= =?UTF-8?q?Deployment=20guide:=20Concepts,=20Uvicorn,=20Gunicorn,=20Docker?= =?UTF-8?q?,=20Containers,=20Kubernetes=20(#3974)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/deployment/concepts.md | 311 +++++++++ docs/en/docs/deployment/deta.md | 18 + docs/en/docs/deployment/docker.md | 655 ++++++++++++++++-- docs/en/docs/deployment/index.md | 16 +- docs/en/docs/deployment/manually.md | 50 +- docs/en/docs/deployment/server-workers.md | 178 +++++ .../docs/img/deployment/concepts/image01.png | Bin 0 -> 124827 bytes .../deployment/concepts/process-ram.drawio | 106 +++ .../img/deployment/concepts/process-ram.svg | 59 ++ docs/en/mkdocs.yml | 6 +- 10 files changed, 1320 insertions(+), 79 deletions(-) create mode 100644 docs/en/docs/deployment/concepts.md create mode 100644 docs/en/docs/deployment/server-workers.md create mode 100644 docs/en/docs/img/deployment/concepts/image01.png create mode 100644 docs/en/docs/img/deployment/concepts/process-ram.drawio create mode 100644 docs/en/docs/img/deployment/concepts/process-ram.svg diff --git a/docs/en/docs/deployment/concepts.md b/docs/en/docs/deployment/concepts.md new file mode 100644 index 000000000..d22b53fe0 --- /dev/null +++ b/docs/en/docs/deployment/concepts.md @@ -0,0 +1,311 @@ +# Deployments Concepts + +When deploying a **FastAPI** application, or actually, any type of web API, there are several concepts that you probably care about, and using them you can find the **most appropriate** way to **deploy your application**. + +Some of the important concepts are: + +* Security - HTTPS +* Running on startup +* Restarts +* Replication (the number of processes running) +* Memory +* Previous steps before starting + +We'll see how they would affect **deployments**. + +In the end, the ultimate objective is to be able to **serve your API clients** in a way that is **secure**, to **avoid disruptions**, and to use the **compute resources** (for example remote servers/virtual machines) as efficiently as possible. 🚀 + +I'll tell you a bit more about these **concepts** here, and that would hopefully give you the **intuition** you would need to decide how to deploy your API in very different environments, possibly even in **future** ones that don't exist yet. + +By considering these concepts, you will be able to **evaluate and design** the best way to deploy **your own APIs**. + +In the next chapters, I'll give you more **concrete recipes** to deploy FastAPI applications. + +But for now, let's check these important **conceptual ideas**. These concepts also apply for any other type of web API. 💡 + +## Security - HTTPS + +In the [previous chapter about HTTPS](./https.md){.internal-link target=_blank} we learned about how HTTPS provides encryption for your API. + +We also saw that HTTPS is normally provided by a component **external** to your application server, a **TLS Termination Proxy**. + +And there has to be something in charge of **renewing the HTTPS certificates**, it could be the same component or it could be something different. + +### Example Tools for HTTPS + +Some of the tools you could use as a TLS Termination Proxy are: + +* Traefik + * Automatically handles certificates renewals ✨ +* Caddy + * Automatically handles certificates renewals ✨ +* Nginx + * With an external component like Certbot for certificate renewals +* HAProxy + * With an external component like Certbot for certificate renewals +* Kubernetes with an Ingress Controller like Nginx + * With an external component like cert-manager for certificate renewals +* Handled internally by a cloud provider as part of their services (read below 👇) + +Another option is that you could use a **cloud service** that does more of the work including setting up HTTPS. It could have some restrictions or charge you more, etc. But in that case you wouldn't have to set up a TLS Termination Proxy yourself. + +I'll show you some concrete examples in the next chapters. + +--- + +Then the next concepts to consider are all about the program running your actual API (e.g. Uvicorn). + +## Program and Process + +We will talk a lot about the running "**process**", so it's useful to have clarity about what it means, and what's the difference with the word "**program**". + +### What is a Program + +The word **program** is commonly used to describe many things: + +* The **code** that you write, the **Python files**. +* The **file** that can be **executed** by the operating system, for example `python`, `python.exe` or `uvicorn`. +* A particular program while it is **running** on the operating system, using the CPU, and storing things on memory. This is also called a **process**. + +### What is a Process + +The word **process** is normally used in a more specific way, only referring to the thing that is running in the operating system (like in the last point above): + +* A particular program while it is **running** on the operating system. + * This doesn't refer to the file, nor to the code, it refers **specifically** to the thing that is being **executed** and managed by the operating system. +* Any program, any code, **can only do things** when it is being **executed**. So, when there's a **process running**. +* The process can be **terminated** (or "killed") by you, or by the operating system. At that point, it stops running/being executed, and it can **no longer do things**. +* Each application that you have running in your computer has some process behind it, each running program, each window, etc. And there are normally many processes running **at the same time** while a computer is on. +* There can be **multiple processes** of the **same program** running at the same time. + +If you check out the "task manager" or "system monitor" (or similar tools) in your operating system, you will be able to see many of those processes running. + +And, for example, you will probably see that there are multiple processes running the same browser program (Firefox, Chrome, Edge, etc). They normally run one process per tab, plus some other extra processes. + + + +--- + +Now that we know the difference between the terms **process** and **program**, let's continue talking about deployments. + +## Running on Startup + +In most cases, when you create a web API, you want it to be **always running**, uninterrupted, so that your clients can always access it. This is of course, unless you have a specific reason why you want it to run only on certain situations, but most of the time you want it constantly running and **available**. + +### In a Remote Server + +When you set up a remote server (a cloud server, a virtual machine, etc.) the simplest thing you can do is to run Uvicorn (or similar) manually, the same way you do when developing locally. + +And it will work, and will be useful **during development**. + +But if your connection to the server is lost, the **running process** will probably die. + +And if the server is restarted (for example after updates, or migrations from the cloud provider) you probably **won't notice it**. And because of that, you won't even know that you have to restart the process manually. So, your API will just stay dead. 😱 + +### Run Automatically on Startup + +In general, you will probably want the server program (e.g. Uvicorn) to be started automatically on server startup, and without needing any **human intervention**, to have a process always running with your API (e.g. Uvicorn running your FastAPI app). + +### Separate Program + +To achieve this, you will normally have a **separate program** that would make sure your application is run on startup. And in many cases it would also make sure other components or applications are also run, for example a database. + +### Example Tools to Run at Startup + +Some examples of the tools that can do this job are: + +* Docker +* Kubernetes +* Docker Compose +* Docker in Swarm Mode +* Systemd +* Supervisor +* Handled internally by a cloud provider as part of their services +* Others... + +I'll give you more concrete examples in the next chapters. + +## Restarts + +Similar to making sure your application is run on startup, you probably also want to make sure it is **restarted** after failures. + +### We Make Mistakes + +We, as humans, make **mistakes**, all the time. Software almost *always* has **bugs** hidden in different places. 🐛 + +And we as developers keep improving the code as we find those bugs and as we implement new features (possibly adding new bugs too 😅). + +### Small Errors Automatically Handled + +When building web APIs with FastAPI, if there's an error in our code, FastAPI will normally contain it to the single request that triggered the error. 🛡 + +The client will get a **500 Internal Server Error** for that request, but the application will continue working for the next requests instead of just crashing completely. + +### Bigger Errors - Crashes + +Nevertheless, there might be cases where we write some code that **crashes the entire application** making Uvicorn and Python crash. 💥 + +And still, you would probably not want the application to stay dead because there was an error in one place, you probably want it to **continue running** at least for the *path operations* that are not broken. + +### Restart After Crash + +But in those cases with really bad errors that crash the running **process**, you would want an external component that is in charge of **restarting** the process, at least a couple of times... + +!!! tip + ...Although if the whole application is just **crashing immediately** it probably doesn't make sense to keep restarting it forever. But in those cases, you will probably notice it during development, or at least right after deployment. + + So let's focus on the main cases, where it could crash entirely in some particular cases **in the future**, and it still makes sense to restart it. + +You would probably want to have the thing in charge of restarting your application as an **external component**, because by that point, the same application with Uvicorn and Python already crashed, so there's nothing in the same code of the same app that could do anything about it. + +### Example Tools to Restart Automatically + +In most cases, the same tool that is used to **run the program on startup** is also used to handle automatic **restarts**. + +For example, this could be handled by: + +* Docker +* Kubernetes +* Docker Compose +* Docker in Swarm Mode +* Systemd +* Supervisor +* Handled internally by a cloud provider as part of their services +* Others... + +## Replication - Processes and Memory + +With a FastAPI application, using a server program like Uvicorn, running it once in **one process** can serve multiple clients concurrently. + +But in many cases you will want to run several worker processes at the same time. + +### Multiple Processes - Workers + +If you have more clients than what a single process can handle (for example if the virtual machine is not too big) and you have **multiple cores** in the server's CPU, then you could have **multiple processes** running with the same application at the same time, and distribute all the requests among them. + +When you run **multiple processes** of the same API program, they are commonly called **workers**. + +### Worker Processes and Ports + +Remember from the docs [About HTTPS](./https.md){.internal-link target=_blank} that only one process can be listening on one combination of port and IP address in a server? + +This is still true. + +So, to be able to have **multiple processes** at the same time, there has to be a **single process listening on a port** that then transmits the communication to each worker process in some way. + +### Memory per Process + +Now, when the program loads things in memory, for example, a machine learning model in a variable, or the contents of a large file in a variable, all that **consumes a bit of the memory (RAM)** of the server. + +And multiple processes normally **don't share any memory**. This means that each running process has its own things, its own variables, its own memory. And if you are consuming a large amount of memory in your code, **each process** will consume an equivalent amount of memory. + +### Server Memory + +For example, if your code loads a Machine Learning model with **1 GB in size**, when you run one process with your API, it will consume at least 1 GB or RAM. And if you start **4 processes** (4 workers), each will consume 1 GB of RAM. So, in total your API will consume **4 GB of RAM**. + +And if your remote server or virtual machine only has 3 GB of RAM, trying to load more than 4 GB of RAM will cause problems. 🚨 + +### Multiple Processes - An Example + +In this example, there's a **Manager Process** that starts and controls two **Worker Processes**. + +This Manager Process would probably be the one listening on the **port** in the IP. And it would transmit all the communication to the worker processes. + +Those worker processes would be the ones running your application, they would perform the main computations to receive a **request** and return a **response**, and they would load anything you put in variables in RAM. + + + +And of course, the same machine would probably have **other processes** running as well, apart from your application. + +An interesting detail is that the percentage of the **CPU used** by each process can **vary** a lot over time, but the **memory (RAM)** normally stays more or less **stable**. + +If you have an API that does a comparable amount of computations every time and you have a lot of clients, then the **CPU utilization** will probably *also be stable* (instead of constantly going up and down quickly). + +### Examples of Replication Tools and Strategies + +There can be several approaches to achieve this, and I'll tell you more about specific strategies in the next chapters, for example when talking about Docker and containers. + +The main constraint to consider is that there has to be a **single** component handling the **port** in the **public IP**. And then it has to have a way to **transmit** the communication to the replicated **processes/workers**. + +Here are some possible combinations and strategies: + +* **Gunicorn** managing **Uvicorn workers** + * Gunicorn would be the **process manager** listening on the **IP** and **port**, the replication would be by having **multiple Uvicorn worker processes** +* **Uvicorn** managing **Uvicorn workers** + * One Uvicorn **process manager** would listen on the **IP** and **port**, and it would start **multiple Uvicorn worker processes** +* **Kubernetes** and other distributed **container systems** + * Something in the **Kubernetes** layer would listen on the **IP** and **port**. The replication would be by having **multiple containers**, each with **one Uvicorn process** running +* **Cloud services** that handle this for your + * The cloud service will probably **handle replication for you**. It would possibly let you define **a process to run**, or a **container image** to use, in any case, it would most probably be **a single Uvicorn process**, and the cloud service would be in charge of replicating it. + +!!! tip + Don't worry if some of these items about **containers**, Docker, or Kubernetes don't make a lot of sense yet. + + I'll tell you more about container images, Docker, Kubernetes, etc. in a future chapter: [FastAPI in Containers - Docker](./docker.md){.internal-link target=_blank}. + +## Previous Steps Before Starting + +There are many cases where you want to perform some steps **before starting** your application. + +For example, you might want to run **database migrations**. + +But in most cases, you will want to perform these steps only **once**. + +So, you will want to have a **single process** to perform those **previous steps**, before starting the application. + +And you will have to make sure that it's a single process running those previous steps *even* if afterwards you start **multiple processes** (multiple workers) for the application itself. If those steps were run by **multiple processes**, they would **duplicate** the work by running it on **parallel**, and if the steps were something delicate like a database migration, they could cause conflicts with each other. + +Of course, there are some cases where there's no problem in running the previous steps multiple times, in that case it's a lot easier to handle. + +!!! tip + Also have in mind that depending on your setup, in some cases you **might not even need any previous steps** before starting your application. + + In that case, you wouldn't have to worry about any of this. 🤷 + +### Examples of Previous Steps Strategies + +This will **depend heavily** on the way you **deploy your system**, and it would probably be connected to the way you start programs, handling restarts, etc. + +Here are some possible ideas: + +* An "Init Container" in Kubernetes that runs before your app container +* A bash script that runs the previous steps and then starts your application + * You would still need a way to start/restart *that* bash script, detect errors, etc. + +!!! tip + I'll give you more concrete examples for doing this with containers in a future chapter: [FastAPI in Containers - Docker](./docker.md){.internal-link target=_blank}. + +## Resource Utilization + +Your server(s) is (are) a **resource**, you can consume or **utilize**, with your programs, the computation time on the CPUs, and the RAM memory available. + +How much resources do you want to be consuming/utilizing? It might be easy to think "not much", but in reality, you will probably want to consume **as much as possible without crashing**. + +If you are paying for 3 servers but you are using only a little bit of their RAM and CPU, you are probably **wasting money** 💸, and probably **wasting server electric power** 🌎, etc. + +In that case, it could be better to have only 2 servers and use a higher percentage of their resources (CPU, memory, disk, network bandwidth, etc). + +On the other hand, if you have 2 servers and you are using **100% of their CPU and RAM**, at some point one process will ask for more memory, and the server will have to use the disk as "memory" (which can be thousands of times slower), or even **crash**. Or one process might need to do some computation and would have to wait until the CPU is free again. + +In this case, it would be better to get **one extra server** and run some processes on it so that they all have **enough RAM and CPU time**. + +There's also the chance that for some reason you have a **spike** of usage of your API. Maybe it went viral, or maybe some other services or bots start using it. And you might want to have extra resources to be safe in those cases. + +You could put an **arbitrary number** to target, for example something **between 50% to 90%** of resource utilization. The point is that those are probably the main things you will want to measure and use to tweak your deployments. + +You can use simple tools like `htop` to see the CPU and RAM used in your server, or the amount used by each process. Or you can use more complex monitoring tools, maybe distributed across servers, etc. + +## Recap + +You have been reading here some of the main concepts that you would probably need to have in mind when deciding how to deploy your application: + +* Security - HTTPS +* Running on startup +* Restarts +* Replication (the number of processes running) +* Memory +* Previous steps before starting + +Understanding these ideas and how to apply them should give you the intuition necessary to take any decisions when configuring and tweaking your deployments. 🤓 + +In the next sections I'll give you more concrete examples of possible strategies you can follow. 🚀 diff --git a/docs/en/docs/deployment/deta.md b/docs/en/docs/deployment/deta.md index fd4b30a3c..b0d1cb928 100644 --- a/docs/en/docs/deployment/deta.md +++ b/docs/en/docs/deployment/deta.md @@ -238,3 +238,21 @@ You can also edit them and re-play them. At some point you will probably want to store some data for your app in a way that persists through time. For that you can use Deta Base, it also has a generous **free tier**. You can also read more in the Deta Docs. + +## Deployment Concepts + +Coming back to the concepts we discussed in [Deployments Concepts](./concepts.md){.internal-link target=_blank}, here's how each of them would be handled with Deta: + +* **HTTPS**: Handled by Deta, they will give you a subdomain and handle HTTPS automatically. +* **Running on startup**: Handled by Deta, as part of their service. +* **Restarts**: Handled by Deta, as part of their service. +* **Replication**: Handled by Deta, as part of their service. +* **Memory**: Limit predefined by Deta, you could contact them to increase it. +* **Previous steps before starting**: Not directly supported, you could make it work with their Cron system or additional scripts. + +!!! note + Deta is designed to make it easy (and free) to deploy simple applications quickly. + + It can simplify a lot several use cases, but at the same time it doesn't support others, like using external databases (apart from Deta's own NoSQL database system), custom virtual machines, etc. + + You can read more details in the Deta docs to see if it's the right choice for you. diff --git a/docs/en/docs/deployment/docker.md b/docs/en/docs/deployment/docker.md index e32a2969d..e25401f20 100644 --- a/docs/en/docs/deployment/docker.md +++ b/docs/en/docs/deployment/docker.md @@ -1,67 +1,144 @@ -# Deploy with Docker +# FastAPI in Containers - Docker -In this section you'll see instructions and links to guides to know how to: +When deploying FastAPI applications a common approach is to build a **Linux container image**. It's normally done using **Docker**. And then you can deploy that container image in one of different possible ways. -* Make your **FastAPI** application a Docker image/container with maximum performance. In about **5 min**. -* (Optionally) understand what you, as a developer, need to know about HTTPS. -* Set up a Docker Swarm mode cluster with automatic HTTPS, even on a simple $5 USD/month server. In about **20 min**. -* Generate and deploy a full **FastAPI** application, using your Docker Swarm cluster, with HTTPS, etc. In about **10 min**. - -You can use **Docker** for deployment. It has several advantages like security, replicability, development simplicity, etc. - -If you are using Docker, you can use the official Docker image: - -## tiangolo/uvicorn-gunicorn-fastapi - -This image has an "auto-tuning" mechanism included, so that you can just add your code and get very high performance automatically. And without making sacrifices. - -But you can still change and update all the configurations with environment variables or configuration files. +Using Linux containers has several advantages including **security**, **replicability**, **simplicity**, and others. !!! tip - To see all the configurations and options, go to the Docker image page: tiangolo/uvicorn-gunicorn-fastapi. + In a hurry and already know this stuff? Jump to the [`Dockerfile` below 👇](#build-a-docker-image-for-fastapi). -## Create a `Dockerfile` - -* Go to your project directory. -* Create a `Dockerfile` with: +
+Dockerfile Preview 👀 ```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 +FROM python:3.9 -COPY ./app /app -``` +WORKDIR /code -### Bigger Applications +COPY ./requirements.txt /code/requirements.txt -If you followed the section about creating [Bigger Applications with Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}, your `Dockerfile` might instead look like: +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 - -COPY ./app /app/app -``` - -### Raspberry Pi and other architectures - -If you are running Docker in a Raspberry Pi (that has an ARM processor) or any other architecture, you can create a `Dockerfile` from scratch, based on a Python base image (that is multi-architecture) and use Uvicorn alone. - -In this case, your `Dockerfile` could look like: - -```Dockerfile -FROM python:3.7 - -RUN pip install fastapi uvicorn - -EXPOSE 80 - -COPY ./app /app +COPY ./app /code/app CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] + +# If running behind a proxy like Nginx or Traefik add --proxy-headers +# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"] ``` -## Create the **FastAPI** Code +
+ +## What is a Container + +Containers (mainly Linux containers) are a very **lightweight** way to package applications including all their dependencies and necessary files while keeping them isolated from other containers (other applications or components) in the same system. + +Linux containers run using the same Linux kernel of the host (machine, virtual machine, cloud server, etc). This just means that they are very lightweight (compared to full virtual machines emulating an entire operating system). + +This way, containers consume **little resources**, an amount comparable to running the processes directly (a virtual machine would consume much more). + +Containers also have their own **isolated** running processes (commonly just one process), file system, and network, simplifying deployment, security, development, etc. + +## What is a Container Image + +A **container** is run from a **container image**. + +A container image is a **static** version of all the files, environment variables, and the default command/program that should be present in a container. **Static** here means that the container **image** is not running, it's not being executed, it's only the packaged files and metadata. + +In contrast to a "**container image**" that is the stored static contents, a "**container**" normally refers to the running instance, the thing that is being **executed**. + +When the **container** is started and running (started from a **container image**) it could create or change files, environment variables, etc. Those changes will exist only in that container, but would not persist in the underlying container image (would not be saved to disk). + +A container image is comparable to the **program** file and contents, e.g. `python` and some file `main.py`. + +And the **container** itself (in contrast to the **container image**) is the actual running instance of the image, comparable to a **process**. In fact, a container is running only when it has a **process running** (and normally it's only a single process). The container stops when there's no process running in it. + +## Container Images + +Docker has been one of the main tools to create and manage **container images** and **containers**. + +And there's a public Docker Hub with pre-made **official container images** for many tools, environments, databases, and applications. + +For example, there's an official Python Image. + +And there are many other images for different things like databases, for example for: + +* PostgreSQL +* MySQL +* MongoDB +* Redis, etc. + +By using a pre-made container image it's very easy to **combine** and use different tools. For example, to try out a new database. In most cases you can use the **official images**, and just configure them with environment variables. + +That way, in many cases you can learn about containers and Docker and re-use that knowledge with many different tools and components. + +So, you would run **multiple containers** with different things, like a database, a Python application, a web server with a React frontend application, and connect them together via their internal network. + +All the container management systems (like Docker or Kubernetes) have these networking features integrated in them. + +## Containers and Processes + +A **container image** normally includes in its metadata the default program or command that should be run when the **container** is started and the parameters to be passed to that program. Very similar to what would be if it was in the command line. + +When a **container** is started, it will run that command/program (although you can override it and make it run a different command/program). + +A container is running as long as the **main process** (command or program) is running. + +A container normally has a **single process**, but it's also possible to start subprocesses from the main process, and that way have **multiple processes** in the same container. + +But it's not possible to have a running container without **at least one running process**. If the main process stops, the container stops. + +## Build a Docker Image for FastAPI + +Okay, let's build something now! 🚀 + +I'll show you how to build a **Docker image** for FastAPI **from scratch**, based on the **official Python** image. + +This is what you would want to do in **most cases**, for example: + +* Using **Kubernetes** or similar tools +* When running on a **Raspberry Pi** +* Using a cloud service that would run a container image for you, etc. + +### Package Requirements + +You would normally have the **package requirements** for your application in some file. + +It would depend mainly on the tool you use to **install** those requirements. + +The most common way to do it is to have a file `requirements.txt` with the package names and their versions, one per line. + +You would of course use the same ideas you read in [About FastAPI versions](./versions.md){.internal-link target=_blank} to set the ranges of versions. + +For example, your `requirements.txt` could look like: + +``` +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +``` + +And you would normally install those package dependencies with `pip`, for example: + +
+ +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic uvicorn +``` + +
+ +!!! info + There are other formats and tools to define and install package dependencies. + + I'll show you an example using Poetry later in a section below. 👇 + +### Create the **FastAPI** Code * Create an `app` directory and enter in it. +* Create an empty file `__init__.py`. * Create a `main.py` file with: ```Python @@ -82,16 +159,126 @@ def read_item(item_id: int, q: Optional[str] = None): return {"item_id": item_id, "q": q} ``` -* You should now have a directory structure like: +### Dockerfile + +Now in the same project directory create a file `Dockerfile` with: + +```{ .dockerfile .annotate } +# (1) +FROM python:3.9 + +# (2) +WORKDIR /code + +# (3) +COPY ./requirements.txt /code/requirements.txt + +# (4) +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (5) +COPY ./app /code/app + +# (6) +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +``` + +1. Start from the official Python base image. + +2. Set the current working directory to `/code`. + + This is where we'll put the `requirements.txt` file and the `app` directory. + +3. Copy the file with the requirements to the `/code` directory. + + Copy **only** the file with the requirements first, not the rest of the code. + + As this file **doesn't change often**, Docker will detect it and use the **cache** for this step, enabling the cache for the next step too. + +4. Install the package dependencies in the requirements file. + + The `--no-cache-dir` option tells `pip` to not save the downloaded packages locally, as that is only if `pip` was going to be run again to install the same packages, but that's not the case when working with containers. + + !!! note + The `--no-cache-dir` is only related to `pip`, it has nothing to do with Docker or containers. + + The `--upgrade` option tells `pip` to upgrade the packages if they are already installed. + + Because the previous step copying the file could be detected by the **Docker cache**, this step will also **use the Docker cache** when available. + + Using the cache in this step will **save** you a lot of **time** when building the image again and again during development, instead of **downloading and installing** all the dependencies **every time**. + +5. Copy the `./app` directory inside the `/code` directory. + + As this has all the code which is what **changes most frequently** the Docker **cache** won't be used for this or any **following steps** easily. + + So, it's important to put this **near the end** of the `Dockerfile`, to optimize the container image build times. + +6. Set the **command** to run the `uvicorn` server. + + `CMD` takes a list of strings, each of this strings is what you would type in the command line separated by spaces. + + This command will be run from the **current working directory**, the same `/code` directory you set above with `WORKDIR /code`. + + Because the program will be started at `/code` and inside of it is the directory `./app` with your code, **Uvicorn** will be able to see and **import** `app` from `app.main`. + +!!! tip + Review what each line does by clicking each number bubble in the code. 👆 + +You should now have a directory structure like: ``` . ├── app +│   ├── __init__.py │ └── main.py -└── Dockerfile +├── Dockerfile +└── requirements.txt ``` -## Build the Docker image +#### Behind a TLS Termination Proxy + +If you are running your container behind a TLS Termination Proxy (load balancer) like Nginx or Traefik, add the option `--proxy-headers`, this will tell Uvicorn to trust the headers sent by that proxy telling it that the application is running behind HTTPS, etc. + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +#### Docker Cache + +There's an important trick in this `Dockerfile`, we first copy the **file with the dependencies alone**, not the rest of the code. Let me tell you why is that. + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +Docker and other tools **build** these container images **incrementally**, adding **one layer on top of the other**, starting from the top of the `Dockerfile` and adding any files created by each of the instructions of the `Dockerfile`. + +Docker and similar tools also use an **internal cache** when building the image, if a file hasn't changed since the last time building the container image, then it will **re-use the same layer** created the last time, instead of copying the file again and creating a new layer from scratch. + +Just avoiding the copy of files doesn't necessarily improve things too much, but because it used the cache for that step, it can **use the cache for the next step**. For example, it could use the cache for the instruction that installs dependencies with: + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +The file with the package requirements **won't change frequently**. So, by copying only that file, Docker will be able to **use the cache** for that step. + +And then, Docker will be able to **use the cache for the next step** that downloads and install those dependencies. And here's where we **save a lot of time**. ✨ ...and avoid boredom waiting. 😪😆 + +Downloading and installing the package dependencies **could take minutes**, but using the **cache** would **take seconds** at most. + +And as you would be building the container image again and again during development to check that your code changes are working, there's a lot of accumulated time this would save. + +Then, near the end of the `Dockerfile`, we copy all the code. As this is what **changes most frequently**, we put it near the end, because almost always, anything after this step will not be able to use the cache. + +```Dockerfile +COPY ./app /code/app +``` + +### Build the Docker Image + +Now that all the files are in place, let's build the container image. * Go to the project directory (in where your `Dockerfile` is, containing your `app` directory). * Build your FastAPI image: @@ -106,7 +293,12 @@ $ docker build -t myimage . -## Start the Docker container +!!! tip + Notice the `.` at the end, it's equivalent to `./`, it tells Docker the directory to use to build the container image. + + In this case, it's the same current directory (`.`). + +### Start the Docker Container * Run a container based on your image: @@ -118,8 +310,6 @@ $ docker run -d --name mycontainer -p 80:80 myimage -Now you have an optimized FastAPI server in a Docker container. Auto-tuned for your current server (and number of CPU cores). - ## Check it You should be able to check it in your Docker container's URL, for example: http://192.168.99.100/items/5?q=somequery or http://127.0.0.1/items/5?q=somequery (or equivalent, using your Docker host). @@ -146,34 +336,363 @@ You will see the alternative automatic documentation (provided by Traefik is a high performance reverse proxy / load balancer. It can do the "TLS Termination Proxy" job (apart from other features). +If your FastAPI is a single file, for example `main.py` without an `./app` directory, your file structure could look like: -It has integration with Let's Encrypt. So, it can handle all the HTTPS parts, including certificate acquisition and renewal. +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` -It also has integrations with Docker. So, you can declare your domains in each application configurations and have it read those configurations, generate the HTTPS certificates and serve HTTPS to your application automatically, without requiring any change in its configuration. +Then you would just have to change the corresponding paths to copy the file inside the `Dockerfile`: + +```{ .dockerfile .annotate hl_lines="10 13" } +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (1) +COPY ./main.py /code/ + +# (2) +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] +``` + +1. Copy the `main.py` file to the `/code` directory directly (without any `./app` directory). + +2. Run Uvicorn and tell it to import the `app` object from `main` (instead of importing from `app.main`). + +Then adjust the Uvicorn command to use the new module `main` instead of `app.main` to import the FastAPI object `app`. + +## Deployment Concepts + +Let's talk again about some of the same [Deployment Concepts](./concepts.md){.internal-link target=_blank} in terms of containers. + +Containers are mainly a tool to simplify the process of **building and deploying** an application, but they don't enforce a particular approach to handle these **deployment concepts**, and there are several possible strategies. + +The **good news** is that with each different strategy there's a way to cover all of the deployment concepts. 🎉 + +Let's review these **deployment concepts** in terms of containers: + +* HTTPS +* Running on startup +* Restarts +* Replication (the number of processes running) +* Memory +* Previous steps before starting + +## HTTPS + +If we focus just on the **container image** for a FastAPI application (and later the running **container**), HTTPS normally would be handled **externally** by another tool. + +It could be another container, for example with Traefik, handling **HTTPS** and **automatic** acquisition of **certificates**. + +!!! tip + Traefik has integrations with Docker, Kubernetes, and others, so it's very easy to set up and configure HTTPS for your containers with it. + +Alternatively, HTTPS could be handled by a cloud provider as one of their services (while still running the application in a container). + +## Running on Startup and Restarts + +There is normally another tool in charge of **starting and running** your container. + +It could be **Docker** directly, **Docker Compose**, **Kubernetes**, a **cloud service**, etc. + +In most (or all) cases, there's a simple option to enable running the container on startup and enabling restarts on failures. For example, in Docker, it's the command line option `--restart`. + +Without using containers, making applications run on startup and with restarts can be cumbersome and difficult. But when **working with containers** in most cases that functionality is included by default. ✨ + +## Replication - Number of Processes + +If you have a cluster of machines with **Kubernetes**, Docker Swarm Mode, Nomad, or other similar complex system to manage distributed containers on multiple machines, then you will probably want to **handle replication** at the **cluster level** instead of using a **process manager** (like Gunicorn with workers) in each container. + +One of those distributed container management systems like Kubernetes normally has some integrated way of handling **replication of containers** while still supporting **load balancing** for the incoming requests. All at the **cluster level**. + +In those cases, you would probably want to build a **Docker image from scratch** as [explained above](#dockerfile), installing your dependencies, and running **a single Uvicorn process** instead of running something like Gunicorn with Uvicorn workers. + +### Load Balancer + +When using containers, you would normally have some component **listening on the main port**. It could possibly be another container that is also a **TLS Termination Proxy** to handle **HTTPS** or some similar tool. + +As this component would take the **load** of requests and distribute that among the workers in a (hopefully) **balanced** way, it is also commonly called a **Load Balancer**. + +!!! tip + The same **TLS Termination Proxy** component used for HTTPS would probably also be a **Load Balancer**. + +And when working with containers, the same system you use to start and manage them would already have internal tools to transmit the **network communication** (e.g. HTTP requests) from that **load balancer** (that could also be a **TLS Termination Proxy**) to the container(s) with your app. + +### One Load Balancer - Multiple Worker Containers + +When working with **Kubernetes** or similar distributed container management systems, using their internal networking mechanisms would allow the single **load balancer** that is listening on the main **port** to transmit communication (requests) to possibly **multiple containers** running your app. + +Each of these containers running your app would normally have **just one process** (e.g. a Uvicorn process running your FastAPI application). They would all be **identical containers**, running the same thing, but each with its own process, memory, etc. That way you would take advantage of **parallelization** in **different cores** of the CPU, or even in **different machines**. + +And the distributed container system with the **load balancer** would **distribute the requests** to each one of the containers with your app **in turns**. So, each request could be handled by one of the multiple **replicated containers** running your app. + +And normally this **load balancer** would be able to handle requests that go to *other* apps in your cluster (e.g. to a different domain, or under a different URL path prefix), and would transmit that communication to the right containers for *that other* application running in your cluster. + +### One Process per Container + +In this type of scenario, you probably would want to have **a single (Uvicorn) process per container**, as you would already be handling replication at the cluster level. + +So, in this case, you **would not** want to have a process manager like Gunicorn with Uvicorn workers, or Uvicorn using its own Uvicorn workers. You would want to have just a **single Uvicorn process** per container (but probably multiple containers). + +Having another process manager inside the container (as would be with Gunicorn or Uvicorn managing Uvicorn workers) would only add **unnecessary complexity** that you are most probably already taking care of with your cluster system. + +### Containers with Multiple Processes and Special Cases + +Of course, there are **special cases** where you could want to have **a container** with a **Gunicorn process manager** starting several **Uvicorn worker processes** inside. + +In those cases, you can use the **official Docker image** that includes **Gunicorn** as a process manager running multiple **Uvicorn worker processes**, and some default settings to adjust the number of workers based on the current CPU cores automatically. I'll tell you more about it below in [Official Docker Image with Gunicorn - Uvicorn](#official-docker-image-with-gunicorn-uvicorn). + +Here are some examples of when that could make sense: + +#### A Simple App + +You could want a process manager in the container if your application is **simple enough** that you don't need (at least not yet) to fine-tune the number of processes too much, and you can just use an automated default (with the official Docker image), and you are running it on a **single server**, not a cluster. + +#### Docker Compose + +You could be deploying to a **single server** (not a cluster) with **Docker Compose**, so you wouldn't have an easy way to manage replication of containers (with Docker Compose) while preserving the shared network and **load balancing**. + +Then you could want to have **a single container** with a **process manager** starting **several worker processes** inside. + +#### Prometheus and Other Reasons + +You could also have **other reasons** that would make it easier to have a **single container** with **multiple processes** instead of having **multiple containers** with **a single process** in each of them. + +For example (depending on your setup) you could have some tool like a Prometheus exporter in the same container that should have access to **each of the requests** that come. + +In this case, if you had **multiple containers**, by default, when Prometheus came to **read the metrics**, it would get the ones for **a single container each time** (for the container that handled that particular request), instead of getting the **accumulated metrics** for all the replicated containers. + +Then, in that case, it could be simpler to have **one container** with **multiple processes**, and a local tool (e.g. a Prometheus exporter) on the same container collecting Prometheus metrics for all the internal processes and exposing those metrics on that single container. --- -With this information and tools, continue with the next section to combine everything. +The main point is, **none** of these are **rules written in stone** that you have to blindly follow. You can use these ideas to **evaluate your own use case** and decide what is the best approach for your system, checking out how to manage the concepts of: -## Docker Swarm mode cluster with Traefik and HTTPS +* Security - HTTPS +* Running on startup +* Restarts +* Replication (the number of processes running) +* Memory +* Previous steps before starting -You can have a Docker Swarm mode cluster set up in minutes (about 20 min) with a main Traefik handling HTTPS (including certificate acquisition and renewal). +## Memory -By using Docker Swarm mode, you can start with a "cluster" of a single machine (it can even be a $5 USD / month server) and then you can grow as much as you need adding more servers. +If you run **a single process per container** you will have a more or less well defined, stable, and limited amount of memory consumed by each of of those containers (more than one if they are replicated). -To set up a Docker Swarm Mode cluster with Traefik and HTTPS handling, follow this guide: +And then you can set those same memory limits and requirements in your configurations for your container management system (for example in **Kubernetes**). That way it will be able to **replicate the containers** in the **available machines** taking into account the amount of memory needed by them, and the amount available in the machines in the cluster. -### Docker Swarm Mode and Traefik for an HTTPS cluster +If your application is **simple**, this will probably **not be a problem**, and you might not need to specify hard memory limits. But if you are **using a lot of memory** (for example with **machine learning** models), you should check how much memory you are consuming and adjust the **number of containers** that runs in **each machine** (and maybe add more machines to your cluster). -### Deploy a FastAPI application +If you run **multiple processes per container** (for example with the official Docker image) you will have to make sure that the number of processes started doesn't **consume more memory** than what is available. -The easiest way to set everything up, would be using the [**FastAPI** Project Generators](../project-generation.md){.internal-link target=_blank}. +## Previous Steps Before Starting and Containers -It is designed to be integrated with this Docker Swarm cluster with Traefik and HTTPS described above. +If you are using containers (e.g. Docker, Kubernetes), then there are two main approaches you can use. -You can generate a project in about 2 min. +### Multiple Containers -The generated project has instructions to deploy it, doing it takes another 2 min. +If you have **multiple containers**, probably each one running a **single process** (for example, in a **Kubernetes** cluster), then you would probably want to have a **separate container** doing the work of the **previous steps** in a single container, running a single process, **before** running the replicated worker containers. + +!!! info + If you are using Kubernetes, this would probably be an Init Container. + +If in your use case there's no problem in running those previous steps **multiple times in parallel** (for example if you are not running database migrations, but just checking if the database is ready yet), then you could also just put them in each container right before starting the main process. + +### Single Container + +If you have a simple setup, with a **single container** that then starts multiple **worker processes** (or also just one process), then you could run those previous steps in the same container, right before starting the process with the app. The official Docker image supports this internally. + +## Official Docker Image with Gunicorn - Uvicorn + +There is an official Docker image that includes Gunicorn running with Uvicorn workers, as detailed in a previous chapter: [Server Workers - Gunicorn with Uvicorn](./server-workers.md){.internal-link target=_blank}. + +This image would be useful mainly in the situations described above in: [Containers with Multiple Processes and Special Cases](#containers-with-multiple-processes-and-special-cases). + +* tiangolo/uvicorn-gunicorn-fastapi. + +!!! warning + There's a high chance that you **don't** need this base image or any other similar one, and would be better off by building the image from scratch as [described above in: Build a Docker Image for FastAPI](#build-a-docker-image-for-fastapi). + +This image has an **auto-tuning** mechanism included to set the **number of worker processes** based on the CPU cores available. + +It has **sensible defaults**, but you can still change and update all the configurations with **environment variables** or configuration files. + +It also supports running **previous steps before starting** with a script. + +!!! tip + To see all the configurations and options, go to the Docker image page: tiangolo/uvicorn-gunicorn-fastapi. + +### Number of Processes on the Official Docker Image + +The **number of processes** on this image is **computed automatically** from the CPU **cores** available. + +This means that it will try to **squeeze** as much **performance** from the CPU as possible. + +You can also adjust it with the configurations using **environment variables**, etc. + +But it also means that as the number of processes depends on the CPU the container is running, the **amount of memory consumed** will also depend on that. + +So, if your application consumes a lot of memory (for example with machine learning models), and your server has a lot of CPU cores **but little memory**, then your container could end up trying to use more memory than what is available, and degrading performance a lot (or even crashing). 🚨 + +### Create a `Dockerfile` + +Here's how you would create a `Dockerfile` based on this image: + +```Dockerfile +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 + +COPY ./requirements.txt /app/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +COPY ./app /app +``` + +### Bigger Applications + +If you followed the section about creating [Bigger Applications with Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}, your `Dockerfile` might instead look like: + +```Dockerfile hl_lines="7" +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 + +COPY ./requirements.txt /app/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +COPY ./app /app/app +``` + +### When to Use + +You should probably **not** use this official base image (or any other similar one) if you are using **Kubernetes** (or others) and you are already setting **replication** at the cluster level, with multiple **containers**. In those cases, you are better off **building an image from scratch** as described above: [Build a Docker Image for FastAPI](#build-a-docker-image-for-fastapi). + +This image would be useful mainly in the special cases described above in [Containers with Multiple Processes and Special Cases](#containers-with-multiple-processes-and-special-cases). For example, if your application is **simple enough** that setting a default number of processes based on the CPU works well, you don't want to bother with manually configuring the replication at the cluster level, and you are not running more than one container with your app. Or if you are deploying with **Docker Compose**, running on a single server, etc. + +## Deploy the Container Image + +After having a Container (Docker) Image there are several ways to deploy it. + +For example: + +* With **Docker Compose** in a single server +* With a **Kubernetes** cluster +* With a Docker Swarm Mode cluster +* With another tool like Nomad +* With a cloud service that takes your container image and deploys it + +## Docker Image with Poetry + +If you use Poetry to manage your project's dependencies, you could use Docker multi-stage building: + +```{ .dockerfile .annotate } +# (1) +FROM python:3.9 as requirements-stage + +# (2) +WORKDIR /tmp + +# (3) +RUN pip install poetry + +# (4) +COPY ./pyproject.toml ./poetry.lock* /tmp/ + +# (5) +RUN poetry export -f requirements.txt --output requirements.txt --without-hashes + +# (6) +FROM python:3.9 + +# (7) +WORKDIR /code + +# (8) +COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt + +# (9) +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +# (10) +COPY ./app /code/app + +# (11) +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +``` + +1. This is the first stage, it is named `requirements-stage`. + +2. Set `/tmp` as the current working directory. + + Here's where we will generate the file `requirements.txt` + +3. Install Poetry in this Docker stage. + +4. Copy the `pyproject.toml` and `poetry.lock` files to the `/tmp` directory. + + Because it uses `./poetry.lock*` (ending with a `*`), it won't crash if that file is not available yet. + +5. Generate the `requirements.txt` file. + +6. This is the final stage, anything here will be preserved in the final container image. + +7. Set the current working directory to `/code`. + +8. Copy the `requirements.txt` file to the `/code` directory. + + This file only lives in the previous Docker stage, that's why we use `--from-requirements-stage` to copy it. + +9. Install the package dependencies in the generated `requirements.txt` file. + +10. Copy the `app` directory to the `/code` directory. + +11. Run the `uvicorn` command, telling it to use the `app` object imported from `app.main`. + +!!! tip + Click the bubble numbers to see what each line does. + +A **Docker stage** is a part of a `Dockerfile` that works as a **temporary container image** that is only used to generate some files to be used later. + +The first stage will only be used to **install Poetry** and to **generate the `requirements.txt`** with your project dependencies from Poetry's `pyproject.toml` file. + +This `requirements.txt` file will be used with `pip` later in the **next stage**. + +In the final container image **only the final stage** is preserved. The previous stage(s) will be discarded. + +When using Poetry, it would make sense to use **Docker multi-stage builds** because you don't really need to have Poetry and its dependencies installed in the final container image, you **only need** to have the generated `requirements.txt` file to install your project dependencies. + +Then in the next (and final) stage you would build the image more or less in the same way as described before. + +### Behind a TLS Termination Proxy - Poetry + +Again, if you are running your container behind a TLS Termination Proxy (load balancer) like Nginx or Traefik, add the option `--proxy-headers` to the command: + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +## Recap + +Using container systems (e.g. with **Docker** and **Kubernetes**) it becomes fairly straightforward to handle all the **deployment concepts**: + +* HTTPS +* Running on startup +* Restarts +* Replication (the number of processes running) +* Memory +* Previous steps before starting + +In most cases, you probably won't want to use any base image, and instead **build a container image from scratch** one based on the official Python Docker image. + +Taking care of the **order** of instructions in the `Dockerfile` and the **Docker cache** you can **minimize build times**, to maximize your productivity (and avoid boredom). 😎 + +In certain special cases, you might want to use the official Docker image for FastAPI. 🤓 diff --git a/docs/en/docs/deployment/index.md b/docs/en/docs/deployment/index.md index d898cfefc..d1941ad93 100644 --- a/docs/en/docs/deployment/index.md +++ b/docs/en/docs/deployment/index.md @@ -2,6 +2,20 @@ Deploying a **FastAPI** application is relatively easy. +## What Does Deployment Mean + +To **deploy** an application means to perform the necessary steps to make it **available to the users**. + +For a **web API**, it normally involves putting it in a **remote machine**, with a **server program** that provides good performance, stability, etc, so that your **users** can **access** the application efficiently and without interruptions or problems. + +This is in contrast to the the **development** stages, where you are constantly changing the code, breaking it and fixing it, stopping and restarting the development server, etc. + +## Deployment Strategies + There are several ways to do it depending on your specific use case and the tools that you use. -You will see more details to have in mind and some of the techniques to do it in the next sections. +You could **deploy a server** yourself using a combination of tools, you could use a **cloud service** that does part of the work for you, or other possible options. + +I will show you some of the main concepts you should probably have in mind when deploying a **FastAPI** application (although most of it applies to any other type of web application). + +You will see more details to have in mind and some of the techniques to do it in the next sections. ✨ diff --git a/docs/en/docs/deployment/manually.md b/docs/en/docs/deployment/manually.md index daa31a304..80a7df7e6 100644 --- a/docs/en/docs/deployment/manually.md +++ b/docs/en/docs/deployment/manually.md @@ -1,8 +1,26 @@ -# Deploy manually +# Run a Server Manually - Uvicorn -You can deploy **FastAPI** manually as well. +The main thing you need to run a **FastAPI** application in a remote server machine is an ASGI server program like **Uvicorn**. -You just need to install an ASGI compatible server like: +There are 3 main alternatives: + +* Uvicorn: a high performance ASGI server. +* Hypercorn: an ASGI server compatible with HTTP/2 and Trio among other features. +* Daphne: the ASGI server built for Django Channels. + +## Server Machine and Server Program + +There's a small detail about names to have in mind. 💡 + +The word "**server**" is commonly used to refer to both the remote/cloud computer (the physical or virtual machine) and also the program that is running on that machine (e.g. Uvicorn). + +Just have that in mind when you read "server" in general, it could refer to one of those two things. + +When referring to the remote machine, it's common to call it **server**, but also **machine**, **VM** (virtual machine), **node**. Those all refer to some type of remote machine, normally running Linux, where you run programs. + +## Install the Server Program + +You can install an ASGI compatible server with: === "Uvicorn" @@ -39,7 +57,9 @@ You just need to install an ASGI compatible server like: ...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.: +## Run the Server Program + +You can then your application the same way you have done in the tutorials, but without the `--reload` option, e.g.: === "Uvicorn" @@ -65,10 +85,24 @@ And run your application the same way you have done in the tutorials, but withou -You might want to set up some tooling to make sure it is restarted automatically if it stops. +!!! warning + Remember to remove the `--reload` option if you were using it. -You might also want to install Gunicorn and use it as a manager for Uvicorn, or use Hypercorn with multiple workers. + The `--reload` option consumes much more resources, is more unstable, etc. + + It helps a lot during **development**, but you **shouldn't** use it in **production**. -Making sure to fine-tune the number of workers, etc. +## Deployment Concepts -But if you are doing all that, you might just use the Docker image that does it automatically. +These examples run the server program (e.g Uvicorn), starting **a single process**, listening on all the IPs (`0.0.0.0`) on a predefined port (e.g. `80`). + +This is the basic idea. But you will probably want to take care of some additional things, like: + +* Security - HTTPS +* Running on startup +* Restarts +* Replication (the number of processes running) +* Memory +* Previous steps before starting + +I'll tell you more about each of these concepts, how to think about them, and some concrete examples with strategies to handle them in the next chapters. 🚀 diff --git a/docs/en/docs/deployment/server-workers.md b/docs/en/docs/deployment/server-workers.md new file mode 100644 index 000000000..2892d5797 --- /dev/null +++ b/docs/en/docs/deployment/server-workers.md @@ -0,0 +1,178 @@ +# Server Workers - Gunicorn with Uvicorn + +Let's check back those deployment concepts from before: + +* Security - HTTPS +* Running on startup +* Restarts +* **Replication (the number of processes running)** +* Memory +* Previous steps before starting + +Up to this point, with all the tutorials in the docs, you have probably been running a **server program** like Uvicorn, running a **single process**. + +When deploying applications you will probably want to have some **replication of processes** to take advantage of **multiple cores** and to be able to handle more requests. + +As you saw in the previous chapter about [Deployment Concepts](./concepts.md){.internal-link target=_blank}, there are multiple strategies you can use. + +Here I'll show you how to use **Gunicorn** with **Uvicorn worker processes**. + +!!! info + If you are using containers, for example with Docker or Kubernetes, I'll tell you more about that in the next chapter: [FastAPI in Containers - Docker](./docker.md){.internal-link target=_blank}. + + In particular, when running on **Kubernetes** you will probably **not** want to use Gunicorn, and instead run **a single Uvicorn process per container**, but I'll tell you about it later in that chapter. + +## Gunicorn with Uvicorn Workers + +**Gunicorn** is mainly an application server using the **WSGI standard**. That means that Gunicorn can serve applications like Flask and Django. Gunicorn by itself is not compatible with **FastAPI**, as FastAPI uses the newest **ASGI standard**. + +But Gunicorn supports working as a **process manager** and allowing users to tell it which specific **worker process class** to use. Then Gunicorn would start one or more **worker processes** using that class. + +And **Uvicorn** has a **Gunicorn-compatible worker class**. + +Using that combination, Gunicorn would act as a **process manager**, listening on the **port** and the **IP**. And it would **transmit** the communication to the worker processes running the **Uvicorn class**. + +And then the Gunicorn-compatible **Uvicorn worker** class would be in charge of converting the data sent by Gunicorn to the ASGI standard for FastAPI to use it. + +## Install Gunicorn and Uvicorn + +
+ +```console +$ pip install uvicorn[standard] gunicorn + +---> 100% +``` + +
+ +That will install both Uvicorn with the `standard` extra packages (to get high performance) and Gunicorn. + +## Run Gunicorn with Uvicorn Workers + +Then you can run Gunicorn with: + +
+ +```console +$ gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 + +[19499] [INFO] Starting gunicorn 20.1.0 +[19499] [INFO] Listening at: http://0.0.0.0:80 (19499) +[19499] [INFO] Using worker: uvicorn.workers.UvicornWorker +[19511] [INFO] Booting worker with pid: 19511 +[19513] [INFO] Booting worker with pid: 19513 +[19514] [INFO] Booting worker with pid: 19514 +[19515] [INFO] Booting worker with pid: 19515 +[19511] [INFO] Started server process [19511] +[19511] [INFO] Waiting for application startup. +[19511] [INFO] Application startup complete. +[19513] [INFO] Started server process [19513] +[19513] [INFO] Waiting for application startup. +[19513] [INFO] Application startup complete. +[19514] [INFO] Started server process [19514] +[19514] [INFO] Waiting for application startup. +[19514] [INFO] Application startup complete. +[19515] [INFO] Started server process [19515] +[19515] [INFO] Waiting for application startup. +[19515] [INFO] Application startup complete. +``` + +
+ +Let's see what each of those options mean: + +* `main:app`: This is the same syntax used by Uvicorn, `main` means the Python module named "`main`", so, a file `main.py`. And `app` is the name of the variable that is the **FastAPI** application. + * You can imagine that `main:app` is equivalent to a Python `import` statement like: + + ```Python + from main import app + ``` + + * So, the colon in `main:app` would be equivalent to the Python `import` part in `from main import app`. +* `--workers`: The number of worker processes to use, each will run a Uvicorn worker, in this case 4 workers. +* `--worker-class`: The Gunicorn-compatible worker class to use in the worker processes. + * Here we pass the class that Gunicorn can import and use with: + + ```Python + import uvicorn.workers.UvicornWorker + ``` + +* `--bind`: This tells Gunicorn the IP and the port to listen to, using a colon (`:`) to separate the IP and the port. + * If you were running Uvicorn directly, instead of `--bind 0.0.0.0:80` (the Gunicorn option) you would use `--host 0.0.0.0` and `--port 80`. + +In the output you can see that it shows the **PID** (process ID) of each process (it's just a number). + +You can see that: + +* The Gunicorn **process manager** starts with PID `19499` (in your case it will be a different number). +* Then it starts `Listening at: http://0.0.0.0:80`. +* Then it detects that it has to use the worker class at `uvicorn.workers.UvicornWorker`. +* And then it starts **4 workers**, each with its own PID: `19511`, `19513`, `19514`, and `19515`. + +Gunicorn would also take care of managing **dead processes** and **restarting** new ones if needed to keep the number of workers. So that helps in part with the **restart** concept from the list above. + +Nevertheless, you would probably also want to have something outside making sure to **restart Gunicorn** if necessary, and also to **run it on startup**, etc. + +## Uvicorn with Workers + +Uvicorn also has an option to start and run several **worker processes**. + +Nevertheless, as of now, Uvicorn's capabilities for handling worker processes are more limited than Gunicorn's. So, if you want to have a process manager at this level (at the Python level), then it might be better to try with Gunicorn as the process manager. + +In any case, you would run it like this: + +
+ +```console +$ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 +INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit) +INFO: Started parent process [27365] +INFO: Started server process [27368] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27369] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27370] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Started server process [27367] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +The only new option here is `--workers` telling Uvicorn to start 4 worker processes. + +You can also see that it shows the **PID** of each process, `27365` for the parent process (this is the **process manager**) and one for each worker process: `27368`, `27369`, `27370`, and `27367`. + +## Deployment Concepts + +Here you saw how to use **Gunicorn** (or Uvicorn) managing **Uvicorn worker processes** to **parallelize** the execution of the application, take advantage of **multiple cores** in the CPU, and be able to serve **more requests**. + +From the list of deployment concepts from above, using workers would mainly help with the **replication** part, and a little bit with the **restarts**, but you still need to take care of the others: + +* **Security - HTTPS** +* **Running on startup** +* ***Restarts*** +* Replication (the number of processes running) +* **Memory** +* **Previous steps before starting** + +## Containers and Docker + +In the next chapter about [FastAPI in Containers - Docker](./docker.md){.internal-link target=_blank} I'll tell some strategies you could use to handle the other **deployment concepts**. + +I'll also show you the **official Docker image** that includes **Gunicorn with Uvicorn workers** and some default configurations that can be useful for simple cases. + +There I'll also show you how to **build your own image from scratch** to run a single Uvicorn process (without Gunicorn). It is a simple process and is probably what you would want to do when using a distributed container management system like **Kubernetes**. + +## Recap + +You can use **Gunicorn** (or also Uvicorn) as a process manager with Uvicorn workers to take advantage of **multi-core CPUs**, to run **multiple processes in parallel**. + +You could use these tools and ideas if you are setting up **your own deployment system** while taking care of the other deployment concepts yourself. + +Check out the next chapter to learn about **FastAPI** with containers (e.g. Docker and Kubernetes). You will see that those tools have simple ways to solve the other **deployment concepts** as well. ✨ diff --git a/docs/en/docs/img/deployment/concepts/image01.png b/docs/en/docs/img/deployment/concepts/image01.png new file mode 100644 index 0000000000000000000000000000000000000000..fdce75e983509b17ff6fdb0a1bef0f2806791163 GIT binary patch literal 124827 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 s!MNjY0r`9Ne+uG#|5C + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/en/docs/img/deployment/concepts/process-ram.svg b/docs/en/docs/img/deployment/concepts/process-ram.svg new file mode 100644 index 000000000..cd086c36b --- /dev/null +++ b/docs/en/docs/img/deployment/concepts/process-ram.svg @@ -0,0 +1,59 @@ +
Server
Server
RAM
RAM +
CPU
CPU +
Process Manager
Process Manager
Worker Process
Worker Process
Worker Process
Worker Process
Another Process
Another Process
1 GB
1 GB
1 GB
1 GB
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index a11ac6871..b32468783 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -147,9 +147,11 @@ nav: - deployment/index.md - deployment/versions.md - deployment/https.md - - deployment/deta.md - - deployment/docker.md - deployment/manually.md + - deployment/concepts.md + - deployment/deta.md + - deployment/server-workers.md + - deployment/docker.md - project-generation.md - alternatives.md - history-design-future.md From 1b6350ad9e67cd700ecec2d360bd06b946c3231c Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 1 Oct 2021 20:44:53 +0000 Subject: [PATCH 14/38] =?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 f67b11f0f..7206c6a6f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Re-write and extend Deployment guide: Concepts, Uvicorn, Gunicorn, Docker, Containers, Kubernetes. PR [#3974](https://github.com/tiangolo/fastapi/pull/3974) by [@tiangolo](https://github.com/tiangolo). * 📝 Upgrade HTTPS guide with more explanations and diagrams. PR [#3950](https://github.com/tiangolo/fastapi/pull/3950) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add German translation for `docs/features.md`. PR [#3699](https://github.com/tiangolo/fastapi/pull/3699) by [@mawassk](https://github.com/mawassk). * 🎨 Tweak CSS styles for shell animations. PR [#3888](https://github.com/tiangolo/fastapi/pull/3888) by [@tiangolo](https://github.com/tiangolo). From d29aa3d436849c7abb7c5fd6988ef4531b08f9e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 2 Oct 2021 12:16:55 +0200 Subject: [PATCH 15/38] =?UTF-8?q?=E2=9C=A8=20Add=20Deepset=20Sponsorship?= =?UTF-8?q?=20(#3976)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/en/data/sponsors.yml | 3 + docs/en/data/sponsors_badge.yml | 1 + .../en/docs/img/sponsors/haystack-fastapi.svg | 192 ++++++++++++++++++ 4 files changed, 197 insertions(+) create mode 100644 docs/en/docs/img/sponsors/haystack-fastapi.svg diff --git a/README.md b/README.md index bdc8c3b07..233e8ed57 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index e0a4ee783..4949e6c56 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -18,6 +18,9 @@ silver: - url: https://testdriven.io/courses/tdd-fastapi/ title: Learn to build high-quality web apps with best practices img: https://fastapi.tiangolo.com/img/sponsors/testdriven.svg + - url: https://github.com/deepset-ai/haystack/ + title: Build powerful search from composable, open source building blocks + img: https://fastapi.tiangolo.com/img/sponsors/haystack-fastapi.svg bronze: - url: https://calmcode.io title: Code. Simply. Clearly. Calmly. diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 9e95a6255..0496c6279 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -5,3 +5,4 @@ logins: - vimsoHQ - mikeckennedy - koaning + - deepset-ai diff --git a/docs/en/docs/img/sponsors/haystack-fastapi.svg b/docs/en/docs/img/sponsors/haystack-fastapi.svg new file mode 100644 index 000000000..6303ba61f --- /dev/null +++ b/docs/en/docs/img/sponsors/haystack-fastapi.svg @@ -0,0 +1,192 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 1db0fc29535f91fd026d3e439b9663a4ca6a44b1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Oct 2021 10:17:32 +0000 Subject: [PATCH 16/38] =?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 7206c6a6f..062f295fb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add Deepset Sponsorship. PR [#3976](https://github.com/tiangolo/fastapi/pull/3976) by [@tiangolo](https://github.com/tiangolo). * 📝 Re-write and extend Deployment guide: Concepts, Uvicorn, Gunicorn, Docker, Containers, Kubernetes. PR [#3974](https://github.com/tiangolo/fastapi/pull/3974) by [@tiangolo](https://github.com/tiangolo). * 📝 Upgrade HTTPS guide with more explanations and diagrams. PR [#3950](https://github.com/tiangolo/fastapi/pull/3950) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add German translation for `docs/features.md`. PR [#3699](https://github.com/tiangolo/fastapi/pull/3699) by [@mawassk](https://github.com/mawassk). From 4b968c4e3907066a994e125de1fa90768e9c9188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 3 Oct 2021 20:00:28 +0200 Subject: [PATCH 17/38] =?UTF-8?q?=F0=9F=93=9D=20Update=20GraphQL=20docs,?= =?UTF-8?q?=20recommend=20Strawberry=20(#3981)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- docs/de/docs/features.md | 1 - docs/de/docs/index.md | 2 -- docs/en/docs/advanced/graphql.md | 62 +++++++++++++++++++------------- docs/en/docs/alternatives.md | 3 +- docs/en/docs/features.md | 1 - docs/en/docs/index.md | 3 +- docs_src/graphql/tutorial001.py | 26 ++++++++++---- 8 files changed, 59 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 233e8ed57..5bdd9e6db 100644 --- a/README.md +++ b/README.md @@ -418,9 +418,9 @@ For a more complete example including more features, see the 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). +* **GraphQL** integration with Strawberry and other libraries. * Many extra features (thanks to Starlette) as: * **WebSockets** - * **GraphQL** * extremely easy tests based on `requests` and `pytest` * **CORS** * **Cookie Sessions** @@ -447,7 +447,6 @@ Used by Starlette: * 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: diff --git a/docs/de/docs/features.md b/docs/de/docs/features.md index e5b38616f..f56257e7e 100644 --- a/docs/de/docs/features.md +++ b/docs/de/docs/features.md @@ -167,7 +167,6 @@ Mit **FastAPI** bekommen Sie viele von **Starlette**'s Funktionen (da FastAPI nu * Stark beeindruckende Performanz. Es ist eines der schnellsten Python frameworks, auf Augenhöhe mit **NodeJS** und **Go**. * **WebSocket**-Unterstützung. -* **GraphQL**-Unterstützung. * Hintergrundaufgaben im selben Prozess. * Ereignisse für das Starten und Herunterfahren. * Testclient basierend auf `requests`. diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index 95fb7ae21..d09ce70a0 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -425,7 +425,6 @@ For a more complete example including more features, see the 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: diff --git a/docs/en/docs/advanced/graphql.md b/docs/en/docs/advanced/graphql.md index acc1a39f2..154606406 100644 --- a/docs/en/docs/advanced/graphql.md +++ b/docs/en/docs/advanced/graphql.md @@ -1,44 +1,56 @@ # GraphQL -**FastAPI** has optional support for GraphQL (provided by Starlette directly), using the `graphene` library. +As **FastAPI** is based on the **ASGI** standard, it's very easy to integrate any **GraphQL** library also compatible with ASGI. You can combine normal FastAPI *path operations* with GraphQL on the same application. -## Import and use `graphene` +!!! tip + **GraphQL** solves some very specific use cases. -GraphQL is implemented with Graphene, you can check Graphene's docs for more details. + It has **advantages** and **disadvantages** when compared to common **web APIs**. -Import `graphene` and define your GraphQL data: + Make sure you evaluate if the **benefits** for your use case compensate the **drawbacks**. 🤓 -```Python hl_lines="1 6-10" +## GraphQL Libraries + +Here are some of the **GraphQL** libraries that have **ASGI** support. You could use them with **FastAPI**: + +* Strawberry 🍓 + * With docs for FastAPI +* Ariadne + * With docs for Starlette (that also apply to FastAPI) +* Tartiflette + * With Tartiflette ASGI to provide ASGI integration +* Graphene + * With starlette-graphene3 + +## GraphQL with Strawberry + +If you need or want to work with **GraphQL**, **Strawberry** is the **recommended** library as it has the design closest to **FastAPI's** design, it's all based on **type annotations**. + +Depending on your use case, you might prefer to use a different library, but if you asked me, I would probably suggest you try **Strawberry**. + +Here's a small preview of how you could integrate Strawberry with FastAPI: + +```Python hl_lines="3 22 25-26" {!../../../docs_src/graphql/tutorial001.py!} ``` -## Add Starlette's `GraphQLApp` +You can learn more about Strawberry in the Strawberry documentation. -Then import and add Starlette's `GraphQLApp`: +And also the docs about Strawberry with FastAPI. -```Python hl_lines="3 14" -{!../../../docs_src/graphql/tutorial001.py!} -``` +## Older `GraphQLApp` from Starlette -!!! info - Here we are using `.add_route`, that is the way to add a route in Starlette (inherited by FastAPI) without declaring the specific operation (as would be with `.get()`, `.post()`, etc). +Previous versions of Starlette included a `GraphQLApp` class to integrate with Graphene. -## Check it +It was deprecated from Starlette, but if you have code that used it, you can easily **migrate** to starlette-graphene3, that covers the same use case and has an **almost identical interface**. -Run it with Uvicorn and open your browser at http://127.0.0.1:8000. +!!! tip + If you need GraphQL, I still would recommend you check out Strawberry, as it's based on type annotations instead of custom classes and types. -You will see GraphiQL web user interface: +## Learn More - +You can learn more about **GraphQL** in the official GraphQL documentation. -## More details - -For more details, including: - -* Accessing request information -* Adding background tasks -* Using normal or async functions - -check the official Starlette GraphQL docs. +You can also read more about each those libraries described above in their links. diff --git a/docs/en/docs/alternatives.md b/docs/en/docs/alternatives.md index 8b1fa7786..d2792eb0c 100644 --- a/docs/en/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -367,7 +367,6 @@ It has: * Seriously impressive performance. * WebSocket support. -* GraphQL support. * In-process background tasks. * Startup and shutdown events. * Test client built on requests. @@ -375,7 +374,7 @@ It has: * Session and Cookie support. * 100% test coverage. * 100% type annotated codebase. -* Zero hard dependencies. +* Few hard dependencies. Starlette is currently the fastest Python framework tested. Only surpassed by Uvicorn, which is not a framework, but a server. diff --git a/docs/en/docs/features.md b/docs/en/docs/features.md index c92795d4a..36f80783a 100644 --- a/docs/en/docs/features.md +++ b/docs/en/docs/features.md @@ -164,7 +164,6 @@ With **FastAPI** you get all of **Starlette**'s features (as FastAPI is just Sta * Seriously impressive performance. It is one of the fastest Python frameworks available, on par with **NodeJS** and **Go**. * **WebSocket** support. -* **GraphQL** support. * In-process background tasks. * Startup and shutdown events. * Test client built on `requests`. diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 33aadb8ef..ef593153f 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -419,9 +419,9 @@ For a more complete example including more features, see the 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). +* **GraphQL** integration with Strawberry and other libraries. * Many extra features (thanks to Starlette) as: * **WebSockets** - * **GraphQL** * extremely easy tests based on `requests` and `pytest` * **CORS** * **Cookie Sessions** @@ -448,7 +448,6 @@ Used by Starlette: * 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: diff --git a/docs_src/graphql/tutorial001.py b/docs_src/graphql/tutorial001.py index 41da361d3..3b4ca99cf 100644 --- a/docs_src/graphql/tutorial001.py +++ b/docs_src/graphql/tutorial001.py @@ -1,14 +1,26 @@ -import graphene +import strawberry from fastapi import FastAPI -from starlette.graphql import GraphQLApp +from strawberry.asgi import GraphQL -class Query(graphene.ObjectType): - hello = graphene.String(name=graphene.String(default_value="stranger")) +@strawberry.type +class User: + name: str + age: int - def resolve_hello(self, info, name): - return "Hello " + name +@strawberry.type +class Query: + @strawberry.field + def user(self) -> User: + return User(name="Patrick", age=100) + + +schema = strawberry.Schema(query=Query) + + +graphql_app = GraphQL(schema) app = FastAPI() -app.add_route("/", GraphQLApp(schema=graphene.Schema(query=Query))) +app.add_route("/graphql", graphql_app) +app.add_websocket_route("/graphql", graphql_app) From 54c02d402a47cccb337652f033c6ad2e852e768d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 3 Oct 2021 18:01:11 +0000 Subject: [PATCH 18/38] =?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 062f295fb..cf503d50d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update GraphQL docs, recommend Strawberry. PR [#3981](https://github.com/tiangolo/fastapi/pull/3981) by [@tiangolo](https://github.com/tiangolo). * ✨ Add Deepset Sponsorship. PR [#3976](https://github.com/tiangolo/fastapi/pull/3976) by [@tiangolo](https://github.com/tiangolo). * 📝 Re-write and extend Deployment guide: Concepts, Uvicorn, Gunicorn, Docker, Containers, Kubernetes. PR [#3974](https://github.com/tiangolo/fastapi/pull/3974) by [@tiangolo](https://github.com/tiangolo). * 📝 Upgrade HTTPS guide with more explanations and diagrams. PR [#3950](https://github.com/tiangolo/fastapi/pull/3950) by [@tiangolo](https://github.com/tiangolo). From 099c4786554be070eadf47edf80bf4071bccdb47 Mon Sep 17 00:00:00 2001 From: Andy Challis Date: Mon, 4 Oct 2021 05:13:47 +1100 Subject: [PATCH 19/38] =?UTF-8?q?=F0=9F=92=9A=20Fix=20badges=20in=20README?= =?UTF-8?q?=20and=20main=20page=20(#3979)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Anthony Lukach Co-authored-by: Sebastián Ramírez --- .github/workflows/test.yml | 2 ++ README.md | 8 ++++---- docs/en/docs/index.md | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1867cbb00..02507a9d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,8 @@ name: Test on: push: + branches: + - master pull_request: types: [opened, synchronize] diff --git a/README.md b/README.md index 5bdd9e6db..3055e670d 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ FastAPI framework, high performance, easy to learn, fast to code, ready for production

- - Test + + Test Coverage @@ -316,7 +316,7 @@ And now, go to FastAPI framework, high performance, easy to learn, fast to code, ready for production

- - Test + + Test Coverage @@ -317,7 +317,7 @@ And now, go to Date: Sun, 3 Oct 2021 18:14:25 +0000 Subject: [PATCH 20/38] =?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 cf503d50d..bff7aa48f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 💚 Fix badges in README and main page. PR [#3979](https://github.com/tiangolo/fastapi/pull/3979) by [@ghandic](https://github.com/ghandic). * 📝 Update GraphQL docs, recommend Strawberry. PR [#3981](https://github.com/tiangolo/fastapi/pull/3981) by [@tiangolo](https://github.com/tiangolo). * ✨ Add Deepset Sponsorship. PR [#3976](https://github.com/tiangolo/fastapi/pull/3976) by [@tiangolo](https://github.com/tiangolo). * 📝 Re-write and extend Deployment guide: Concepts, Uvicorn, Gunicorn, Docker, Containers, Kubernetes. PR [#3974](https://github.com/tiangolo/fastapi/pull/3974) by [@tiangolo](https://github.com/tiangolo). From 2680f369d0f5ec69645e1cdf42e2b9f197f8edf5 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Tue, 5 Oct 2021 10:06:35 +0200 Subject: [PATCH 21/38] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20Uvicorn=20max?= =?UTF-8?q?=20range=20to=200.15.0=20(#3345)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eddbb92aa..f48d034b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ dev = [ "passlib[bcrypt] >=1.7.2,<2.0.0", "autoflake >=1.3.1,<2.0.0", "flake8 >=3.8.3,<4.0.0", - "uvicorn[standard] >=0.12.0,<0.14.0", + "uvicorn[standard] >=0.12.0,<0.16.0", "graphene >=2.1.8,<3.0.0" ] all = [ @@ -92,7 +92,7 @@ all = [ "ujson >=4.0.1,<5.0.0", "orjson >=3.2.1,<4.0.0", "email_validator >=1.1.1,<2.0.0", - "uvicorn[standard] >=0.12.0,<0.14.0", + "uvicorn[standard] >=0.12.0,<0.16.0", "async_exit_stack >=1.0.1,<2.0.0", "async_generator >=1.10,<2.0.0" ] From 189ac3e280a3b5698ac40d2f412818106e3f05cf Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 5 Oct 2021 08:07:20 +0000 Subject: [PATCH 22/38] =?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 bff7aa48f..fc8e32c9f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Bump Uvicorn max range to 0.15.0. PR [#3345](https://github.com/tiangolo/fastapi/pull/3345) by [@Kludex](https://github.com/Kludex). * 💚 Fix badges in README and main page. PR [#3979](https://github.com/tiangolo/fastapi/pull/3979) by [@ghandic](https://github.com/ghandic). * 📝 Update GraphQL docs, recommend Strawberry. PR [#3981](https://github.com/tiangolo/fastapi/pull/3981) by [@tiangolo](https://github.com/tiangolo). * ✨ Add Deepset Sponsorship. PR [#3976](https://github.com/tiangolo/fastapi/pull/3976) by [@tiangolo](https://github.com/tiangolo). From 8c102814fd9ac0f0ff329273f65a743849cb2267 Mon Sep 17 00:00:00 2001 From: ArcLight_Slavik Date: Tue, 5 Oct 2021 11:40:08 +0300 Subject: [PATCH 23/38] =?UTF-8?q?=E2=AC=86=20Upgrade=20internal=20testing?= =?UTF-8?q?=20dependencies:=20mypy=20to=20version=200.910,=20add=20newly?= =?UTF-8?q?=20needed=20type=20packages=20(#3350)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- mypy.ini | 25 ------------------------- pyproject.toml | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 27 deletions(-) delete mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index e6a33cffb..000000000 --- a/mypy.ini +++ /dev/null @@ -1,25 +0,0 @@ -[mypy] - -# --strict -disallow_any_generics = True -disallow_subclassing_any = True -disallow_untyped_calls = True -disallow_untyped_defs = True -disallow_incomplete_defs = True -check_untyped_defs = True -disallow_untyped_decorators = True -no_implicit_optional = True -warn_redundant_casts = True -warn_unused_ignores = True -warn_return_any = True -implicit_reexport = False -strict_equality = True -# --strict end - -[mypy-fastapi.concurrency] -warn_unused_ignores = False -ignore_missing_imports = True - -[mypy-fastapi.tests.*] -ignore_missing_imports = True -check_untyped_defs = True diff --git a/pyproject.toml b/pyproject.toml index f48d034b3..7c986dbd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ test = [ "pytest >=6.2.4,<7.0.0", "pytest-cov >=2.12.0,<3.0.0", "pytest-asyncio >=0.14.0,<0.15.0", - "mypy ==0.812", + "mypy ==0.910", "flake8 >=3.8.3,<4.0.0", "black ==20.8b1", "isort >=5.0.6,<6.0.0", @@ -63,7 +63,12 @@ test = [ "async_generator >=1.10,<2.0.0", "python-multipart >=0.0.5,<0.0.6", "aiofiles >=0.5.0,<0.6.0", - "flask >=1.1.2,<2.0.0" + "flask >=1.1.2,<2.0.0", + + # types + "types-ujson ==0.1.1", + "types-orjson ==3.6.0", + "types-dataclasses ==0.1.7; python_version<'3.7'", ] doc = [ "mkdocs >=1.1.2,<2.0.0", @@ -101,6 +106,33 @@ all = [ profile = "black" known_third_party = ["fastapi", "pydantic", "starlette"] +[tool.mypy] +# --strict +disallow_any_generics = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_return_any = true +implicit_reexport = false +strict_equality = true +# --strict end + +[[tool.mypy.overrides]] +module = "fastapi.concurrency" +warn_unused_ignores = false +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "fastapi.tests.*" +ignore_missing_imports = true +check_untyped_defs = true + [tool.pytest.ini_options] addopts = [ "--strict-config", From 655db2af1f4175b7ef0dd7004d27325d980b90e4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 5 Oct 2021 08:40:44 +0000 Subject: [PATCH 24/38] =?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 fc8e32c9f..ea32949b2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Upgrade internal testing dependencies: mypy to version 0.910, add newly needed type packages. PR [#3350](https://github.com/tiangolo/fastapi/pull/3350) by [@ArcLightSlavik](https://github.com/ArcLightSlavik). * ⬆️ Bump Uvicorn max range to 0.15.0. PR [#3345](https://github.com/tiangolo/fastapi/pull/3345) by [@Kludex](https://github.com/Kludex). * 💚 Fix badges in README and main page. PR [#3979](https://github.com/tiangolo/fastapi/pull/3979) by [@ghandic](https://github.com/ghandic). * 📝 Update GraphQL docs, recommend Strawberry. PR [#3981](https://github.com/tiangolo/fastapi/pull/3981) by [@tiangolo](https://github.com/tiangolo). From cf5e67590a6585355e3169072732cd73a3de4e28 Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen <3275109+hukkin@users.noreply.github.com> Date: Tue, 5 Oct 2021 10:46:42 +0200 Subject: [PATCH 25/38] =?UTF-8?q?=E2=AC=86=20Upgrade=20required=20Python?= =?UTF-8?q?=20version=20to=20>=3D=203.6.1,=20needed=20by=20typing.Deque,?= =?UTF-8?q?=20used=20by=20Pydantic=20(#2733)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Taneli Hukkinen Co-authored-by: Sebastián Ramírez --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7c986dbd7..b2be66fa1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ requires = [ "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0" ] description-file = "README.md" -requires-python = ">=3.6" +requires-python = ">=3.6.1" [tool.flit.metadata.urls] Documentation = "https://fastapi.tiangolo.com/" From 5607c198c47da11a7998f0ca2d3aad2c0bcb7411 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 5 Oct 2021 08:47:25 +0000 Subject: [PATCH 26/38] =?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 ea32949b2..68c03d0eb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Upgrade required Python version to >= 3.6.1, needed by typing.Deque, used by Pydantic. PR [#2733](https://github.com/tiangolo/fastapi/pull/2733) by [@hukkin](https://github.com/hukkin). * ⬆ Upgrade internal testing dependencies: mypy to version 0.910, add newly needed type packages. PR [#3350](https://github.com/tiangolo/fastapi/pull/3350) by [@ArcLightSlavik](https://github.com/ArcLightSlavik). * ⬆️ Bump Uvicorn max range to 0.15.0. PR [#3345](https://github.com/tiangolo/fastapi/pull/3345) by [@Kludex](https://github.com/Kludex). * 💚 Fix badges in README and main page. PR [#3979](https://github.com/tiangolo/fastapi/pull/3979) by [@ghandic](https://github.com/ghandic). From 8a02a471240b5f7991f05c2ff6bb0f49caa7d198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Tue, 5 Oct 2021 09:58:00 +0100 Subject: [PATCH 27/38] =?UTF-8?q?=E2=9E=96=20Do=20not=20require=20backport?= =?UTF-8?q?s=20in=20Python=20>=3D=203.7=20(#1880)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b2be66fa1..30ebe55cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,11 +59,11 @@ test = [ "databases[sqlite] >=0.3.2,<0.4.0", "orjson >=3.2.1,<4.0.0", "ujson >=4.0.1,<5.0.0", - "async_exit_stack >=1.0.1,<2.0.0", - "async_generator >=1.10,<2.0.0", "python-multipart >=0.0.5,<0.0.6", "aiofiles >=0.5.0,<0.6.0", "flask >=1.1.2,<2.0.0", + "async_exit_stack >=1.0.1,<2.0.0; python_version < '3.7'", + "async_generator >=1.10,<2.0.0; python_version < '3.7'", # types "types-ujson ==0.1.1", @@ -98,8 +98,8 @@ all = [ "orjson >=3.2.1,<4.0.0", "email_validator >=1.1.1,<2.0.0", "uvicorn[standard] >=0.12.0,<0.16.0", - "async_exit_stack >=1.0.1,<2.0.0", - "async_generator >=1.10,<2.0.0" + "async_exit_stack >=1.0.1,<2.0.0; python_version < '3.7'", + "async_generator >=1.10,<2.0.0; python_version < '3.7'", ] [tool.isort] From 69784e514161827f1702444ffd9b50585d79d6eb Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 5 Oct 2021 08:58:41 +0000 Subject: [PATCH 28/38] =?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 68c03d0eb..b9fd722a9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ➖ Do not require backports in Python >= 3.7. PR [#1880](https://github.com/tiangolo/fastapi/pull/1880) by [@FFY00](https://github.com/FFY00). * ⬆ Upgrade required Python version to >= 3.6.1, needed by typing.Deque, used by Pydantic. PR [#2733](https://github.com/tiangolo/fastapi/pull/2733) by [@hukkin](https://github.com/hukkin). * ⬆ Upgrade internal testing dependencies: mypy to version 0.910, add newly needed type packages. PR [#3350](https://github.com/tiangolo/fastapi/pull/3350) by [@ArcLightSlavik](https://github.com/ArcLightSlavik). * ⬆️ Bump Uvicorn max range to 0.15.0. PR [#3345](https://github.com/tiangolo/fastapi/pull/3345) by [@Kludex](https://github.com/Kludex). From ba8c78d87fe3901ed74e57199d4f282ea2ceb5c2 Mon Sep 17 00:00:00 2001 From: SnkSynthesis <63564282+SnkSynthesis@users.noreply.github.com> Date: Tue, 5 Oct 2021 02:17:14 -0700 Subject: [PATCH 29/38] =?UTF-8?q?=E2=AC=86Increase=20supported=20version?= =?UTF-8?q?=20of=20aiofiles=20to=20suppress=20warnings=20(#2899)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 30ebe55cc..49af63663 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ test = [ "orjson >=3.2.1,<4.0.0", "ujson >=4.0.1,<5.0.0", "python-multipart >=0.0.5,<0.0.6", - "aiofiles >=0.5.0,<0.6.0", + "aiofiles >=0.5.0,<0.8.0", "flask >=1.1.2,<2.0.0", "async_exit_stack >=1.0.1,<2.0.0; python_version < '3.7'", "async_generator >=1.10,<2.0.0; python_version < '3.7'", @@ -88,7 +88,7 @@ dev = [ ] all = [ "requests >=2.24.0,<3.0.0", - "aiofiles >=0.5.0,<0.6.0", + "aiofiles >=0.5.0,<0.8.0", "jinja2 >=2.11.2,<3.0.0", "python-multipart >=0.0.5,<0.0.6", "itsdangerous >=1.1.0,<2.0.0", From f6a3bc2902ecec1cd66b36b2f2de146055d8e26b Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 5 Oct 2021 09:17:56 +0000 Subject: [PATCH 30/38] =?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 b9fd722a9..c7e29cd5d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆Increase supported version of aiofiles to suppress warnings. PR [#2899](https://github.com/tiangolo/fastapi/pull/2899) by [@SnkSynthesis](https://github.com/SnkSynthesis). * ➖ Do not require backports in Python >= 3.7. PR [#1880](https://github.com/tiangolo/fastapi/pull/1880) by [@FFY00](https://github.com/FFY00). * ⬆ Upgrade required Python version to >= 3.6.1, needed by typing.Deque, used by Pydantic. PR [#2733](https://github.com/tiangolo/fastapi/pull/2733) by [@hukkin](https://github.com/hukkin). * ⬆ Upgrade internal testing dependencies: mypy to version 0.910, add newly needed type packages. PR [#3350](https://github.com/tiangolo/fastapi/pull/3350) by [@ArcLightSlavik](https://github.com/ArcLightSlavik). From 24749aef7185b2d98f22650a71e73d90f29c7f56 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Oct 2021 11:54:36 +0200 Subject: [PATCH 31/38] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20People?= =?UTF-8?q?=20(#3986)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/people.yml | 228 ++++++++++++++++++++++------------------ 1 file changed, 124 insertions(+), 104 deletions(-) diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 1e4ee30ce..8f1710d33 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1229 - prs: 250 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=05f95ca7fdead36edd9c86be46b4ef6c3c71f876&v=4 + answers: 1230 + prs: 260 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=5cad72c846b7aba2e960546af490edc7375dafc4&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 278 + count: 299 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 url: https://github.com/Kludex - login: dmontagu @@ -14,11 +14,11 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu - login: ycd - count: 217 + count: 219 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 url: https://github.com/ycd - login: Mause - count: 202 + count: 207 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause - login: euri10 @@ -30,7 +30,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 - login: ArcLightSlavik - count: 66 + count: 68 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4 url: https://github.com/ArcLightSlavik - login: falkben @@ -42,7 +42,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen - login: raphaelauv - count: 47 + count: 48 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv - login: includeamin @@ -69,18 +69,26 @@ experts: count: 29 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes +- login: frankie567 + count: 29 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 + url: https://github.com/frankie567 - login: dbanty count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=0cf33e4f40efc2ea206a1189fd63a11344eb88ed&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4 url: https://github.com/dbanty +- login: chbndrhnns + count: 24 + avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 + url: https://github.com/chbndrhnns - login: SirTelemak count: 24 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak -- login: chbndrhnns - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 - url: https://github.com/chbndrhnns +- login: STeveShary + count: 24 + avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 + url: https://github.com/STeveShary - login: acnebs count: 22 avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=bfd127b3e6200f4d00afd714f0fc95c2512df19b&v=4 @@ -89,10 +97,6 @@ experts: count: 22 avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 url: https://github.com/nsidnev -- login: frankie567 - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 - url: https://github.com/frankie567 - login: chris-allnutt count: 21 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 @@ -101,10 +105,18 @@ experts: count: 19 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt +- login: ghandic + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 + url: https://github.com/ghandic - login: Hultner count: 18 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 url: https://github.com/Hultner +- login: panla + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 + url: https://github.com/panla - login: jorgerpo count: 17 avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 @@ -113,42 +125,38 @@ experts: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4 url: https://github.com/nkhitrov +- login: adriangb + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 + url: https://github.com/adriangb - login: waynerv count: 16 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 url: https://github.com/waynerv -- login: adriangb - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 - url: https://github.com/adriangb -- login: panla - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 - url: https://github.com/panla - login: haizaar count: 13 avatarUrl: https://avatars.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4 url: https://github.com/haizaar -- login: STeveShary - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 - url: https://github.com/STeveShary - login: acidjunk - count: 11 + count: 13 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk +- login: David-Lor + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 + url: https://github.com/David-Lor - login: zamiramir count: 11 avatarUrl: https://avatars.githubusercontent.com/u/40475662?u=e58ef61034e8d0d6a312cc956fb09b9c3332b449&v=4 url: https://github.com/zamiramir -- login: David-Lor - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 - url: https://github.com/David-Lor - login: juntatalor count: 11 avatarUrl: https://avatars.githubusercontent.com/u/8134632?v=4 url: https://github.com/juntatalor +- login: dstlny + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 + url: https://github.com/dstlny - login: valentin994 count: 11 avatarUrl: https://avatars.githubusercontent.com/u/42819267?u=fdeeaa9242a59b243f8603496b00994f6951d5a2&v=4 @@ -166,42 +174,42 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/47581948?v=4 url: https://github.com/hellocoldworld last_month_active: -- login: Mause - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 - url: https://github.com/Mause - login: Kludex - count: 9 + count: 13 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 url: https://github.com/Kludex -- login: panla - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 - url: https://github.com/panla -- login: adriangb - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 - url: https://github.com/adriangb -- login: Dustyposa - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 - url: https://github.com/Dustyposa +- login: ghandic + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 + url: https://github.com/ghandic - login: STeveShary - count: 5 + count: 8 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary -- login: Cosmicoppai +- login: Honda-a count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/63765823?u=b97c98ddd6eaf06cd5a0d312db36f97996ac5b23&v=4 - url: https://github.com/Cosmicoppai -- login: AlexanderPodorov + avatarUrl: https://avatars.githubusercontent.com/u/22759187?u=f45bd5fb17b4dca331529b8e9e5eab6122b84b8b&v=4 + url: https://github.com/Honda-a +- login: frankie567 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/54511144?v=4 - url: https://github.com/AlexanderPodorov -- login: SamuelHeath + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 + url: https://github.com/frankie567 +- login: panla count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/12291364?v=4 - url: https://github.com/SamuelHeath + avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 + url: https://github.com/panla +- login: dstlny + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 + url: https://github.com/dstlny +- login: trevorwang + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/121966?v=4 + url: https://github.com/trevorwang +- login: klaa97 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/39653693?v=4 + url: https://github.com/klaa97 top_contributors: - login: waynerv count: 25 @@ -224,19 +232,19 @@ top_contributors: avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 url: https://github.com/mariacamilagl - login: jaystone776 - count: 9 + count: 11 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 url: https://github.com/jaystone776 +- login: Serrones + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 + url: https://github.com/Serrones - login: RunningIkkyu count: 7 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=706e1ee3f248245f2d68b976d149d06fd5a2010d&v=4 url: https://github.com/RunningIkkyu -- login: Serrones - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 - url: https://github.com/Serrones - login: Kludex - count: 6 + count: 7 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 url: https://github.com/Kludex - login: hard-coders @@ -255,6 +263,10 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 url: https://github.com/Attsun1031 +- login: Smlep + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 + url: https://github.com/Smlep - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 @@ -267,15 +279,15 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki -- login: Smlep - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 - url: https://github.com/Smlep top_reviewers: - login: Kludex - count: 85 + count: 90 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 url: https://github.com/Kludex +- login: waynerv + count: 47 + avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 + url: https://github.com/waynerv - login: Laineyzhang55 count: 47 avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 @@ -284,18 +296,18 @@ top_reviewers: count: 46 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi -- login: waynerv - count: 43 - avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 - url: https://github.com/waynerv - login: ycd - count: 41 + count: 43 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 url: https://github.com/ycd - login: AdrianDeAnda - count: 28 + count: 32 avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=bb7f8a0d6c9de4e9d0320a9f271210206e202250&v=4 url: https://github.com/AdrianDeAnda +- login: ArcLightSlavik + count: 27 + avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4 + url: https://github.com/ArcLightSlavik - login: dmontagu count: 23 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 @@ -304,12 +316,8 @@ top_reviewers: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki -- login: ArcLightSlavik - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4 - url: https://github.com/ArcLightSlavik - login: cassiobotaro - count: 18 + count: 19 avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 url: https://github.com/cassiobotaro - login: yanever @@ -320,6 +328,10 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 url: https://github.com/SwftAlpc +- login: hard-coders + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=f2d3d2038c55d86d7f9348f4e6c5e30191e4ee8b&v=4 + url: https://github.com/hard-coders - login: pedabraham count: 15 avatarUrl: https://avatars.githubusercontent.com/u/16860088?u=abf922a7b920bf8fdb7867d8b43e091f1e796178&v=4 @@ -328,10 +340,18 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 +- login: Smlep + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 + url: https://github.com/Smlep - login: lsglucas - count: 13 + count: 14 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas +- login: rjNemo + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 + url: https://github.com/rjNemo - login: RunningIkkyu count: 12 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=706e1ee3f248245f2d68b976d149d06fd5a2010d&v=4 @@ -340,14 +360,6 @@ top_reviewers: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 url: https://github.com/sh0nk -- login: rjNemo - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo -- login: hard-coders - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=f2d3d2038c55d86d7f9348f4e6c5e30191e4ee8b&v=4 - url: https://github.com/hard-coders - login: mariacamilagl count: 10 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -360,10 +372,6 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/7887703?v=4 url: https://github.com/maoyibo -- login: Smlep - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 - url: https://github.com/Smlep - login: PandaHun count: 9 avatarUrl: https://avatars.githubusercontent.com/u/13096845?u=646eba44db720e37d0dbe8e98e77ab534ea78a20&v=4 @@ -400,10 +408,22 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause +- login: solomein-sv + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 + url: https://github.com/solomein-sv +- login: ComicShrimp + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=b3e4d9a14d9a65d429ce62c566aef73178b7111d&v=4 + url: https://github.com/ComicShrimp - login: jovicon count: 6 avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4 url: https://github.com/jovicon +- login: BilalAlpaslan + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 + url: https://github.com/BilalAlpaslan - login: nimctl count: 5 avatarUrl: https://avatars.githubusercontent.com/u/49960770?u=e39b11d47188744ee07b2a1c7ce1a1bdf3c80760&v=4 @@ -416,18 +436,10 @@ top_reviewers: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/63564282?u=0078826509dbecb2fdb543f4e881c9cd06157893&v=4 url: https://github.com/SnkSynthesis -- login: solomein-sv - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 - url: https://github.com/solomein-sv - login: anthonycepeda count: 5 avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=892f700c79f9732211bd5221bf16eec32356a732&v=4 url: https://github.com/anthonycepeda -- login: ComicShrimp - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=b3e4d9a14d9a65d429ce62c566aef73178b7111d&v=4 - url: https://github.com/ComicShrimp - login: oandersonmagalhaes count: 5 avatarUrl: https://avatars.githubusercontent.com/u/83456692?v=4 @@ -440,6 +452,10 @@ top_reviewers: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/23391143?u=56ab6bff50be950fa8cae5cf736f2ae66e319ff3&v=4 url: https://github.com/rkbeatss +- login: izaguerreiro + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 + url: https://github.com/izaguerreiro - login: aviramha count: 4 avatarUrl: https://avatars.githubusercontent.com/u/41201924?u=6883cc4fc13a7b2e60d4deddd4be06f9c5287880&v=4 @@ -452,6 +468,10 @@ top_reviewers: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/31370133?v=4 url: https://github.com/Zxilly +- login: dukkee + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/36825394?u=ccfd86e6a4f2d093dad6f7544cc875af67fa2df8&v=4 + url: https://github.com/dukkee - login: Bluenix2 count: 4 avatarUrl: https://avatars.githubusercontent.com/u/38372706?u=c9d28aff15958d6ebf1971148bfb3154ff943c4f&v=4 From c86a4e1dd3a83f82eafb67fd2b41b8ec8a6b93d4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 5 Oct 2021 09:55:12 +0000 Subject: [PATCH 32/38] =?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 c7e29cd5d..04a4a8c7e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👥 Update FastAPI People. PR [#3986](https://github.com/tiangolo/fastapi/pull/3986) by [@github-actions[bot]](https://github.com/apps/github-actions). * ⬆Increase supported version of aiofiles to suppress warnings. PR [#2899](https://github.com/tiangolo/fastapi/pull/2899) by [@SnkSynthesis](https://github.com/SnkSynthesis). * ➖ Do not require backports in Python >= 3.7. PR [#1880](https://github.com/tiangolo/fastapi/pull/1880) by [@FFY00](https://github.com/FFY00). * ⬆ Upgrade required Python version to >= 3.6.1, needed by typing.Deque, used by Pydantic. PR [#2733](https://github.com/tiangolo/fastapi/pull/2733) by [@hukkin](https://github.com/hukkin). From 96ca425b1f7c4918d8cd2b79342b883c2fb97a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 5 Oct 2021 12:07:40 +0200 Subject: [PATCH 33/38] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Increase=20dependenc?= =?UTF-8?q?y=20ranges=20for=20tests=20and=20docs:=20pytest-cov,=20pytest-a?= =?UTF-8?q?syncio,=20black,=20httpx,=20sqlalchemy,=20databases,=20mkdocs-m?= =?UTF-8?q?arkdownextradata-plugin=20(#3987)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 49af63663..06802aba9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,22 +45,23 @@ Documentation = "https://fastapi.tiangolo.com/" [tool.flit.metadata.requires-extra] test = [ "pytest >=6.2.4,<7.0.0", - "pytest-cov >=2.12.0,<3.0.0", - "pytest-asyncio >=0.14.0,<0.15.0", + "pytest-cov >=2.12.0,<4.0.0", + "pytest-asyncio >=0.14.0,<0.16.0", "mypy ==0.910", "flake8 >=3.8.3,<4.0.0", - "black ==20.8b1", + "black ==21.9b0", "isort >=5.0.6,<6.0.0", "requests >=2.24.0,<3.0.0", - "httpx >=0.14.0,<0.15.0", + "httpx >=0.14.0,<0.19.0", "email_validator >=1.1.1,<2.0.0", - "sqlalchemy >=1.3.18,<1.4.0", + "sqlalchemy >=1.3.18,<1.5.0", "peewee >=3.13.3,<4.0.0", - "databases[sqlite] >=0.3.2,<0.4.0", + "databases[sqlite] >=0.3.2,<0.6.0", "orjson >=3.2.1,<4.0.0", "ujson >=4.0.1,<5.0.0", "python-multipart >=0.0.5,<0.0.6", "aiofiles >=0.5.0,<0.8.0", + # TODO: try to upgrade after upgrading Starlette "flask >=1.1.2,<2.0.0", "async_exit_stack >=1.0.1,<2.0.0; python_version < '3.7'", "async_generator >=1.10,<2.0.0; python_version < '3.7'", @@ -74,7 +75,7 @@ doc = [ "mkdocs >=1.1.2,<2.0.0", "mkdocs-material >=7.1.9,<8.0.0", "mdx-include >=1.4.1,<2.0.0", - "mkdocs-markdownextradata-plugin >=0.1.7,<0.2.0", + "mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0", "typer-cli >=0.0.12,<0.0.13", "pyyaml >=5.3.1,<6.0.0" ] @@ -84,15 +85,19 @@ dev = [ "autoflake >=1.3.1,<2.0.0", "flake8 >=3.8.3,<4.0.0", "uvicorn[standard] >=0.12.0,<0.16.0", + # TODO: remove in the next major version "graphene >=2.1.8,<3.0.0" ] all = [ "requests >=2.24.0,<3.0.0", "aiofiles >=0.5.0,<0.8.0", + # TODO: try to upgrade after upgrading Starlette "jinja2 >=2.11.2,<3.0.0", "python-multipart >=0.0.5,<0.0.6", + # TODO: try to upgrade after upgrading Starlette "itsdangerous >=1.1.0,<2.0.0", "pyyaml >=5.3.1,<6.0.0", + # TODO: remove in the next major version "graphene >=2.1.8,<3.0.0", "ujson >=4.0.1,<5.0.0", "orjson >=3.2.1,<4.0.0", From 9229ba8078897d5ad1210d42b0cf4b672dc06457 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 5 Oct 2021 10:08:26 +0000 Subject: [PATCH 34/38] =?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 04a4a8c7e..dc623d9b0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Increase dependency ranges for tests and docs: pytest-cov, pytest-asyncio, black, httpx, sqlalchemy, databases, mkdocs-markdownextradata-plugin. PR [#3987](https://github.com/tiangolo/fastapi/pull/3987) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#3986](https://github.com/tiangolo/fastapi/pull/3986) by [@github-actions[bot]](https://github.com/apps/github-actions). * ⬆Increase supported version of aiofiles to suppress warnings. PR [#2899](https://github.com/tiangolo/fastapi/pull/2899) by [@SnkSynthesis](https://github.com/SnkSynthesis). * ➖ Do not require backports in Python >= 3.7. PR [#1880](https://github.com/tiangolo/fastapi/pull/1880) by [@FFY00](https://github.com/FFY00). From ae22bca9fea481eb0bd20e08dcd5f89d9e158894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 5 Oct 2021 12:17:31 +0200 Subject: [PATCH 35/38] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20developmen?= =?UTF-8?q?t=20`autoflake`,=20supporting=20multi-line=20imports=20(#3988)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/contributing.md | 14 -------------- pyproject.toml | 2 +- scripts/format-imports.sh | 6 ------ 3 files changed, 1 insertion(+), 21 deletions(-) delete mode 100755 scripts/format-imports.sh diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index cd87faa4e..9ef6e22c8 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -163,20 +163,6 @@ It will also auto-sort all your imports. For it to sort them correctly, you need to have FastAPI installed locally in your environment, with the command in the section above using `--symlink` (or `--pth-file` on Windows). -### Format imports - -There is another script that formats all the imports and makes sure you don't have unused imports: - -

- -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. diff --git a/pyproject.toml b/pyproject.toml index 06802aba9..5b6b272a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ doc = [ dev = [ "python-jose[cryptography] >=3.3.0,<4.0.0", "passlib[bcrypt] >=1.7.2,<2.0.0", - "autoflake >=1.3.1,<2.0.0", + "autoflake >=1.4.0,<2.0.0", "flake8 >=3.8.3,<4.0.0", "uvicorn[standard] >=0.12.0,<0.16.0", # TODO: remove in the next major version diff --git a/scripts/format-imports.sh b/scripts/format-imports.sh deleted file mode 100755 index 1fe193b97..000000000 --- a/scripts/format-imports.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -e -set -x - -# Sort imports one per line, so autoflake can remove unused imports -isort fastapi tests docs_src scripts --force-single-line-imports -sh ./scripts/format.sh From 458a7fbf15034d848ab0f099a965e4c622fb41db Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 5 Oct 2021 10:18:13 +0000 Subject: [PATCH 36/38] =?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 dc623d9b0..005d024ba 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade development `autoflake`, supporting multi-line imports. PR [#3988](https://github.com/tiangolo/fastapi/pull/3988) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Increase dependency ranges for tests and docs: pytest-cov, pytest-asyncio, black, httpx, sqlalchemy, databases, mkdocs-markdownextradata-plugin. PR [#3987](https://github.com/tiangolo/fastapi/pull/3987) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#3986](https://github.com/tiangolo/fastapi/pull/3986) by [@github-actions[bot]](https://github.com/apps/github-actions). * ⬆Increase supported version of aiofiles to suppress warnings. PR [#2899](https://github.com/tiangolo/fastapi/pull/2899) by [@SnkSynthesis](https://github.com/SnkSynthesis). From 557fe61d9298d213f26357439c59bf3002b78979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 5 Oct 2021 12:24:02 +0200 Subject: [PATCH 37/38] =?UTF-8?q?=E2=9C=A8=20Update=20GitHub=20Action:=20n?= =?UTF-8?q?otify-translations,=20to=20avoid=20a=20race=20conditon=20(#3989?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../actions/notify-translations/app/main.py | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/actions/notify-translations/app/main.py b/.github/actions/notify-translations/app/main.py index 79850513a..7d6c1a4d2 100644 --- a/.github/actions/notify-translations/app/main.py +++ b/.github/actions/notify-translations/app/main.py @@ -1,5 +1,7 @@ import logging +import time from pathlib import Path +import random from typing import Dict, Optional import yaml @@ -45,8 +47,11 @@ if __name__ == "__main__": github_event = PartialGitHubEvent.parse_raw(contents) translations_map: Dict[str, int] = yaml.safe_load(translations_path.read_text()) logging.debug(f"Using translations map: {translations_map}") + sleep_time = random.random() * 10 # random number between 0 and 10 seconds pr = repo.get_pull(github_event.pull_request.number) - logging.debug(f"Processing PR: {pr.number}") + logging.debug( + f"Processing PR: {pr.number}, with anti-race condition sleep time: {sleep_time}" + ) if pr.state == "open": logging.debug(f"PR is open: {pr.number}") label_strs = set([label.name for label in pr.get_labels()]) @@ -67,19 +72,31 @@ if __name__ == "__main__": for lang in langs: if lang in translations_map: num = translations_map[lang] - logging.info(f"Found a translation issue for language: {lang} in issue: {num}") + logging.info( + f"Found a translation issue for language: {lang} in issue: {num}" + ) issue = repo.get_issue(num) message = f"Good news everyone! 😉 There's a new translation PR to be reviewed: #{pr.number} 🎉" already_notified = False - logging.info(f"Checking current comments in issue: {num} to see if already notified about this PR: {pr.number}") + time.sleep(sleep_time) + logging.info( + f"Sleeping for {sleep_time} seconds to avoid race conditions and multiple comments" + ) + logging.info( + f"Checking current comments in issue: {num} to see if already notified about this PR: {pr.number}" + ) for comment in issue.get_comments(): if message in comment.body: already_notified = True if not already_notified: - logging.info(f"Writing comment in issue: {num} about PR: {pr.number}") + logging.info( + f"Writing comment in issue: {num} about PR: {pr.number}" + ) issue.create_comment(message) else: - logging.info(f"Issue: {num} was already notified of PR: {pr.number}") + logging.info( + f"Issue: {num} was already notified of PR: {pr.number}" + ) else: logging.info( f"Changing labels in a closed PR doesn't trigger comments, PR: {pr.number}" From 39ec5b33d9d4109333d9429352db792464b159a6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 5 Oct 2021 10:24:47 +0000 Subject: [PATCH 38/38] =?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 005d024ba..1e260585f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Update GitHub Action: notify-translations, to avoid a race conditon. PR [#3989](https://github.com/tiangolo/fastapi/pull/3989) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade development `autoflake`, supporting multi-line imports. PR [#3988](https://github.com/tiangolo/fastapi/pull/3988) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Increase dependency ranges for tests and docs: pytest-cov, pytest-asyncio, black, httpx, sqlalchemy, databases, mkdocs-markdownextradata-plugin. PR [#3987](https://github.com/tiangolo/fastapi/pull/3987) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#3986](https://github.com/tiangolo/fastapi/pull/3986) by [@github-actions[bot]](https://github.com/apps/github-actions).